Skip to content

Add C csm-tcp-router-client SDK (multi-platform, VS2026, CMake, tests)#40

Merged
nevstop merged 3 commits intoDev_2026Q2from
copilot/add-csm-tcp-router-client-sdk
Apr 27, 2026
Merged

Add C csm-tcp-router-client SDK (multi-platform, VS2026, CMake, tests)#40
nevstop merged 3 commits intoDev_2026Q2from
copilot/add-csm-tcp-router-client-sdk

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 27, 2026

Plan

  • Initial C SDK implementation, build system, VS2026 workspace, tests, examples, CI.
  • Address all 6 PR review comments:
    • mock_server.c – NULL-check every malloc and free partially-allocated state on failure.
    • csm_tcp_router_client.c – WSA bootstrap uses InitOnceExecuteOnce for true one-time init; redundant g_wsa_lock_inited flag removed (cleanup also routed through InitOnceExecuteOnce).
    • csm_tcp_router_client.c – Receive thread snapshots stop_flag/sock under state_lock each iteration.
    • csm_tcp_router_client.c – Removed unconditional #define CSM_BUILD_LIBRARY (controlled by build system only).
    • csm_tcp_router_client.c – csm_do_connect checks fcntl return values and skips to the next addrinfo on failure.
    • csm_tcp_router_client.h – Updated csm_client_last_server_error doc to match implemented behaviour.

All 22 tests pass on Release and under ASan + UBSan on Linux.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new portable C (C99) client SDK under SDK/c/ for the CSM-TCP-Router project, including cross-platform build tooling (CMake + VS2026), an in-process mock server, and a test suite with CI.

Changes:

  • Introduces the C SDK public API (include/) and implementation (src/) with a background receive thread, protocol codec, and sync/async APIs.
  • Adds unit + integration tests (including a cross-platform in-process MockServer) and example programs.
  • Adds build & developer workflows: CMake project, VS2026 solution/projects, and a GitHub Actions workflow running ctest across OSes (+ sanitizers on Linux).

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
.github/workflows/C_SDK.yml CI workflow for building/testing the C SDK on Linux/macOS/Windows + sanitizers on Linux
SDK/c/.gitignore Ignores CMake and Visual Studio build artifacts for the C SDK
SDK/c/CMakeLists.txt Cross-platform CMake build for library, tests, and examples
SDK/c/CHANGELOG.md Initial changelog entry for C SDK v0.1.0
SDK/c/LICENSE MIT license file for the C SDK distribution
SDK/c/README.md English documentation for the C SDK (features, build, API, tests)
SDK/c/README.zh-cn.md Chinese documentation for the C SDK (features, build, API, tests)
SDK/c/examples/basic_usage.c Example showing connect/ping/list and basic command flow
SDK/c/examples/subscribe_status.c Example showing status subscription callback usage
SDK/c/include/csm_tcp_router_client.h Public C API: types, protocol helpers, client API, docs
SDK/c/src/csm_tcp_router_client.c Single-file implementation: platform shims, protocol codec, client logic, threading
SDK/c/tests/mock_server.c Cross-platform in-process mock server implementation used by integration tests
SDK/c/tests/mock_server.h Mock server public test fixture header
SDK/c/tests/test_client.c Unit tests covering client lifecycle + invalid args + unreachable server behavior
SDK/c/tests/test_harness.h Minimal assertion/test harness used by the C test suite
SDK/c/tests/test_integration.c End-to-end tests against the in-process mock server
SDK/c/tests/test_main.c Test runner that registers and executes all tests
SDK/c/tests/test_protocol.c Unit tests for protocol encode/decode/parse helpers
SDK/c/vs2026/README.md Documentation for building/running tests via Visual Studio 2026
SDK/c/vs2026/csm_tcp_router_client.sln VS2026 solution containing library + test executable projects
SDK/c/vs2026/csm_tcp_router_client/csm_tcp_router_client.vcxproj VS2026 static library project configuration
SDK/c/vs2026/csm_tcp_router_client/csm_tcp_router_client.vcxproj.filters VS project filters for source/header organization
SDK/c/vs2026/csm_tcp_router_client.tests/csm_tcp_router_client.tests.vcxproj VS2026 console test executable project configuration
SDK/c/vs2026/csm_tcp_router_client.tests/csm_tcp_router_client.tests.vcxproj.filters VS test project filters for source/header organization

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread SDK/c/tests/mock_server.c Outdated
Comment on lines +496 to +499
r->cmd = (char *)malloc(strlen(cmd_text) + 1); strcpy(r->cmd, cmd_text);
r->type = type;
size_t dl = strlen(data);
r->data = (uint8_t *)malloc(dl); if (dl) memcpy(r->data, data, dl);
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ms_set_response_typed() does not check allocation results before using them: malloc(strlen(cmd_text)+1) is immediately passed to strcpy, and malloc(dl) can be NULL but is still used by memcpy when dl > 0. This can crash tests under low-memory conditions or sanitizers. Add NULL checks and free r on failure before returning.

