Add an embedding API: ss_local_start/stop/wait and a C++ wrapper (#3076)

<shadowsocks/control.h> starts ss-local on a library-owned thread,
stops it from any thread through a libuv async handle and waits for it;
<shadowsocks/shadowsocks.hpp> wraps that in a move-only, header-only
shadowsocks::LocalServer whose destructor stops and waits. The library
exports only C symbols, shadowsocks.h is unchanged, and the shared
library version becomes 2.1.0 (SOVERSION 2). test_local_api_cpp covers
the lifecycle; the installed-package consumer gains a C++ program.
This commit is contained in:
Max Lv
2026-10-15 10:09:37 +08:00
committed by GitHub
parent a22281b1ac
commit 1b4c00f28d
8 changed files with 421 additions and 3 deletions
+3 -1
View File
@@ -177,7 +177,7 @@ foreach(kind STATIC SHARED)
target_link_libraries(${target} PRIVATE ${SS_CORE_DEPS} ${SS_ACL_DEPS})
set_target_properties(${target} PROPERTIES OUTPUT_NAME shadowsocks-c)
if(kind STREQUAL "SHARED")
set_target_properties(${target} PROPERTIES VERSION 2.0.0 SOVERSION 2)
set_target_properties(${target} PROPERTIES VERSION 2.1.0 SOVERSION 2)
endif()
ss_project_options(${target})
install(TARGETS ${target}
@@ -188,4 +188,6 @@ foreach(kind STATIC SHARED)
endforeach()
if(SS_BUILD_STATIC_LIBRARY OR SS_BUILD_SHARED_LIBRARY)
install(FILES shadowsocks.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES shadowsocks/control.h shadowsocks/shadowsocks.hpp
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/shadowsocks)
endif()
+155 -1
View File
@@ -41,6 +41,7 @@
#endif
#ifdef LIB_ONLY
#include "shadowsocks.h"
#include "shadowsocks/control.h"
#endif
#if defined(HAVE_SYS_IOCTL_H) && defined(HAVE_NET_IF_H) && defined(__linux__)
@@ -1332,6 +1333,144 @@ Same behavior as `-S`; see that option for details. Short alias: `-S`.
#else
// ------------------------------------------------------------ library API
int _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udata);
struct ss_local_handle {
profile_t profile;
ss_local_callback callback;
void *udata;
uv_thread_t thread;
int result;
bool waited;
bool stop_requested; // guarded by control_lock
};
namespace {
// The running server's stop signal. ss_local_stop() and the server thread
// take the lock around it, so a stop never wakes a loop that is gone.
uv_mutex_t control_lock;
uv_once_t control_once = UV_ONCE_INIT;
uv_async_t *stop_signal = NULL;
ss_local_handle *running_handle = NULL;
bool server_busy = false;
void control_init(void) { uv_mutex_init(&control_lock); }
void on_stop_signal(uv_async_t *) { ss_unloop(SS_DEFAULT, SS_UNLOOP_ALL); }
char *copy_string(const char *s)
{
return s == NULL ? NULL : strdup(s); // heap-ok: startup, library only
}
void free_profile(profile_t *p)
{
free(p->remote_host); // heap-ok: library API, startup
free(p->local_addr); // heap-ok: library API, startup
free(p->method); // heap-ok: library API, startup
free(p->password); // heap-ok: library API, startup
free(p->acl); // heap-ok: library API, startup
free(p->log); // heap-ok: library API, startup
}
void server_thread(void *arg)
{
ss_local_handle *h = (ss_local_handle *)arg;
h->result = _start_ss_local_server(h->profile, h->callback, h->udata);
uv_mutex_lock(&control_lock);
running_handle = NULL;
server_busy = false;
uv_mutex_unlock(&control_lock);
}
} // namespace
extern "C" int
ss_local_start(const profile_t *profile, ss_local_callback callback, void *udata, ss_local_handle **handle)
{
if (profile == NULL || handle == NULL || profile->remote_host == NULL || profile->method == NULL
|| profile->password == NULL) {
return EINVAL;
}
uv_once(&control_once, control_init);
uv_mutex_lock(&control_lock);
if (server_busy) {
uv_mutex_unlock(&control_lock);
return EBUSY;
}
ss_local_handle *h = (ss_local_handle *)calloc(1, sizeof(*h)); // heap-ok: startup, library only
if (h == NULL) {
uv_mutex_unlock(&control_lock);
return ENOMEM;
}
h->profile = *profile;
h->profile.remote_host = copy_string(profile->remote_host);
h->profile.local_addr = copy_string(profile->local_addr);
h->profile.method = copy_string(profile->method);
h->profile.password = copy_string(profile->password);
h->profile.acl = copy_string(profile->acl);
h->profile.log = copy_string(profile->log);
h->callback = callback;
h->udata = udata;
server_busy = true;
running_handle = h;
uv_mutex_unlock(&control_lock);
if (uv_thread_create(&h->thread, server_thread, h) != 0) {
uv_mutex_lock(&control_lock);
server_busy = false;
running_handle = NULL;
uv_mutex_unlock(&control_lock);
free_profile(&h->profile);
free(h); // heap-ok: library API, startup
return ENOMEM;
}
*handle = h;
return 0;
}
extern "C" void
ss_local_stop(ss_local_handle *handle)
{
if (handle == NULL) {
return;
}
uv_once(&control_once, control_init);
uv_mutex_lock(&control_lock);
handle->stop_requested = true;
if (running_handle == handle && stop_signal != NULL) {
uv_async_send(stop_signal);
}
uv_mutex_unlock(&control_lock);
}
extern "C" int
ss_local_wait(ss_local_handle *handle)
{
if (handle == NULL) {
return EINVAL;
}
if (!handle->waited) {
uv_thread_join(&handle->thread);
handle->waited = true;
}
return handle->result;
}
extern "C" void
ss_local_destroy(ss_local_handle *handle)
{
if (handle == NULL) {
return;
}
ss_local_stop(handle);
(void)ss_local_wait(handle);
free_profile(&handle->profile);
free(handle); // heap-ok: library API, startup
}
int
_start_ss_local_server(profile_t profile, ss_local_callback callback, void *udata)
{
@@ -1441,8 +1580,23 @@ _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udat
callback(listen_fd, udp_fd, udata);
}
// ss_local_stop() wakes the loop through this handle.
uv_async_t stop_async;
uv_async_init(ss_native_loop(loop), &stop_async, on_stop_signal);
uv_once(&control_once, control_init);
uv_mutex_lock(&control_lock);
bool stop_now = running_handle != NULL && running_handle->stop_requested;
stop_signal = &stop_async;
uv_mutex_unlock(&control_lock);
// Enter the loop
ss_run(loop, 0);
if (!stop_now)
ss_run(loop, 0);
uv_mutex_lock(&control_lock);
stop_signal = NULL;
uv_mutex_unlock(&control_lock);
uv_close((uv_handle_t *)&stop_async, NULL);
if (verbose) {
LOGI("closed gracefully");
+44
View File
@@ -0,0 +1,44 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef SHADOWSOCKS_CONTROL_H
#define SHADOWSOCKS_CONTROL_H
/*
* Run ss-local inside an application: start it on a thread owned by the
* library, stop it from anywhere, and wait for it to finish.
*
* The server state is process-wide, so one instance runs at a time; a
* second ss_local_start() while one is running fails with EBUSY.
*/
#include <shadowsocks.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct ss_local_handle ss_local_handle;
/*
* Copy the profile and start serving on a new thread. `callback` (may be
* NULL) runs on that thread once the sockets are listening, as with
* start_ss_local_server_with_callback(). Returns 0 and sets *handle, or an
* errno value: EBUSY, EINVAL or ENOMEM.
*/
int ss_local_start(const profile_t *profile, ss_local_callback callback, void *udata, ss_local_handle **handle);
/* Ask the server to stop. Safe from any thread (not from a signal
* handler), and harmless once it has stopped. */
void ss_local_stop(ss_local_handle *handle);
/* Wait for the server thread and return what the server returned (0 after
* a requested stop). Call it once. */
int ss_local_wait(ss_local_handle *handle);
/* Release the handle, stopping and waiting for the server first if needed. */
void ss_local_destroy(ss_local_handle *handle);
#ifdef __cplusplus
}
#endif
#endif /* SHADOWSOCKS_CONTROL_H */
+84
View File
@@ -0,0 +1,84 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef SHADOWSOCKS_SHADOWSOCKS_HPP
#define SHADOWSOCKS_SHADOWSOCKS_HPP
// C++ wrapper for embedding ss-local (header-only; C++17 or later).
//
// shadowsocks::LocalServer server;
// if (int err = server.start(profile)) { ... }
// ...
// server.stop(); // from any thread
// int result = server.wait();
//
// The library exports only the C entry points in <shadowsocks/control.h>,
// so this header adds no C++ ABI to libshadowsocks-c. The destructor stops
// and waits for a running server.
#include <errno.h>
#include <utility>
#include <shadowsocks/control.h>
namespace shadowsocks {
class LocalServer {
public:
LocalServer() noexcept = default;
LocalServer(const LocalServer &) = delete;
LocalServer &operator=(const LocalServer &) = delete;
LocalServer(LocalServer &&other) noexcept : handle_(std::exchange(other.handle_, nullptr)) {}
LocalServer &operator=(LocalServer &&other) noexcept
{
if (this != &other) {
reset();
handle_ = std::exchange(other.handle_, nullptr);
}
return *this;
}
~LocalServer() { reset(); }
// 0 on success, otherwise an errno value (EBUSY when a server already
// runs in this process).
int start(const profile_t &profile, ss_local_callback callback = nullptr, void *udata = nullptr) noexcept
{
if (handle_ != nullptr) {
return EBUSY;
}
return ss_local_start(&profile, callback, udata, &handle_);
}
void stop() noexcept
{
if (handle_ != nullptr) {
ss_local_stop(handle_);
}
}
// The server's return value; the object can start again afterwards.
int wait() noexcept
{
if (handle_ == nullptr) {
return 0;
}
int result = ss_local_wait(handle_);
ss_local_destroy(handle_);
handle_ = nullptr;
return result;
}
bool running() const noexcept { return handle_ != nullptr; }
private:
void reset() noexcept
{
stop();
(void)wait();
}
ss_local_handle *handle_ = nullptr;
};
} // namespace shadowsocks
#endif
+12
View File
@@ -63,6 +63,18 @@ foreach(core_test result pool buffer arena fixed_map ownership)
ss_strict_warnings(test_core_${core_test}.cpp)
endforeach()
# The embedding API on the static library (POSIX: the test uses BSD sockets).
if(TARGET shadowsocks-c-static AND UNIX)
add_executable(test_local_api_cpp test_local_api_cpp.cpp)
target_link_libraries(test_local_api_cpp PRIVATE shadowsocks-c-static Threads::Threads)
target_compile_options(test_local_api_cpp PRIVATE -UNDEBUG)
target_include_directories(test_local_api_cpp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
ss_project_options(test_local_api_cpp)
ss_strict_warnings(test_local_api_cpp.cpp)
add_test(NAME test_local_api_cpp COMMAND test_local_api_cpp)
set_tests_properties(test_local_api_cpp PROPERTIES LABELS "unit" TIMEOUT 60)
endif()
# C++ event layer (src/event) over libuv.
ss_add_test(test_event_cpp "test_event_cpp.cpp" "ss_uv")
ss_strict_warnings(test_event_cpp.cpp)
+4 -1
View File
@@ -1,5 +1,6 @@
cmake_minimum_required(VERSION 3.20)
project(shadowsocks_consumer LANGUAGES C)
project(shadowsocks_consumer LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(SS_PACKAGE_NAME shadowsocks-c CACHE STRING "Installed package to test")
find_package(${SS_PACKAGE_NAME} CONFIG REQUIRED)
foreach(kind shared static)
@@ -18,5 +19,7 @@ foreach(kind shared static)
endif()
add_executable(consumer_${kind} main.c)
target_link_libraries(consumer_${kind} PRIVATE shadowsocks::${kind})
add_executable(consumer_cxx_${kind} main.cpp)
target_link_libraries(consumer_cxx_${kind} PRIVATE shadowsocks::${kind})
endif()
endforeach()
+10
View File
@@ -0,0 +1,10 @@
// C++ consumer of the installed headers: the RAII wrapper over the C
// control API, without starting a server.
#include <shadowsocks/shadowsocks.hpp>
int main()
{
shadowsocks::LocalServer server;
static int (*volatile start)(const profile_t *, ss_local_callback, void *, ss_local_handle **) = ss_local_start;
return server.running() || start == nullptr;
}
+109
View File
@@ -0,0 +1,109 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
// The embedding API (<shadowsocks/control.h>, <shadowsocks/shadowsocks.hpp>):
// start ss-local on the library's thread, talk SOCKS5 to it, refuse a second
// instance, stop it from this thread, and start it again.
#include <atomic>
#include <cerrno>
#include <chrono>
#include <cstring>
#include <thread>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <shadowsocks/shadowsocks.hpp>
#include "test_helpers.hpp"
namespace {
std::atomic<int> ready_fd{ -1 };
void on_ready(int socks_fd, int, void *) { ready_fd = socks_fd; }
int free_port()
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in a{};
a.sin_family = AF_INET;
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
SS_CHECK(bind(fd, reinterpret_cast<sockaddr *>(&a), sizeof(a)) == 0);
socklen_t len = sizeof(a);
SS_CHECK(getsockname(fd, reinterpret_cast<sockaddr *>(&a), &len) == 0);
close(fd);
return ntohs(a.sin_port);
}
bool socks_greeting(int port)
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in a{};
a.sin_family = AF_INET;
a.sin_port = htons(static_cast<uint16_t>(port));
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
bool ok = false;
if (connect(fd, reinterpret_cast<sockaddr *>(&a), sizeof(a)) == 0) {
const unsigned char hello[3] = { 5, 1, 0 };
unsigned char reply[2] = {};
ok = send(fd, hello, sizeof(hello), 0) == 3 && recv(fd, reply, 2, MSG_WAITALL) == 2 && reply[0] == 5
&& reply[1] == 0;
}
close(fd);
return ok;
}
void wait_ready()
{
for (int i = 0; i < 500 && ready_fd < 0; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
SS_CHECK(ready_fd >= 0);
}
void test_lifecycle()
{
int port = free_port();
char host[] = "127.0.0.1", method[] = "aes-256-gcm", password[] = "secret";
profile_t profile{};
profile.remote_host = host;
profile.local_addr = host;
profile.method = method;
profile.password = password;
profile.remote_port = 1; // nothing listens there; only the SOCKS side is exercised
profile.local_port = port;
profile.timeout = 10;
for (int round = 0; round < 2; ++round) {
ready_fd = -1;
shadowsocks::LocalServer server;
SS_CHECK(server.start(profile, &on_ready, nullptr) == 0);
wait_ready();
SS_CHECK(socks_greeting(port));
shadowsocks::LocalServer second;
SS_CHECK(second.start(profile) == EBUSY);
server.stop();
SS_CHECK(server.wait() == 0);
SS_CHECK(!server.running());
}
// A stop before the loop runs is honoured too.
ss_local_handle *h = nullptr;
ready_fd = -1;
SS_CHECK(ss_local_start(&profile, &on_ready, nullptr, &h) == 0);
ss_local_stop(h);
SS_CHECK(ss_local_wait(h) == 0);
ss_local_destroy(h);
SS_CHECK(ss_local_start(nullptr, nullptr, nullptr, &h) == EINVAL);
}
} // namespace
int main()
{
SS_RUN(test_lifecycle);
return 0;
}