From 1b4c00f28db0e22c218bf56e6bb56573f4b2c826 Mon Sep 17 00:00:00 2001 From: Max Lv Date: Thu, 15 Oct 2026 10:09:37 +0800 Subject: [PATCH] Add an embedding API: ss_local_start/stop/wait and a C++ wrapper (#3076) starts ss-local on a library-owned thread, stops it from any thread through a libuv async handle and waits for it; 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. --- src/CMakeLists.txt | 4 +- src/local.cpp | 156 +++++++++++++++++++++++++++++++- src/shadowsocks/control.h | 44 +++++++++ src/shadowsocks/shadowsocks.hpp | 84 +++++++++++++++++ tests/CMakeLists.txt | 12 +++ tests/consumer/CMakeLists.txt | 5 +- tests/consumer/main.cpp | 10 ++ tests/test_local_api_cpp.cpp | 109 ++++++++++++++++++++++ 8 files changed, 421 insertions(+), 3 deletions(-) create mode 100644 src/shadowsocks/control.h create mode 100644 src/shadowsocks/shadowsocks.hpp create mode 100644 tests/consumer/main.cpp create mode 100644 tests/test_local_api_cpp.cpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 16d1705b..c60676b1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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() diff --git a/src/local.cpp b/src/local.cpp index 72e6d536..06021d09 100644 --- a/src/local.cpp +++ b/src/local.cpp @@ -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"); diff --git a/src/shadowsocks/control.h b/src/shadowsocks/control.h new file mode 100644 index 00000000..4ba3f41e --- /dev/null +++ b/src/shadowsocks/control.h @@ -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 + +#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 */ diff --git a/src/shadowsocks/shadowsocks.hpp b/src/shadowsocks/shadowsocks.hpp new file mode 100644 index 00000000..aaca6cf8 --- /dev/null +++ b/src/shadowsocks/shadowsocks.hpp @@ -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 , +// so this header adds no C++ ABI to libshadowsocks-c. The destructor stops +// and waits for a running server. + +#include + +#include + +#include + +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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0277352b..6a75a1de 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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) diff --git a/tests/consumer/CMakeLists.txt b/tests/consumer/CMakeLists.txt index 0bbe775f..a9fdafe0 100644 --- a/tests/consumer/CMakeLists.txt +++ b/tests/consumer/CMakeLists.txt @@ -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() diff --git a/tests/consumer/main.cpp b/tests/consumer/main.cpp new file mode 100644 index 00000000..230121cb --- /dev/null +++ b/tests/consumer/main.cpp @@ -0,0 +1,10 @@ +// C++ consumer of the installed headers: the RAII wrapper over the C +// control API, without starting a server. +#include + +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; +} diff --git a/tests/test_local_api_cpp.cpp b/tests/test_local_api_cpp.cpp new file mode 100644 index 00000000..03e6dd28 --- /dev/null +++ b/tests/test_local_api_cpp.cpp @@ -0,0 +1,109 @@ +/* SPDX-License-Identifier: GPL-3.0-or-later */ +// The embedding API (, ): +// 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 +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include "test_helpers.hpp" + +namespace { + +std::atomic 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(&a), sizeof(a)) == 0); + socklen_t len = sizeof(a); + SS_CHECK(getsockname(fd, reinterpret_cast(&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(port)); + a.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + bool ok = false; + if (connect(fd, reinterpret_cast(&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; +}