Suggested change
r->cmd = (char *)malloc(strlen(cmd_text) + 1); strcpy(r->cmd, cmd_text);
r->type = type;
size_t dl = strlen(data);
r->data = (uint8_t *)malloc(dl); if (dl) memcpy(r->data, data, dl);
r->cmd = (char *)malloc(strlen(cmd_text) + 1);
if (!r->cmd) {
free(r);
return;
}
strcpy(r->cmd, cmd_text);
r->type = type;
size_t dl = strlen(data);
if (dl > 0) {
r->data = (uint8_t *)malloc(dl);
if (!r->data) {
free(r->cmd);
free(r);
return;
}
memcpy(r->data, data, dl);
}

Copilot uses AI. Check for mistakes.
Comment thread SDK/c/src/csm_tcp_router_client.c Outdated
Comment on lines +165 to +180
static int g_wsa_lock_inited = 0;
static int g_wsa_refcount = 0;

static void csm_wsa_lock_init_once(void) {
/* Process-wide one-time init. Safe because csm_client_create is
* the only caller and the OS guarantees serialisation of the very
* first call by virtue of program startup ordering; subsequent
* concurrent callers race only on the lock-init itself, but
* InitializeCriticalSection is idempotent-safe enough for our use
* since we only ever protect refcount. In practice clients should
* create the first instance from a single thread.
*/
if (!g_wsa_lock_inited) {
csm_mutex_init(&g_wsa_lock);
g_wsa_lock_inited = 1;
}
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

csm_wsa_lock_init_once() is not actually thread-safe: multiple concurrent callers can race on g_wsa_lock_inited and call InitializeCriticalSection on the same storage more than once, which is undefined behavior. Use a proper one-time init primitive (e.g., InitOnceExecuteOnce, InterlockedCompareExchange guard, or a function-local static initialized via InitOnce) to guarantee single initialization of g_wsa_lock.

Suggested change
static int g_wsa_lock_inited = 0;
static int g_wsa_refcount = 0;
static void csm_wsa_lock_init_once(void) {
/* Process-wide one-time init. Safe because csm_client_create is
* the only caller and the OS guarantees serialisation of the very
* first call by virtue of program startup ordering; subsequent
* concurrent callers race only on the lock-init itself, but
* InitializeCriticalSection is idempotent-safe enough for our use
* since we only ever protect refcount. In practice clients should
* create the first instance from a single thread.
*/
if (!g_wsa_lock_inited) {
csm_mutex_init(&g_wsa_lock);
g_wsa_lock_inited = 1;
}
static INIT_ONCE g_wsa_lock_init_once_state = INIT_ONCE_STATIC_INIT;
static int g_wsa_refcount = 0;
static BOOL CALLBACK csm_wsa_lock_init_once_callback(PINIT_ONCE init_once,
PVOID param,
PVOID *context) {
(void)init_once;
(void)param;
(void)context;
csm_mutex_init(&g_wsa_lock);
return TRUE;
}
static void csm_wsa_lock_init_once(void) {
InitOnceExecuteOnce(&g_wsa_lock_init_once_state,
csm_wsa_lock_init_once_callback,
NULL,
NULL);

Copilot uses AI. Check for mistakes.
Comment thread SDK/c/src/csm_tcp_router_client.c Outdated
Comment on lines +1009 to +1016
if (c->stop_flag) break;
if (csm_recv_all(c->sock, header, CSM_HEADER_SIZE) != 0) break;
uint32_t data_len = csm_unpack_be32(header);
uint8_t *body = NULL;
if (data_len > 0) {
body = (uint8_t *)malloc(data_len);
if (!body) break;
if (csm_recv_all(c->sock, body, data_len) != 0) {
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The receive thread reads c->stop_flag and c->sock without holding state_lock (or using atomics), while csm_client_disconnect() mutates them under the lock. This introduces a data race (and can be flagged by TSAN/UBSan). Consider making these fields atomic, or always reading them under state_lock (e.g., snapshot stop_flag/sock under lock each loop iteration).

Suggested change
if (c->stop_flag) break;
if (csm_recv_all(c->sock, header, CSM_HEADER_SIZE) != 0) break;
uint32_t data_len = csm_unpack_be32(header);
uint8_t *body = NULL;
if (data_len > 0) {
body = (uint8_t *)malloc(data_len);
if (!body) break;
if (csm_recv_all(c->sock, body, data_len) != 0) {
int stop_flag;
csm_socket_t sock;
csm_mutex_lock(&c->state_lock);
stop_flag = c->stop_flag;
sock = c->sock;
csm_mutex_unlock(&c->state_lock);
if (stop_flag) break;
if (sock == CSM_INVALID_SOCKET) break;
if (csm_recv_all(sock, header, CSM_HEADER_SIZE) != 0) break;
uint32_t data_len = csm_unpack_be32(header);
uint8_t *body = NULL;
if (data_len > 0) {
body = (uint8_t *)malloc(data_len);
if (!body) break;
if (csm_recv_all(sock, body, data_len) != 0) {

Copilot uses AI. Check for mistakes.
Comment thread SDK/c/src/csm_tcp_router_client.c Outdated
Comment on lines +33 to +34
#define CSM_BUILD_LIBRARY 1

Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This translation unit unconditionally defines CSM_BUILD_LIBRARY. Build/ABI selection macros like this should be controlled by the build system (CMake/MSBuild) rather than hard-coded in the source, otherwise consumers that compile the .c directly or build a shared library can end up with incorrect CSM_API import/export decoration. Consider removing this #define and relying on target_compile_definitions / project preprocessor definitions instead.

Suggested change
#define CSM_BUILD_LIBRARY 1

Copilot uses AI. Check for mistakes.
Comment thread SDK/c/src/csm_tcp_router_client.c Outdated
ioctlsocket(sock, FIONBIO, &mode);
#else
int flags = fcntl(sock, F_GETFL, 0);
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On POSIX, the return value from fcntl(sock, F_GETFL, 0) is not checked. If it returns -1, the subsequent F_SETFL uses an invalid flags value, which can break the non-blocking connect logic. Handle F_GETFL/F_SETFL failures (e.g., treat as CSM_ERR_CONNECTION and try the next addrinfo) to avoid undefined socket state.

Suggested change
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
if (flags == -1 || fcntl(sock, F_SETFL, flags | O_NONBLOCK) == -1) {
csm_close_socket(sock);
sock = CSM_INVALID_SOCKET;
continue;
}

Copilot uses AI. Check for mistakes.
Comment thread SDK/c/include/csm_tcp_router_client.h Outdated
Comment on lines +385 to +388
/** Retrieve information about the last CSM_ERR_SERVER returned by *client*.
*
* Returns CSM_OK and fills *out_err* if a server error has been observed
* since the last successful call, otherwise returns CSM_ERR_STATE.
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc says server errors are returned only if observed "since the last successful call", which implies the stored error state should be cleared after returning CSM_OK. The implementation never clears has_server_error, so repeated calls will keep returning the same error. Either update the doc to match the current behavior or clear the flag in csm_client_last_server_error() after copying last_server_error.

Suggested change
/** Retrieve information about the last CSM_ERR_SERVER returned by *client*.
*
* Returns CSM_OK and fills *out_err* if a server error has been observed
* since the last successful call, otherwise returns CSM_ERR_STATE.
/** Retrieve information about the last CSM_ERR_SERVER observed by *client*.
*
* Returns CSM_OK and fills *out_err* if a server error has been observed;
* otherwise returns CSM_ERR_STATE.

Copilot uses AI. Check for mistakes.
@nevstop nevstop marked this pull request as ready for review April 27, 2026 12:57
@nevstop
Copy link
Copy Markdown
Contributor

nevstop commented Apr 27, 2026

@copilot 根据Comment修复

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 27, 2026

@copilot 根据Comment修复

已根据 6 条评审意见修复(commit 6bd3f21):

  1. mock_server.c ms_set_response_typed:每次 malloc 都做 NULL 检查,失败时释放已分配的部分。
  2. csm_tcp_router_client.c Windows WSA 初始化:改用 InitOnceExecuteOnce,并删除冗余的 g_wsa_lock_inited 标志,csm_wsa_cleanup 也走同一个 InitOnce
  3. 接收线程:每轮循环在 state_lock 内读取 stop_flagsock 的快照,消除与 csm_client_disconnect() 的数据竞争。
  4. 移除源文件里硬写的 #define CSM_BUILD_LIBRARY,改由 CMake / MSBuild 工程定义,避免下游用户把 .c 编进自己的 DLL 时 __declspec 装饰错误。
  5. POSIX csm_do_connect:检查 fcntl(F_GETFL/F_SETFL) 返回值,失败时关闭该 socket 并尝试下一个 addrinfo
  6. csm_client_last_server_error 的文档:更新描述以匹配实际行为(保留最近一次 server error,不会被中间的成功调用清除)。

ASan + UBSan 下 22 条测试全部通过。

Copilot AI requested a review from nevstop April 27, 2026 13:07
@nevstop nevstop merged commit aa7d605 into Dev_2026Q2 Apr 27, 2026
@nevstop nevstop deleted the copilot/add-csm-tcp-router-client-sdk branch April 27, 2026 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants