mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-08-05 06:23:19 +00:00
feat: generated sync wrappers
This commit is contained in:
parent
3e57751e3a
commit
a7cba1a4a3
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
@ -93,6 +93,9 @@ jobs:
|
||||
nimble-version: ${{ needs.versions.outputs.nimble }}
|
||||
|
||||
cpp-e2e:
|
||||
# Native-binding e2e across the OS matrix. C++ runs on every OS; the C and
|
||||
# Rust suites (added below) run Linux-only, so the job id stays `cpp-e2e`
|
||||
# for the required-check name even though it now covers all three backends.
|
||||
# Codegen output doesn't vary with mm, so we matrix over OS and Nim only.
|
||||
# Windows runs MSVC by default and may surface codegen tweaks needed in
|
||||
# the generated CMake (e.g. /EHsc, dllexport) — track follow-ups as bugs
|
||||
@ -176,6 +179,21 @@ jobs:
|
||||
shell: bash
|
||||
run: nimble test_c_abi_e2e -y
|
||||
|
||||
# CBOR C bindings: exercises the async callback API and the blocking
|
||||
# `_sync` wrappers. Linux-only (codegen is platform-checked by
|
||||
# check-bindings; one OS is enough for the runtime round-trip).
|
||||
- name: Run C e2e tests
|
||||
if: matrix.label == 'Linux'
|
||||
shell: bash
|
||||
run: nimble test_c_e2e -y
|
||||
|
||||
# Rust bindings: drives the generated blocking + tokio-async wrappers
|
||||
# through the full FFI round-trip. Linux-only for the same reason.
|
||||
- name: Run Rust e2e tests
|
||||
if: matrix.label == 'Linux'
|
||||
shell: bash
|
||||
run: nimble test_rust_e2e -y
|
||||
|
||||
check-bindings:
|
||||
# Single OS is enough — codegen output is platform-independent; the Nim
|
||||
# matrix catches version-sensitive output (the PR #39 drift class).
|
||||
|
||||
3
.github/workflows/tests-sanitized.yml
vendored
3
.github/workflows/tests-sanitized.yml
vendored
@ -102,5 +102,8 @@ jobs:
|
||||
- name: Run C++ e2e tests (${{ inputs.sanitizer }})
|
||||
run: nimble test_cpp_e2e_sanitized -y
|
||||
|
||||
- name: Run C e2e tests (${{ inputs.sanitizer }})
|
||||
run: nimble test_c_e2e_sanitized -y
|
||||
|
||||
- name: Run abi=c C e2e test (${{ inputs.sanitizer }})
|
||||
run: nimble test_c_abi_e2e_sanitized -y
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,6 +26,7 @@ examples/**/cpp_bindings/build/
|
||||
|
||||
# Cargo build artifacts (rust clients / harnesses)
|
||||
examples/**/rust_client/target/
|
||||
tests/e2e/rust/target/
|
||||
|
||||
# Development plans (local only — match PLAN.md and any `*-plan.md` notes)
|
||||
PLAN.md
|
||||
|
||||
@ -28,9 +28,9 @@ find_package(Threads REQUIRED)
|
||||
add_library(echo_headers INTERFACE)
|
||||
target_include_directories(echo_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_link_libraries(echo_headers INTERFACE echo tinycbor Threads::Threads)
|
||||
# The generated header is async (no blocking helper), but consumer code that
|
||||
# waits on a result callback typically uses nanosleep / pthreads, which need a
|
||||
# POSIX feature level that strict `-std=c11` hides. Define it for consumers.
|
||||
# The generated `_sync` wrappers block on a pthread condvar, and consumer code
|
||||
# that polls a result callback typically uses nanosleep — both need a POSIX
|
||||
# feature level that strict `-std=c11` hides. Define it for consumers.
|
||||
target_compile_definitions(echo_headers INTERFACE _POSIX_C_SOURCE=200809L)
|
||||
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c")
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#ifndef NIM_FFI_LIB_ECHO_H_INCLUDED
|
||||
#define NIM_FFI_LIB_ECHO_H_INCLUDED
|
||||
#include "nim_ffi_cbor.h"
|
||||
#include "nim_ffi_sync.h"
|
||||
|
||||
/* ============================================================ */
|
||||
/* Generated types (user-declared + per-proc request envelopes) */
|
||||
@ -287,6 +288,67 @@ static inline int echo_ctx_create(const EchoConfig* config, EchoCreateFn on_crea
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int echo_ctx_create_sync(const EchoConfig* config, EchoCtx** out, char* err_buf, size_t err_len, uint32_t timeout_ms) {
|
||||
EchoCreateCtorReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
ffi_req.config = *config;
|
||||
uint8_t* req_buf = NULL;
|
||||
size_t req_len = 0;
|
||||
char* enc_err = NULL;
|
||||
if (nimffi_encode_to_buf(echo_encv_EchoCreateCtorReq, &ffi_req, &req_buf, &req_len, &enc_err) != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, enc_err ? enc_err : "encode failed");
|
||||
free(enc_err);
|
||||
return -1;
|
||||
}
|
||||
NimFfiSyncState* st = nimffi_sync_state_new();
|
||||
if (!st) {
|
||||
free(req_buf);
|
||||
nimffi_copy_err(err_buf, err_len, "out of memory");
|
||||
return -1;
|
||||
}
|
||||
(void)echo_create(req_buf, req_len, nimffi_sync_cb, st);
|
||||
free(req_buf);
|
||||
if (!nimffi_sync_wait(st, timeout_ms)) {
|
||||
nimffi_copy_err(err_buf, err_len, "FFI create timed out");
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
if (!st->ok) {
|
||||
nimffi_copy_err(err_buf, err_len, st->err ? st->err : "FFI create failed");
|
||||
int rc = st->ret_code ? st->ret_code : -1;
|
||||
nimffi_sync_state_release(st);
|
||||
return rc;
|
||||
}
|
||||
NimFfiStr addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
char* dec_err = NULL;
|
||||
if (nimffi_decode_from_buf(echo_decv_Str, st->bytes, st->bytes_len, &addr, &dec_err) != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, dec_err ? dec_err : "decode failed");
|
||||
free(dec_err);
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
char* endp = NULL;
|
||||
unsigned long long a = addr.data ? strtoull(addr.data, &endp, 10) : 0;
|
||||
bool ok = addr.data && addr.len > 0 && endp && *endp == '\0';
|
||||
nimffi_free_str(&addr);
|
||||
if (!ok) {
|
||||
nimffi_copy_err(err_buf, err_len, "FFI create returned non-numeric address");
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
EchoCtx* c = (EchoCtx*)calloc(1, sizeof(EchoCtx));
|
||||
if (!c) {
|
||||
nimffi_copy_err(err_buf, err_len, "out of memory");
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
c->ptr = (void*)(uintptr_t)a;
|
||||
*out = c;
|
||||
nimffi_sync_state_release(st);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline void echo_ctx_destroy(EchoCtx* ctx) {
|
||||
if (!ctx) return;
|
||||
if (ctx->ptr) { echo_destroy(ctx->ptr); ctx->ptr = NULL; }
|
||||
@ -353,6 +415,57 @@ static inline int echo_ctx_shout(const EchoCtx* ctx, const ShoutRequest* req, Ec
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int echo_ctx_shout_sync(const EchoCtx* ctx, const ShoutRequest* req, ShoutResponse* out, char* err_buf, size_t err_len, uint32_t timeout_ms) {
|
||||
EchoShoutReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
ffi_req.req = *req;
|
||||
uint8_t* req_buf = NULL;
|
||||
size_t req_len = 0;
|
||||
char* enc_err = NULL;
|
||||
if (nimffi_encode_to_buf(echo_encv_EchoShoutReq, &ffi_req, &req_buf, &req_len, &enc_err) != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, enc_err ? enc_err : "encode failed");
|
||||
free(enc_err);
|
||||
return -1;
|
||||
}
|
||||
NimFfiSyncState* st = nimffi_sync_state_new();
|
||||
if (!st) {
|
||||
free(req_buf);
|
||||
nimffi_copy_err(err_buf, err_len, "out of memory");
|
||||
return -1;
|
||||
}
|
||||
int ret = echo_shout(ctx->ptr, nimffi_sync_cb, st, req_buf, req_len);
|
||||
free(req_buf);
|
||||
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
|
||||
nimffi_copy_err(err_buf, err_len, "RET_MISSING_CALLBACK (internal error)");
|
||||
nimffi_sync_state_release(st);
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
if (!nimffi_sync_wait(st, timeout_ms)) {
|
||||
nimffi_copy_err(err_buf, err_len, "FFI call timed out");
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
if (!st->ok) {
|
||||
nimffi_copy_err(err_buf, err_len, st->err ? st->err : "FFI call failed");
|
||||
int rc = st->ret_code ? st->ret_code : -1;
|
||||
nimffi_sync_state_release(st);
|
||||
return rc;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
char* dec_err = NULL;
|
||||
int dec = nimffi_decode_from_buf(echo_decv_ShoutResponse, st->bytes, st->bytes_len, out, &dec_err);
|
||||
if (dec != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, dec_err ? dec_err : "decode failed");
|
||||
free(dec_err);
|
||||
echo_free_ShoutResponse(out);
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
nimffi_sync_state_release(st);
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef void (*EchoVersionReplyFn)(int err_code, const NimFfiStr* reply, const char* err_msg, void* user_data);
|
||||
typedef struct { EchoVersionReplyFn fn; void* user_data; } EchoVersionCallBox;
|
||||
static void echo_version_reply_trampoline(int ret, const char* msg, size_t len, void* ud) {
|
||||
@ -412,4 +525,54 @@ static inline int echo_ctx_version(const EchoCtx* ctx, EchoVersionReplyFn on_rep
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int echo_ctx_version_sync(const EchoCtx* ctx, NimFfiStr* out, char* err_buf, size_t err_len, uint32_t timeout_ms) {
|
||||
EchoVersionReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
uint8_t* req_buf = NULL;
|
||||
size_t req_len = 0;
|
||||
char* enc_err = NULL;
|
||||
if (nimffi_encode_to_buf(echo_encv_EchoVersionReq, &ffi_req, &req_buf, &req_len, &enc_err) != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, enc_err ? enc_err : "encode failed");
|
||||
free(enc_err);
|
||||
return -1;
|
||||
}
|
||||
NimFfiSyncState* st = nimffi_sync_state_new();
|
||||
if (!st) {
|
||||
free(req_buf);
|
||||
nimffi_copy_err(err_buf, err_len, "out of memory");
|
||||
return -1;
|
||||
}
|
||||
int ret = echo_version(ctx->ptr, nimffi_sync_cb, st, req_buf, req_len);
|
||||
free(req_buf);
|
||||
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
|
||||
nimffi_copy_err(err_buf, err_len, "RET_MISSING_CALLBACK (internal error)");
|
||||
nimffi_sync_state_release(st);
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
if (!nimffi_sync_wait(st, timeout_ms)) {
|
||||
nimffi_copy_err(err_buf, err_len, "FFI call timed out");
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
if (!st->ok) {
|
||||
nimffi_copy_err(err_buf, err_len, st->err ? st->err : "FFI call failed");
|
||||
int rc = st->ret_code ? st->ret_code : -1;
|
||||
nimffi_sync_state_release(st);
|
||||
return rc;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
char* dec_err = NULL;
|
||||
int dec = nimffi_decode_from_buf(echo_decv_Str, st->bytes, st->bytes_len, out, &dec_err);
|
||||
if (dec != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, dec_err ? dec_err : "decode failed");
|
||||
free(dec_err);
|
||||
nimffi_free_str(out);
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
nimffi_sync_state_release(st);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* NIM_FFI_LIB_ECHO_H_INCLUDED */
|
||||
|
||||
193
examples/echo/c_bindings/nim_ffi_sync.h
Normal file
193
examples/echo/c_bindings/nim_ffi_sync.h
Normal file
@ -0,0 +1,193 @@
|
||||
#ifndef NIM_FFI_SYNC_HELPER_H_INCLUDED
|
||||
#define NIM_FFI_SYNC_HELPER_H_INCLUDED
|
||||
/* Blocking-call helper shared by the generated <lib>_ctx_<method>_sync wrappers.
|
||||
* Turns the async callback ABI into a synchronous request/reply: submit the
|
||||
* call, block on a condition variable until its single callback fires (or the
|
||||
* timeout elapses), then hand the raw CBOR payload / error text back so the
|
||||
* generated wrapper can decode it into a caller-owned out-param.
|
||||
*
|
||||
* The state is heap-allocated and reference-counted (2: the waiter and the
|
||||
* callback). A callback that fires after the caller has already timed out thus
|
||||
* writes into a still-live object and drops the last reference cleanly — no
|
||||
* use-after-free, no leak. Uses pthreads on POSIX and SRWLOCK/CONDITION_VARIABLE
|
||||
* on Win32, the same platform split the example programs use. */
|
||||
#include "nim_ffi_cbor.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
# include <windows.h>
|
||||
#else
|
||||
# include <pthread.h>
|
||||
# include <errno.h>
|
||||
# include <time.h>
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
#if defined(_WIN32)
|
||||
SRWLOCK lock;
|
||||
CONDITION_VARIABLE cv;
|
||||
#else
|
||||
pthread_mutex_t mtx;
|
||||
pthread_cond_t cv;
|
||||
#endif
|
||||
int done;
|
||||
int ok;
|
||||
int ret_code; /* raw `ret` from the callback (non-zero on error) */
|
||||
uint8_t* bytes; /* owned copy of the CBOR reply payload on success */
|
||||
size_t bytes_len;
|
||||
char* err; /* owned NUL-terminated error text on failure */
|
||||
int refs; /* guarded by lock/mtx; the release reaching 0 frees */
|
||||
} NimFfiSyncState;
|
||||
|
||||
static inline NimFfiSyncState* nimffi_sync_state_new(void) {
|
||||
NimFfiSyncState* s = (NimFfiSyncState*)calloc(1, sizeof(NimFfiSyncState));
|
||||
if (!s) {
|
||||
return NULL;
|
||||
}
|
||||
s->refs = 2;
|
||||
#if defined(_WIN32)
|
||||
InitializeSRWLock(&s->lock);
|
||||
InitializeConditionVariable(&s->cv);
|
||||
#else
|
||||
if (pthread_mutex_init(&s->mtx, NULL) != 0) {
|
||||
free(s);
|
||||
return NULL;
|
||||
}
|
||||
if (pthread_cond_init(&s->cv, NULL) != 0) {
|
||||
pthread_mutex_destroy(&s->mtx);
|
||||
free(s);
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
return s;
|
||||
}
|
||||
|
||||
static inline void nimffi_sync_state_release(NimFfiSyncState* s) {
|
||||
if (!s) {
|
||||
return;
|
||||
}
|
||||
int r;
|
||||
#if defined(_WIN32)
|
||||
AcquireSRWLockExclusive(&s->lock);
|
||||
r = --s->refs;
|
||||
ReleaseSRWLockExclusive(&s->lock);
|
||||
#else
|
||||
pthread_mutex_lock(&s->mtx);
|
||||
r = --s->refs;
|
||||
pthread_mutex_unlock(&s->mtx);
|
||||
#endif
|
||||
if (r != 0) {
|
||||
return;
|
||||
}
|
||||
#if !defined(_WIN32)
|
||||
pthread_mutex_destroy(&s->mtx);
|
||||
pthread_cond_destroy(&s->cv);
|
||||
#endif
|
||||
free(s->bytes);
|
||||
free(s->err);
|
||||
free(s);
|
||||
}
|
||||
|
||||
/* FFICallback-conforming sink: copies the reply/error out of the borrowed
|
||||
* msg/len buffer (owned by the binding only for this call), wakes the waiter,
|
||||
* then drops the callback's reference. */
|
||||
static inline void nimffi_sync_cb(int ret, const char* msg, size_t len, void* ud) {
|
||||
NimFfiSyncState* s = (NimFfiSyncState*)ud;
|
||||
#if defined(_WIN32)
|
||||
AcquireSRWLockExclusive(&s->lock);
|
||||
#else
|
||||
pthread_mutex_lock(&s->mtx);
|
||||
#endif
|
||||
s->ret_code = ret;
|
||||
s->ok = (ret == 0);
|
||||
if (msg && len > 0) {
|
||||
if (s->ok) {
|
||||
uint8_t* p = (uint8_t*)malloc(len);
|
||||
if (p) {
|
||||
memcpy(p, msg, len);
|
||||
s->bytes = p;
|
||||
s->bytes_len = len;
|
||||
}
|
||||
} else {
|
||||
s->err = nimffi_dup_cstr_n(msg, len);
|
||||
}
|
||||
}
|
||||
s->done = 1;
|
||||
#if defined(_WIN32)
|
||||
WakeConditionVariable(&s->cv);
|
||||
ReleaseSRWLockExclusive(&s->lock);
|
||||
#else
|
||||
pthread_cond_signal(&s->cv);
|
||||
pthread_mutex_unlock(&s->mtx);
|
||||
#endif
|
||||
nimffi_sync_state_release(s);
|
||||
}
|
||||
|
||||
/* Blocks until the callback marks the state done or `timeout_ms` elapses.
|
||||
* Returns 1 if the reply landed, 0 on timeout. The deadline is absolute so a
|
||||
* spurious wakeup cannot extend the wait past the requested budget. */
|
||||
static inline int nimffi_sync_wait(NimFfiSyncState* s, uint32_t timeout_ms) {
|
||||
int done;
|
||||
#if defined(_WIN32)
|
||||
ULONGLONG start = GetTickCount64();
|
||||
AcquireSRWLockExclusive(&s->lock);
|
||||
while (!s->done) {
|
||||
ULONGLONG elapsed = GetTickCount64() - start;
|
||||
if (elapsed >= timeout_ms) {
|
||||
break;
|
||||
}
|
||||
if (!SleepConditionVariableSRW(&s->cv, &s->lock,
|
||||
(DWORD)(timeout_ms - elapsed), 0)) {
|
||||
if (GetLastError() == ERROR_TIMEOUT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
done = s->done;
|
||||
ReleaseSRWLockExclusive(&s->lock);
|
||||
#else
|
||||
struct timespec deadline;
|
||||
clock_gettime(CLOCK_REALTIME, &deadline);
|
||||
deadline.tv_sec += (time_t)(timeout_ms / 1000u);
|
||||
deadline.tv_nsec += (long)(timeout_ms % 1000u) * 1000000L;
|
||||
if (deadline.tv_nsec >= 1000000000L) {
|
||||
deadline.tv_sec += 1;
|
||||
deadline.tv_nsec -= 1000000000L;
|
||||
}
|
||||
pthread_mutex_lock(&s->mtx);
|
||||
while (!s->done) {
|
||||
if (pthread_cond_timedwait(&s->cv, &s->mtx, &deadline) == ETIMEDOUT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
done = s->done;
|
||||
pthread_mutex_unlock(&s->mtx);
|
||||
#endif
|
||||
return done;
|
||||
}
|
||||
|
||||
/* Bounded copy of `src` into `buf`, always NUL-terminating when err_len > 0. */
|
||||
static inline void nimffi_copy_err(char* buf, size_t err_len, const char* src) {
|
||||
if (!buf || err_len == 0) {
|
||||
return;
|
||||
}
|
||||
if (!src) {
|
||||
src = "";
|
||||
}
|
||||
size_t n = strlen(src);
|
||||
if (n >= err_len) {
|
||||
n = err_len - 1;
|
||||
}
|
||||
memcpy(buf, src, n);
|
||||
buf[n] = '\0';
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NIM_FFI_SYNC_HELPER_H_INCLUDED */
|
||||
|
||||
@ -28,9 +28,9 @@ find_package(Threads REQUIRED)
|
||||
add_library(my_timer_headers INTERFACE)
|
||||
target_include_directories(my_timer_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_link_libraries(my_timer_headers INTERFACE my_timer tinycbor Threads::Threads)
|
||||
# The generated header is async (no blocking helper), but consumer code that
|
||||
# waits on a result callback typically uses nanosleep / pthreads, which need a
|
||||
# POSIX feature level that strict `-std=c11` hides. Define it for consumers.
|
||||
# The generated `_sync` wrappers block on a pthread condvar, and consumer code
|
||||
# that polls a result callback typically uses nanosleep — both need a POSIX
|
||||
# feature level that strict `-std=c11` hides. Define it for consumers.
|
||||
target_compile_definitions(my_timer_headers INTERFACE _POSIX_C_SOURCE=200809L)
|
||||
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c")
|
||||
|
||||
@ -6,6 +6,9 @@ This folder contains **auto-generated C bindings** for the `my_timer` Nim
|
||||
library. It is generated from `../timer.nim` and provides:
|
||||
|
||||
- `my_timer.h`: header-only C binding (`MyTimerCtx` + `my_timer_ctx_*` API)
|
||||
- `nim_ffi_prelude.h` / `nim_ffi_cbor.h` / `nim_ffi_sync.h`: shared, library-
|
||||
agnostic headers (owned string/byte types, leaf CBOR codecs, and the blocking
|
||||
`_sync` call helper)
|
||||
- `main.c`: example executable demonstrating how to use the bindings
|
||||
- `CMakeLists.txt`: build configuration that compiles the Nim library, the
|
||||
vendored TinyCBOR, and the C example
|
||||
@ -36,12 +39,34 @@ cmake --build build
|
||||
./build/my_timer_example
|
||||
```
|
||||
|
||||
## Blocking API (`_sync`)
|
||||
|
||||
Every method and the constructor also get a blocking `_sync` variant. It
|
||||
submits the request, waits up to `timeout_ms` for the reply, deep-copies the
|
||||
result into a caller-owned out-param and returns 0 — or fills `err_buf` and
|
||||
returns non-zero. This is what `main.c` uses:
|
||||
|
||||
```c
|
||||
EchoResponse resp = {0};
|
||||
char err[256];
|
||||
if (my_timer_ctx_echo_sync(ctx, &req, &resp, err, sizeof(err), 5000) != 0) {
|
||||
fprintf(stderr, "echo failed: %s\n", err);
|
||||
return 1;
|
||||
}
|
||||
printf("echoed: %s\n", resp.echoed.data);
|
||||
my_timer_free_EchoResponse(&resp); /* release the owned reply */
|
||||
```
|
||||
|
||||
The out-param is yours: release any owned reply fields (strings, sequences)
|
||||
with the generated `my_timer_free_<Type>()` helper — or `nimffi_free_str` for a
|
||||
bare string reply.
|
||||
|
||||
## Asynchronous API
|
||||
|
||||
Every method and the constructor take a typed **result callback** and return
|
||||
immediately. The callback fires exactly once — synchronously if the request
|
||||
fails to even submit, otherwise from the Nim dispatch thread when the reply
|
||||
arrives:
|
||||
Every method and the constructor also take a typed **result callback** and
|
||||
return immediately. The callback fires exactly once — synchronously if the
|
||||
request fails to even submit, otherwise from the Nim dispatch thread when the
|
||||
reply arrives:
|
||||
|
||||
```c
|
||||
static void on_echo(int err_code, const EchoResponse* reply,
|
||||
@ -53,18 +78,20 @@ static void on_echo(int err_code, const EchoResponse* reply,
|
||||
my_timer_ctx_echo(ctx, &req, on_echo, /*user_data=*/NULL);
|
||||
```
|
||||
|
||||
See `main.c` for the full pattern, including a small `wait_done()` poll helper
|
||||
that turns each async call back into a sequential step.
|
||||
Library-initiated **events** are always asynchronous — they arrive on the Nim
|
||||
dispatch thread via `my_timer_ctx_add_on_<event>_listener`.
|
||||
|
||||
## Memory Ownership
|
||||
|
||||
- Request-side strings/sequences are *borrowed* — wrap C strings with
|
||||
`nimffi_str(...)`; the binding never frees them.
|
||||
- Reply values and error strings passed into a result callback are **owned by
|
||||
the binding** and valid only for the duration of that callback. The caller
|
||||
never frees them — copy out anything you need to keep before returning.
|
||||
- A `MyTimerCtx*` delivered to the constructor callback is the exception:
|
||||
ownership transfers to you, and you release it with `my_timer_ctx_destroy()`.
|
||||
- A `_sync` out-param is **owned by the caller** on success — release owned
|
||||
fields with the generated `my_timer_free_<Type>()` helper.
|
||||
- Reply values and error strings passed into an *async* result callback are
|
||||
**owned by the binding** and valid only for the duration of that callback.
|
||||
The caller never frees them — copy out anything you need before returning.
|
||||
- A `MyTimerCtx*` (from `_sync`'s out-param or the constructor callback) is
|
||||
yours either way: release it with `my_timer_ctx_destroy()`.
|
||||
|
||||
## Do Not Edit
|
||||
|
||||
|
||||
@ -2,15 +2,12 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#if defined(__STDC_NO_ATOMICS__)
|
||||
# error "C11 atomics required (or provide a mutex/condvar fallback)"
|
||||
#endif
|
||||
#include <stdatomic.h>
|
||||
/* Uses the generated blocking `_sync` API: each call submits, waits up to
|
||||
* `timeout_ms`, deep-copies the reply into a caller-owned out-param (released
|
||||
* with the generated <lib>_free_<Type>() helper) and returns 0, or fills `err`
|
||||
* and returns non-zero — no hand-rolled waiters. Events still arrive async on
|
||||
* the dispatch thread, so the typed listener records into globals below. */
|
||||
|
||||
/* The `done` flags below are written from the library's dispatch thread and
|
||||
* polled from main, so they cross a thread boundary — atomics, not `volatile`,
|
||||
* give the visibility guarantee. sleep_ms wraps the platform nap so the demo
|
||||
* builds on Windows too. */
|
||||
#if defined(_WIN32)
|
||||
# include <windows.h>
|
||||
static void sleep_ms(unsigned ms) { Sleep(ms); }
|
||||
@ -22,152 +19,51 @@ static void sleep_ms(unsigned ms) {
|
||||
}
|
||||
#endif
|
||||
|
||||
/* The generated bindings are asynchronous: each call takes a result callback
|
||||
* and returns immediately. The reply and any error string handed to that
|
||||
* callback are owned by the binding and valid only while the callback runs —
|
||||
* the caller never frees them; it copies out whatever it wants to keep. This
|
||||
* demo turns each async call back into a sequential step by polling a `done`
|
||||
* flag (the same pattern the typed event listener already uses). */
|
||||
#define TIMEOUT_MS 5000
|
||||
|
||||
/* Poll up to ~5s for a callback to fire. Returns false if it never did, so the
|
||||
* caller can report a stuck call instead of treating it as an empty success. */
|
||||
static bool wait_done(atomic_int* done) {
|
||||
for (int i = 0; i < 500 && !atomic_load(done); i++) {
|
||||
sleep_ms(10);
|
||||
}
|
||||
return atomic_load(done) != 0;
|
||||
}
|
||||
|
||||
static atomic_int g_echo_count = 0;
|
||||
static int g_echo_count;
|
||||
static char g_echo_message[256];
|
||||
|
||||
static void on_echo_fired(const EchoEvent* evt, void* user_data) {
|
||||
(void)user_data;
|
||||
atomic_store(&g_echo_count, (int)evt->echoCount);
|
||||
g_echo_count = (int)evt->echoCount;
|
||||
snprintf(g_echo_message, sizeof(g_echo_message), "%s",
|
||||
evt->message.data ? evt->message.data : "");
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
atomic_int done;
|
||||
int err_code;
|
||||
MyTimerCtx* ctx;
|
||||
char err[256];
|
||||
} CreateWaiter;
|
||||
|
||||
static void on_created(int ec, MyTimerCtx* ctx, const char* em, void* ud) {
|
||||
CreateWaiter* w = (CreateWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
w->ctx = ctx;
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
atomic_store(&w->done, 1);
|
||||
}
|
||||
|
||||
/* Generic reply sink: each step copies the fields it cares about out of its
|
||||
* typed reply into these slots (text_a/text_b for strings, num_a/num_b for
|
||||
* integers, flag for a boolean) before the binding reclaims the reply. */
|
||||
typedef struct {
|
||||
atomic_int done;
|
||||
int err_code;
|
||||
char err[256];
|
||||
char text_a[256];
|
||||
char text_b[256];
|
||||
long long num_a;
|
||||
long long num_b;
|
||||
int flag;
|
||||
} ReplyWaiter;
|
||||
|
||||
static void on_version(int ec, const NimFfiStr* reply, const char* em, void* ud) {
|
||||
ReplyWaiter* w = (ReplyWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
if (reply && reply->data) snprintf(w->text_a, sizeof(w->text_a), "%s", reply->data);
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
atomic_store(&w->done, 1);
|
||||
}
|
||||
|
||||
static void on_echo(int ec, const EchoResponse* reply, const char* em, void* ud) {
|
||||
ReplyWaiter* w = (ReplyWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
if (reply) {
|
||||
if (reply->echoed.data)
|
||||
snprintf(w->text_a, sizeof(w->text_a), "%s", reply->echoed.data);
|
||||
if (reply->timerName.data)
|
||||
snprintf(w->text_b, sizeof(w->text_b), "%s", reply->timerName.data);
|
||||
}
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
atomic_store(&w->done, 1);
|
||||
}
|
||||
|
||||
static void on_complex(int ec, const ComplexResponse* reply, const char* em, void* ud) {
|
||||
ReplyWaiter* w = (ReplyWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
if (reply) {
|
||||
w->num_a = (long long)reply->itemCount;
|
||||
w->flag = (int)reply->hasNote;
|
||||
if (reply->summary.data)
|
||||
snprintf(w->text_a, sizeof(w->text_a), "%s", reply->summary.data);
|
||||
}
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
atomic_store(&w->done, 1);
|
||||
}
|
||||
|
||||
static void on_schedule(int ec, const ScheduleResult* reply, const char* em, void* ud) {
|
||||
ReplyWaiter* w = (ReplyWaiter*)ud;
|
||||
w->err_code = ec;
|
||||
if (reply) {
|
||||
w->num_a = (long long)reply->willRunCount;
|
||||
w->num_b = (long long)reply->firstRunAtMs;
|
||||
if (reply->jobId.data)
|
||||
snprintf(w->text_a, sizeof(w->text_a), "%s", reply->jobId.data);
|
||||
}
|
||||
if (em) snprintf(w->err, sizeof(w->err), "%s", em);
|
||||
atomic_store(&w->done, 1);
|
||||
}
|
||||
|
||||
/* Fire an async call, block until its callback lands, and bail to cleanup on a
|
||||
* timeout or error. Relies on `ctx` being in scope for that cleanup — these
|
||||
* steps all run against the one context created in main(). */
|
||||
#define RUN(call, w) \
|
||||
do { \
|
||||
memset(&(w), 0, sizeof(w)); \
|
||||
call; \
|
||||
const char* run_err = NULL; \
|
||||
if (!wait_done(&(w).done)) \
|
||||
run_err = "FFI call did not complete"; \
|
||||
else if ((w).err_code != 0) \
|
||||
run_err = (w).err[0] ? (w).err : "unknown"; \
|
||||
if (run_err) { \
|
||||
fprintf(stderr, "Error: %s\n", run_err); \
|
||||
my_timer_ctx_destroy(ctx); \
|
||||
return 1; \
|
||||
} \
|
||||
/* Run a blocking `_sync` call; on failure print the error it wrote into `err`,
|
||||
* tear the context down and bail. `ctx` and `err` are in scope in main(). */
|
||||
#define RUN(call) \
|
||||
do { \
|
||||
if ((call) != 0) { \
|
||||
fprintf(stderr, "Error: %s\n", err); \
|
||||
my_timer_ctx_destroy(ctx); \
|
||||
return 1; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
int main(void) {
|
||||
CreateWaiter cw;
|
||||
memset(&cw, 0, sizeof(cw));
|
||||
char err[256] = {0};
|
||||
MyTimerCtx* ctx = NULL;
|
||||
TimerConfig config = {nimffi_str("c-demo")};
|
||||
my_timer_ctx_create(&config, on_created, &cw);
|
||||
if (!wait_done(&cw.done) || cw.err_code != 0 || !cw.ctx) {
|
||||
fprintf(stderr, "Error: %s\n",
|
||||
cw.err[0] ? cw.err : "create did not complete");
|
||||
if (my_timer_ctx_create_sync(&config, &ctx, err, sizeof(err), TIMEOUT_MS) != 0) {
|
||||
fprintf(stderr, "Error: %s\n", err);
|
||||
return 1;
|
||||
}
|
||||
MyTimerCtx* ctx = cw.ctx;
|
||||
printf("[1] Context created\n");
|
||||
|
||||
ReplyWaiter w;
|
||||
RUN(my_timer_ctx_version(ctx, on_version, &w), w);
|
||||
printf("[2] Version: %s\n", w.text_a);
|
||||
NimFfiStr version = {0};
|
||||
RUN(my_timer_ctx_version_sync(ctx, &version, err, sizeof(err), TIMEOUT_MS));
|
||||
printf("[2] Version: %s\n", version.data ? version.data : "");
|
||||
nimffi_free_str(&version);
|
||||
|
||||
EchoRequest echo_req = {nimffi_str("hello from C"), 50};
|
||||
RUN(my_timer_ctx_echo(ctx, &echo_req, on_echo, &w), w);
|
||||
printf("[3] Echo: echoed=%s, timerName=%s\n", w.text_a, w.text_b);
|
||||
EchoResponse echo = {0};
|
||||
RUN(my_timer_ctx_echo_sync(ctx, &echo_req, &echo, err, sizeof(err), TIMEOUT_MS));
|
||||
printf("[3] Echo: echoed=%s, timerName=%s\n", echo.echoed.data, echo.timerName.data);
|
||||
my_timer_free_EchoResponse(&echo);
|
||||
|
||||
EchoRequest items[2] = {
|
||||
{nimffi_str("one"), 10},
|
||||
{nimffi_str("two"), 20},
|
||||
};
|
||||
EchoRequest items[2] = {{nimffi_str("one"), 10}, {nimffi_str("two"), 20}};
|
||||
NimFfiStr tags[2] = {nimffi_str("fast"), nimffi_str("c")};
|
||||
ComplexRequest complex_req;
|
||||
complex_req.messages.data = items;
|
||||
@ -179,9 +75,12 @@ int main(void) {
|
||||
complex_req.retries.has_value = true;
|
||||
complex_req.retries.value = 3;
|
||||
|
||||
RUN(my_timer_ctx_complex(ctx, &complex_req, on_complex, &w), w);
|
||||
printf("[4] Complex: summary=%s, itemCount=%lld, hasNote=%d\n", w.text_a, w.num_a,
|
||||
w.flag);
|
||||
ComplexResponse complex = {0};
|
||||
RUN(my_timer_ctx_complex_sync(ctx, &complex_req, &complex, err, sizeof(err),
|
||||
TIMEOUT_MS));
|
||||
printf("[4] Complex: summary=%s, itemCount=%lld, hasNote=%d\n", complex.summary.data,
|
||||
(long long)complex.itemCount, (int)complex.hasNote);
|
||||
my_timer_free_ComplexResponse(&complex);
|
||||
|
||||
NimFfiStr job_payload[2] = {nimffi_str("rollup"), nimffi_str("v2")};
|
||||
JobSpec job;
|
||||
@ -203,21 +102,23 @@ int main(void) {
|
||||
schedule.jitter.has_value = true;
|
||||
schedule.jitter.value = 250;
|
||||
|
||||
RUN(my_timer_ctx_schedule(ctx, &job, &retry, &schedule, on_schedule, &w), w);
|
||||
ScheduleResult sched = {0};
|
||||
RUN(my_timer_ctx_schedule_sync(ctx, &job, &retry, &schedule, &sched, err,
|
||||
sizeof(err), TIMEOUT_MS));
|
||||
printf("[5] Schedule: jobId=%s, willRunCount=%lld, firstRunAtMs=%lld\n",
|
||||
w.text_a, w.num_a, w.num_b);
|
||||
sched.jobId.data, (long long)sched.willRunCount,
|
||||
(long long)sched.firstRunAtMs);
|
||||
my_timer_free_ScheduleResult(&sched);
|
||||
|
||||
uint64_t handle =
|
||||
my_timer_ctx_add_on_echo_fired_listener(ctx, on_echo_fired, NULL);
|
||||
uint64_t handle = my_timer_ctx_add_on_echo_fired_listener(ctx, on_echo_fired, NULL);
|
||||
EchoRequest evt_req = {nimffi_str("event-demo"), 1};
|
||||
memset(&w, 0, sizeof(w));
|
||||
my_timer_ctx_echo(ctx, &evt_req, on_echo, &w);
|
||||
wait_done(&w.done);
|
||||
EchoResponse evt_echo = {0};
|
||||
RUN(my_timer_ctx_echo_sync(ctx, &evt_req, &evt_echo, err, sizeof(err), TIMEOUT_MS));
|
||||
my_timer_free_EchoResponse(&evt_echo);
|
||||
/* The event fires from the library's dispatch thread; give it a moment. */
|
||||
sleep_ms(500);
|
||||
printf("[6] typed event onEchoFired: message=%s, echoCount=%d\n",
|
||||
g_echo_message, atomic_load(&g_echo_count));
|
||||
|
||||
printf("[6] typed event onEchoFired: message=%s, echoCount=%d\n", g_echo_message,
|
||||
g_echo_count);
|
||||
my_timer_ctx_remove_event_listener(ctx, handle);
|
||||
|
||||
my_timer_ctx_destroy(ctx);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
#ifndef NIM_FFI_LIB_MY_TIMER_H_INCLUDED
|
||||
#define NIM_FFI_LIB_MY_TIMER_H_INCLUDED
|
||||
#include "nim_ffi_cbor.h"
|
||||
#include "nim_ffi_sync.h"
|
||||
|
||||
/* ============================================================ */
|
||||
/* Generated types (user-declared + per-proc request envelopes) */
|
||||
@ -894,6 +895,67 @@ static inline int my_timer_ctx_create(const TimerConfig* config, MyTimerCreateFn
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int my_timer_ctx_create_sync(const TimerConfig* config, MyTimerCtx** out, char* err_buf, size_t err_len, uint32_t timeout_ms) {
|
||||
MyTimerCreateCtorReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
ffi_req.config = *config;
|
||||
uint8_t* req_buf = NULL;
|
||||
size_t req_len = 0;
|
||||
char* enc_err = NULL;
|
||||
if (nimffi_encode_to_buf(my_timer_encv_MyTimerCreateCtorReq, &ffi_req, &req_buf, &req_len, &enc_err) != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, enc_err ? enc_err : "encode failed");
|
||||
free(enc_err);
|
||||
return -1;
|
||||
}
|
||||
NimFfiSyncState* st = nimffi_sync_state_new();
|
||||
if (!st) {
|
||||
free(req_buf);
|
||||
nimffi_copy_err(err_buf, err_len, "out of memory");
|
||||
return -1;
|
||||
}
|
||||
(void)my_timer_create(req_buf, req_len, nimffi_sync_cb, st);
|
||||
free(req_buf);
|
||||
if (!nimffi_sync_wait(st, timeout_ms)) {
|
||||
nimffi_copy_err(err_buf, err_len, "FFI create timed out");
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
if (!st->ok) {
|
||||
nimffi_copy_err(err_buf, err_len, st->err ? st->err : "FFI create failed");
|
||||
int rc = st->ret_code ? st->ret_code : -1;
|
||||
nimffi_sync_state_release(st);
|
||||
return rc;
|
||||
}
|
||||
NimFfiStr addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
char* dec_err = NULL;
|
||||
if (nimffi_decode_from_buf(my_timer_decv_Str, st->bytes, st->bytes_len, &addr, &dec_err) != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, dec_err ? dec_err : "decode failed");
|
||||
free(dec_err);
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
char* endp = NULL;
|
||||
unsigned long long a = addr.data ? strtoull(addr.data, &endp, 10) : 0;
|
||||
bool ok = addr.data && addr.len > 0 && endp && *endp == '\0';
|
||||
nimffi_free_str(&addr);
|
||||
if (!ok) {
|
||||
nimffi_copy_err(err_buf, err_len, "FFI create returned non-numeric address");
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
MyTimerCtx* c = (MyTimerCtx*)calloc(1, sizeof(MyTimerCtx));
|
||||
if (!c) {
|
||||
nimffi_copy_err(err_buf, err_len, "out of memory");
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
c->ptr = (void*)(uintptr_t)a;
|
||||
*out = c;
|
||||
nimffi_sync_state_release(st);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline void my_timer_ctx_destroy(MyTimerCtx* ctx) {
|
||||
if (!ctx) return;
|
||||
if (ctx->ptr) { my_timer_destroy(ctx->ptr); ctx->ptr = NULL; }
|
||||
@ -996,6 +1058,57 @@ static inline int my_timer_ctx_echo(const MyTimerCtx* ctx, const EchoRequest* re
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int my_timer_ctx_echo_sync(const MyTimerCtx* ctx, const EchoRequest* req, EchoResponse* out, char* err_buf, size_t err_len, uint32_t timeout_ms) {
|
||||
MyTimerEchoReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
ffi_req.req = *req;
|
||||
uint8_t* req_buf = NULL;
|
||||
size_t req_len = 0;
|
||||
char* enc_err = NULL;
|
||||
if (nimffi_encode_to_buf(my_timer_encv_MyTimerEchoReq, &ffi_req, &req_buf, &req_len, &enc_err) != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, enc_err ? enc_err : "encode failed");
|
||||
free(enc_err);
|
||||
return -1;
|
||||
}
|
||||
NimFfiSyncState* st = nimffi_sync_state_new();
|
||||
if (!st) {
|
||||
free(req_buf);
|
||||
nimffi_copy_err(err_buf, err_len, "out of memory");
|
||||
return -1;
|
||||
}
|
||||
int ret = my_timer_echo(ctx->ptr, nimffi_sync_cb, st, req_buf, req_len);
|
||||
free(req_buf);
|
||||
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
|
||||
nimffi_copy_err(err_buf, err_len, "RET_MISSING_CALLBACK (internal error)");
|
||||
nimffi_sync_state_release(st);
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
if (!nimffi_sync_wait(st, timeout_ms)) {
|
||||
nimffi_copy_err(err_buf, err_len, "FFI call timed out");
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
if (!st->ok) {
|
||||
nimffi_copy_err(err_buf, err_len, st->err ? st->err : "FFI call failed");
|
||||
int rc = st->ret_code ? st->ret_code : -1;
|
||||
nimffi_sync_state_release(st);
|
||||
return rc;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
char* dec_err = NULL;
|
||||
int dec = nimffi_decode_from_buf(my_timer_decv_EchoResponse, st->bytes, st->bytes_len, out, &dec_err);
|
||||
if (dec != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, dec_err ? dec_err : "decode failed");
|
||||
free(dec_err);
|
||||
my_timer_free_EchoResponse(out);
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
nimffi_sync_state_release(st);
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef void (*MyTimerVersionReplyFn)(int err_code, const NimFfiStr* reply, const char* err_msg, void* user_data);
|
||||
typedef struct { MyTimerVersionReplyFn fn; void* user_data; } MyTimerVersionCallBox;
|
||||
static void my_timer_version_reply_trampoline(int ret, const char* msg, size_t len, void* ud) {
|
||||
@ -1055,6 +1168,56 @@ static inline int my_timer_ctx_version(const MyTimerCtx* ctx, MyTimerVersionRepl
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int my_timer_ctx_version_sync(const MyTimerCtx* ctx, NimFfiStr* out, char* err_buf, size_t err_len, uint32_t timeout_ms) {
|
||||
MyTimerVersionReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
uint8_t* req_buf = NULL;
|
||||
size_t req_len = 0;
|
||||
char* enc_err = NULL;
|
||||
if (nimffi_encode_to_buf(my_timer_encv_MyTimerVersionReq, &ffi_req, &req_buf, &req_len, &enc_err) != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, enc_err ? enc_err : "encode failed");
|
||||
free(enc_err);
|
||||
return -1;
|
||||
}
|
||||
NimFfiSyncState* st = nimffi_sync_state_new();
|
||||
if (!st) {
|
||||
free(req_buf);
|
||||
nimffi_copy_err(err_buf, err_len, "out of memory");
|
||||
return -1;
|
||||
}
|
||||
int ret = my_timer_version(ctx->ptr, nimffi_sync_cb, st, req_buf, req_len);
|
||||
free(req_buf);
|
||||
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
|
||||
nimffi_copy_err(err_buf, err_len, "RET_MISSING_CALLBACK (internal error)");
|
||||
nimffi_sync_state_release(st);
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
if (!nimffi_sync_wait(st, timeout_ms)) {
|
||||
nimffi_copy_err(err_buf, err_len, "FFI call timed out");
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
if (!st->ok) {
|
||||
nimffi_copy_err(err_buf, err_len, st->err ? st->err : "FFI call failed");
|
||||
int rc = st->ret_code ? st->ret_code : -1;
|
||||
nimffi_sync_state_release(st);
|
||||
return rc;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
char* dec_err = NULL;
|
||||
int dec = nimffi_decode_from_buf(my_timer_decv_Str, st->bytes, st->bytes_len, out, &dec_err);
|
||||
if (dec != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, dec_err ? dec_err : "decode failed");
|
||||
free(dec_err);
|
||||
nimffi_free_str(out);
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
nimffi_sync_state_release(st);
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef void (*MyTimerComplexReplyFn)(int err_code, const ComplexResponse* reply, const char* err_msg, void* user_data);
|
||||
typedef struct { MyTimerComplexReplyFn fn; void* user_data; } MyTimerComplexCallBox;
|
||||
static void my_timer_complex_reply_trampoline(int ret, const char* msg, size_t len, void* ud) {
|
||||
@ -1115,6 +1278,57 @@ static inline int my_timer_ctx_complex(const MyTimerCtx* ctx, const ComplexReque
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int my_timer_ctx_complex_sync(const MyTimerCtx* ctx, const ComplexRequest* req, ComplexResponse* out, char* err_buf, size_t err_len, uint32_t timeout_ms) {
|
||||
MyTimerComplexReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
ffi_req.req = *req;
|
||||
uint8_t* req_buf = NULL;
|
||||
size_t req_len = 0;
|
||||
char* enc_err = NULL;
|
||||
if (nimffi_encode_to_buf(my_timer_encv_MyTimerComplexReq, &ffi_req, &req_buf, &req_len, &enc_err) != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, enc_err ? enc_err : "encode failed");
|
||||
free(enc_err);
|
||||
return -1;
|
||||
}
|
||||
NimFfiSyncState* st = nimffi_sync_state_new();
|
||||
if (!st) {
|
||||
free(req_buf);
|
||||
nimffi_copy_err(err_buf, err_len, "out of memory");
|
||||
return -1;
|
||||
}
|
||||
int ret = my_timer_complex(ctx->ptr, nimffi_sync_cb, st, req_buf, req_len);
|
||||
free(req_buf);
|
||||
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
|
||||
nimffi_copy_err(err_buf, err_len, "RET_MISSING_CALLBACK (internal error)");
|
||||
nimffi_sync_state_release(st);
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
if (!nimffi_sync_wait(st, timeout_ms)) {
|
||||
nimffi_copy_err(err_buf, err_len, "FFI call timed out");
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
if (!st->ok) {
|
||||
nimffi_copy_err(err_buf, err_len, st->err ? st->err : "FFI call failed");
|
||||
int rc = st->ret_code ? st->ret_code : -1;
|
||||
nimffi_sync_state_release(st);
|
||||
return rc;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
char* dec_err = NULL;
|
||||
int dec = nimffi_decode_from_buf(my_timer_decv_ComplexResponse, st->bytes, st->bytes_len, out, &dec_err);
|
||||
if (dec != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, dec_err ? dec_err : "decode failed");
|
||||
free(dec_err);
|
||||
my_timer_free_ComplexResponse(out);
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
nimffi_sync_state_release(st);
|
||||
return 0;
|
||||
}
|
||||
|
||||
typedef void (*MyTimerScheduleReplyFn)(int err_code, const ScheduleResult* reply, const char* err_msg, void* user_data);
|
||||
typedef struct { MyTimerScheduleReplyFn fn; void* user_data; } MyTimerScheduleCallBox;
|
||||
static void my_timer_schedule_reply_trampoline(int ret, const char* msg, size_t len, void* ud) {
|
||||
@ -1177,4 +1391,57 @@ static inline int my_timer_ctx_schedule(const MyTimerCtx* ctx, const JobSpec* jo
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int my_timer_ctx_schedule_sync(const MyTimerCtx* ctx, const JobSpec* job, const RetryPolicy* retry, const ScheduleConfig* schedule, ScheduleResult* out, char* err_buf, size_t err_len, uint32_t timeout_ms) {
|
||||
MyTimerScheduleReq ffi_req;
|
||||
memset(&ffi_req, 0, sizeof(ffi_req));
|
||||
ffi_req.job = *job;
|
||||
ffi_req.retry = *retry;
|
||||
ffi_req.schedule = *schedule;
|
||||
uint8_t* req_buf = NULL;
|
||||
size_t req_len = 0;
|
||||
char* enc_err = NULL;
|
||||
if (nimffi_encode_to_buf(my_timer_encv_MyTimerScheduleReq, &ffi_req, &req_buf, &req_len, &enc_err) != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, enc_err ? enc_err : "encode failed");
|
||||
free(enc_err);
|
||||
return -1;
|
||||
}
|
||||
NimFfiSyncState* st = nimffi_sync_state_new();
|
||||
if (!st) {
|
||||
free(req_buf);
|
||||
nimffi_copy_err(err_buf, err_len, "out of memory");
|
||||
return -1;
|
||||
}
|
||||
int ret = my_timer_schedule(ctx->ptr, nimffi_sync_cb, st, req_buf, req_len);
|
||||
free(req_buf);
|
||||
if (ret == NIMFFI_RET_MISSING_CALLBACK) {
|
||||
nimffi_copy_err(err_buf, err_len, "RET_MISSING_CALLBACK (internal error)");
|
||||
nimffi_sync_state_release(st);
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
if (!nimffi_sync_wait(st, timeout_ms)) {
|
||||
nimffi_copy_err(err_buf, err_len, "FFI call timed out");
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
if (!st->ok) {
|
||||
nimffi_copy_err(err_buf, err_len, st->err ? st->err : "FFI call failed");
|
||||
int rc = st->ret_code ? st->ret_code : -1;
|
||||
nimffi_sync_state_release(st);
|
||||
return rc;
|
||||
}
|
||||
memset(out, 0, sizeof(*out));
|
||||
char* dec_err = NULL;
|
||||
int dec = nimffi_decode_from_buf(my_timer_decv_ScheduleResult, st->bytes, st->bytes_len, out, &dec_err);
|
||||
if (dec != 0) {
|
||||
nimffi_copy_err(err_buf, err_len, dec_err ? dec_err : "decode failed");
|
||||
free(dec_err);
|
||||
my_timer_free_ScheduleResult(out);
|
||||
nimffi_sync_state_release(st);
|
||||
return -1;
|
||||
}
|
||||
nimffi_sync_state_release(st);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* NIM_FFI_LIB_MY_TIMER_H_INCLUDED */
|
||||
|
||||
193
examples/timer/c_bindings/nim_ffi_sync.h
Normal file
193
examples/timer/c_bindings/nim_ffi_sync.h
Normal file
@ -0,0 +1,193 @@
|
||||
#ifndef NIM_FFI_SYNC_HELPER_H_INCLUDED
|
||||
#define NIM_FFI_SYNC_HELPER_H_INCLUDED
|
||||
/* Blocking-call helper shared by the generated <lib>_ctx_<method>_sync wrappers.
|
||||
* Turns the async callback ABI into a synchronous request/reply: submit the
|
||||
* call, block on a condition variable until its single callback fires (or the
|
||||
* timeout elapses), then hand the raw CBOR payload / error text back so the
|
||||
* generated wrapper can decode it into a caller-owned out-param.
|
||||
*
|
||||
* The state is heap-allocated and reference-counted (2: the waiter and the
|
||||
* callback). A callback that fires after the caller has already timed out thus
|
||||
* writes into a still-live object and drops the last reference cleanly — no
|
||||
* use-after-free, no leak. Uses pthreads on POSIX and SRWLOCK/CONDITION_VARIABLE
|
||||
* on Win32, the same platform split the example programs use. */
|
||||
#include "nim_ffi_cbor.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
# include <windows.h>
|
||||
#else
|
||||
# include <pthread.h>
|
||||
# include <errno.h>
|
||||
# include <time.h>
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
#if defined(_WIN32)
|
||||
SRWLOCK lock;
|
||||
CONDITION_VARIABLE cv;
|
||||
#else
|
||||
pthread_mutex_t mtx;
|
||||
pthread_cond_t cv;
|
||||
#endif
|
||||
int done;
|
||||
int ok;
|
||||
int ret_code; /* raw `ret` from the callback (non-zero on error) */
|
||||
uint8_t* bytes; /* owned copy of the CBOR reply payload on success */
|
||||
size_t bytes_len;
|
||||
char* err; /* owned NUL-terminated error text on failure */
|
||||
int refs; /* guarded by lock/mtx; the release reaching 0 frees */
|
||||
} NimFfiSyncState;
|
||||
|
||||
static inline NimFfiSyncState* nimffi_sync_state_new(void) {
|
||||
NimFfiSyncState* s = (NimFfiSyncState*)calloc(1, sizeof(NimFfiSyncState));
|
||||
if (!s) {
|
||||
return NULL;
|
||||
}
|
||||
s->refs = 2;
|
||||
#if defined(_WIN32)
|
||||
InitializeSRWLock(&s->lock);
|
||||
InitializeConditionVariable(&s->cv);
|
||||
#else
|
||||
if (pthread_mutex_init(&s->mtx, NULL) != 0) {
|
||||
free(s);
|
||||
return NULL;
|
||||
}
|
||||
if (pthread_cond_init(&s->cv, NULL) != 0) {
|
||||
pthread_mutex_destroy(&s->mtx);
|
||||
free(s);
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
return s;
|
||||
}
|
||||
|
||||
static inline void nimffi_sync_state_release(NimFfiSyncState* s) {
|
||||
if (!s) {
|
||||
return;
|
||||
}
|
||||
int r;
|
||||
#if defined(_WIN32)
|
||||
AcquireSRWLockExclusive(&s->lock);
|
||||
r = --s->refs;
|
||||
ReleaseSRWLockExclusive(&s->lock);
|
||||
#else
|
||||
pthread_mutex_lock(&s->mtx);
|
||||
r = --s->refs;
|
||||
pthread_mutex_unlock(&s->mtx);
|
||||
#endif
|
||||
if (r != 0) {
|
||||
return;
|
||||
}
|
||||
#if !defined(_WIN32)
|
||||
pthread_mutex_destroy(&s->mtx);
|
||||
pthread_cond_destroy(&s->cv);
|
||||
#endif
|
||||
free(s->bytes);
|
||||
free(s->err);
|
||||
free(s);
|
||||
}
|
||||
|
||||
/* FFICallback-conforming sink: copies the reply/error out of the borrowed
|
||||
* msg/len buffer (owned by the binding only for this call), wakes the waiter,
|
||||
* then drops the callback's reference. */
|
||||
static inline void nimffi_sync_cb(int ret, const char* msg, size_t len, void* ud) {
|
||||
NimFfiSyncState* s = (NimFfiSyncState*)ud;
|
||||
#if defined(_WIN32)
|
||||
AcquireSRWLockExclusive(&s->lock);
|
||||
#else
|
||||
pthread_mutex_lock(&s->mtx);
|
||||
#endif
|
||||
s->ret_code = ret;
|
||||
s->ok = (ret == 0);
|
||||
if (msg && len > 0) {
|
||||
if (s->ok) {
|
||||
uint8_t* p = (uint8_t*)malloc(len);
|
||||
if (p) {
|
||||
memcpy(p, msg, len);
|
||||
s->bytes = p;
|
||||
s->bytes_len = len;
|
||||
}
|
||||
} else {
|
||||
s->err = nimffi_dup_cstr_n(msg, len);
|
||||
}
|
||||
}
|
||||
s->done = 1;
|
||||
#if defined(_WIN32)
|
||||
WakeConditionVariable(&s->cv);
|
||||
ReleaseSRWLockExclusive(&s->lock);
|
||||
#else
|
||||
pthread_cond_signal(&s->cv);
|
||||
pthread_mutex_unlock(&s->mtx);
|
||||
#endif
|
||||
nimffi_sync_state_release(s);
|
||||
}
|
||||
|
||||
/* Blocks until the callback marks the state done or `timeout_ms` elapses.
|
||||
* Returns 1 if the reply landed, 0 on timeout. The deadline is absolute so a
|
||||
* spurious wakeup cannot extend the wait past the requested budget. */
|
||||
static inline int nimffi_sync_wait(NimFfiSyncState* s, uint32_t timeout_ms) {
|
||||
int done;
|
||||
#if defined(_WIN32)
|
||||
ULONGLONG start = GetTickCount64();
|
||||
AcquireSRWLockExclusive(&s->lock);
|
||||
while (!s->done) {
|
||||
ULONGLONG elapsed = GetTickCount64() - start;
|
||||
if (elapsed >= timeout_ms) {
|
||||
break;
|
||||
}
|
||||
if (!SleepConditionVariableSRW(&s->cv, &s->lock,
|
||||
(DWORD)(timeout_ms - elapsed), 0)) {
|
||||
if (GetLastError() == ERROR_TIMEOUT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
done = s->done;
|
||||
ReleaseSRWLockExclusive(&s->lock);
|
||||
#else
|
||||
struct timespec deadline;
|
||||
clock_gettime(CLOCK_REALTIME, &deadline);
|
||||
deadline.tv_sec += (time_t)(timeout_ms / 1000u);
|
||||
deadline.tv_nsec += (long)(timeout_ms % 1000u) * 1000000L;
|
||||
if (deadline.tv_nsec >= 1000000000L) {
|
||||
deadline.tv_sec += 1;
|
||||
deadline.tv_nsec -= 1000000000L;
|
||||
}
|
||||
pthread_mutex_lock(&s->mtx);
|
||||
while (!s->done) {
|
||||
if (pthread_cond_timedwait(&s->cv, &s->mtx, &deadline) == ETIMEDOUT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
done = s->done;
|
||||
pthread_mutex_unlock(&s->mtx);
|
||||
#endif
|
||||
return done;
|
||||
}
|
||||
|
||||
/* Bounded copy of `src` into `buf`, always NUL-terminating when err_len > 0. */
|
||||
static inline void nimffi_copy_err(char* buf, size_t err_len, const char* src) {
|
||||
if (!buf || err_len == 0) {
|
||||
return;
|
||||
}
|
||||
if (!src) {
|
||||
src = "";
|
||||
}
|
||||
size_t n = strlen(src);
|
||||
if (n >= err_len) {
|
||||
n = err_len - 1;
|
||||
}
|
||||
memcpy(buf, src, n);
|
||||
buf[n] = '\0';
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NIM_FFI_SYNC_HELPER_H_INCLUDED */
|
||||
|
||||
@ -151,6 +151,12 @@ task test_c_e2e, "Build and run the C end-to-end tests for the timer example":
|
||||
runOrQuit "cmake --build tests/e2e/c/build --config Debug"
|
||||
runOrQuit "ctest --test-dir tests/e2e/c/build --output-on-failure -C Debug"
|
||||
|
||||
task test_rust_e2e, "Build and run the Rust end-to-end tests for the timer example":
|
||||
# Regenerate the Rust bindings so the suite always runs against fresh codegen,
|
||||
# then drive both the blocking and tokio-async wrappers via `cargo test`.
|
||||
runOrQuit "nimble genbindings_rust"
|
||||
runOrQuit "cargo test --manifest-path tests/e2e/rust/Cargo.toml"
|
||||
|
||||
task test_c_abi_e2e, "Build and run the CBOR-free abi=c C end-to-end test (echo)":
|
||||
# Regenerate the abi=c bindings so the suite always runs against fresh codegen.
|
||||
runOrQuit "nimble genbindings_c_abi_echo"
|
||||
@ -286,9 +292,11 @@ task check_bindings_c, "Verify checked-in C bindings match Nim source":
|
||||
exec "git diff --exit-code --" & " examples/timer/c_bindings/my_timer.h" &
|
||||
" examples/timer/c_bindings/nim_ffi_prelude.h" &
|
||||
" examples/timer/c_bindings/nim_ffi_cbor.h" &
|
||||
" examples/timer/c_bindings/nim_ffi_sync.h" &
|
||||
" examples/timer/c_bindings/CMakeLists.txt" & " examples/echo/c_bindings/echo.h" &
|
||||
" examples/echo/c_bindings/nim_ffi_prelude.h" &
|
||||
" examples/echo/c_bindings/nim_ffi_cbor.h" &
|
||||
" examples/echo/c_bindings/nim_ffi_sync.h" &
|
||||
" examples/echo/c_bindings/CMakeLists.txt"
|
||||
|
||||
task check_bindings_c_abi, "Verify checked-in abi=c C bindings match Nim source":
|
||||
|
||||
@ -22,13 +22,15 @@ const CPtrType* = "uint64_t"
|
||||
const
|
||||
HeaderPreludeTpl = staticRead("templates/c/header_prelude.h.tpl")
|
||||
CborHelpersTpl = staticRead("templates/c/cbor_helpers.h.tpl")
|
||||
SyncCallHelperTpl = staticRead("templates/c/sync_call_helper.h.tpl")
|
||||
CMakeListsTpl = staticRead("templates/c/CMakeLists.txt.tpl")
|
||||
|
||||
# Shared headers written alongside the library header. Their names match the
|
||||
# include guards baked into the templates and the `#include` the cbor header
|
||||
# emits for the prelude.
|
||||
# include guards baked into the templates and the `#include` each emits for
|
||||
# the header it depends on.
|
||||
PreludeHeaderName* = "nim_ffi_prelude.h"
|
||||
CborHeaderName* = "nim_ffi_cbor.h"
|
||||
SyncHeaderName* = "nim_ffi_sync.h"
|
||||
|
||||
const scalarCInfoTable: array[ScalarKind, tuple[cType, suffix: string]] = [
|
||||
skBool: ("bool", "bool"),
|
||||
@ -447,6 +449,41 @@ proc emitReplyTrampolineHead(lines: var seq[string], tramp, boxType, fallback: s
|
||||
lines.add(" return;")
|
||||
lines.add(" }")
|
||||
|
||||
proc emitSyncSubmitPrologue(
|
||||
lines: var seq[string], libName, reqName: string, assigns: seq[string]
|
||||
) =
|
||||
## Shared opening of both `_sync` wrappers: build the per-proc Req envelope,
|
||||
## CBOR-encode it, and allocate the blocking-call state — bailing with
|
||||
## `err_buf` set on encode or allocation failure. Leaves `req_buf`/`req_len`
|
||||
## and `st` in scope for the caller to submit and wait on.
|
||||
lines.add(" " & reqName & " ffi_req;")
|
||||
lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));")
|
||||
for a in assigns:
|
||||
lines.add(a)
|
||||
lines.add(" uint8_t* req_buf = NULL;")
|
||||
lines.add(" size_t req_len = 0;")
|
||||
lines.add(" char* enc_err = NULL;")
|
||||
lines.add(
|
||||
" if (nimffi_encode_to_buf(" & libName & "_encv_" & cToken(reqName) &
|
||||
", &ffi_req, &req_buf, &req_len, &enc_err) != 0) {"
|
||||
)
|
||||
lines.add(
|
||||
" nimffi_copy_err(err_buf, err_len, enc_err ? enc_err : \"encode failed\");"
|
||||
)
|
||||
lines.add(" free(enc_err);")
|
||||
lines.add(" return -1;")
|
||||
lines.add(" }")
|
||||
lines.add(" NimFfiSyncState* st = nimffi_sync_state_new();")
|
||||
lines.add(" if (!st) {")
|
||||
lines.add(" free(req_buf);")
|
||||
lines.add(" nimffi_copy_err(err_buf, err_len, \"out of memory\");")
|
||||
lines.add(" return -1;")
|
||||
lines.add(" }")
|
||||
|
||||
proc emitConstructorSync(
|
||||
lines: var seq[string], reg: var CTypeReg, ctxType, libName: string, ctor: FFIProcMeta
|
||||
)
|
||||
|
||||
proc emitConstructors(
|
||||
lines: var seq[string],
|
||||
reg: var CTypeReg,
|
||||
@ -546,6 +583,85 @@ proc emitConstructors(
|
||||
lines.add(" return 0;")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
emitConstructorSync(lines, reg, ctxType, libName, ctor)
|
||||
|
||||
proc emitConstructorSync(
|
||||
lines: var seq[string],
|
||||
reg: var CTypeReg,
|
||||
ctxType, libName: string,
|
||||
ctor: FFIProcMeta,
|
||||
) =
|
||||
## Blocking companion to `<lib>_ctx_create`: submits the ctor request, waits up
|
||||
## to `timeout_ms` for the address callback, and hands the caller an owned
|
||||
## context via `*out`. Returns 0 on success; non-zero with `err_buf` filled on
|
||||
## failure. The caller releases the context with `<lib>_ctx_destroy`.
|
||||
let reqName = reqStructName(ctor)
|
||||
let (params, assigns) = buildReqParams(reg, ctor.extraParams)
|
||||
let head = "static inline int " & libName & "_ctx_create_sync("
|
||||
let tail = ctxType & "** out, char* err_buf, size_t err_len, uint32_t timeout_ms) {"
|
||||
lines.add(
|
||||
if params.len > 0:
|
||||
head & params.join(", ") & ", " & tail
|
||||
else:
|
||||
head & tail
|
||||
)
|
||||
emitSyncSubmitPrologue(lines, libName, reqName, assigns)
|
||||
lines.add(" (void)" & ctor.procName & "(req_buf, req_len, nimffi_sync_cb, st);")
|
||||
lines.add(" free(req_buf);")
|
||||
lines.add(" if (!nimffi_sync_wait(st, timeout_ms)) {")
|
||||
lines.add(" nimffi_copy_err(err_buf, err_len, \"FFI create timed out\");")
|
||||
lines.add(" nimffi_sync_state_release(st);")
|
||||
lines.add(" return -1;")
|
||||
lines.add(" }")
|
||||
lines.add(" if (!st->ok) {")
|
||||
lines.add(
|
||||
" nimffi_copy_err(err_buf, err_len, st->err ? st->err : \"FFI create failed\");"
|
||||
)
|
||||
lines.add(" int rc = st->ret_code ? st->ret_code : -1;")
|
||||
lines.add(" nimffi_sync_state_release(st);")
|
||||
lines.add(" return rc;")
|
||||
lines.add(" }")
|
||||
lines.add(" NimFfiStr addr;")
|
||||
lines.add(" memset(&addr, 0, sizeof(addr));")
|
||||
lines.add(" char* dec_err = NULL;")
|
||||
lines.add(
|
||||
" if (nimffi_decode_from_buf(" & libName &
|
||||
"_decv_Str, st->bytes, st->bytes_len, &addr, &dec_err) != 0) {"
|
||||
)
|
||||
lines.add(
|
||||
" nimffi_copy_err(err_buf, err_len, dec_err ? dec_err : \"decode failed\");"
|
||||
)
|
||||
lines.add(" free(dec_err);")
|
||||
lines.add(" nimffi_sync_state_release(st);")
|
||||
lines.add(" return -1;")
|
||||
lines.add(" }")
|
||||
lines.add(" char* endp = NULL;")
|
||||
lines.add(
|
||||
" unsigned long long a = addr.data ? strtoull(addr.data, &endp, 10) : 0;"
|
||||
)
|
||||
lines.add(" bool ok = addr.data && addr.len > 0 && endp && *endp == '\\0';")
|
||||
lines.add(" nimffi_free_str(&addr);")
|
||||
lines.add(" if (!ok) {")
|
||||
lines.add(
|
||||
" nimffi_copy_err(err_buf, err_len, \"FFI create returned non-numeric address\");"
|
||||
)
|
||||
lines.add(" nimffi_sync_state_release(st);")
|
||||
lines.add(" return -1;")
|
||||
lines.add(" }")
|
||||
lines.add(
|
||||
" " & ctxType & "* c = (" & ctxType & "*)calloc(1, sizeof(" & ctxType & "));"
|
||||
)
|
||||
lines.add(" if (!c) {")
|
||||
lines.add(" nimffi_copy_err(err_buf, err_len, \"out of memory\");")
|
||||
lines.add(" nimffi_sync_state_release(st);")
|
||||
lines.add(" return -1;")
|
||||
lines.add(" }")
|
||||
lines.add(" c->ptr = (void*)(uintptr_t)a;")
|
||||
lines.add(" *out = c;")
|
||||
lines.add(" nimffi_sync_state_release(st);")
|
||||
lines.add(" return 0;")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
proc emitDestructor(
|
||||
lines: var seq[string],
|
||||
@ -720,6 +836,77 @@ proc emitMethod(
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
proc emitMethodSync(
|
||||
lines: var seq[string], reg: var CTypeReg, ctxType, libName: string, m: FFIProcMeta
|
||||
) =
|
||||
## Blocking companion to `<lib>_ctx_<method>`: submits the request, waits up to
|
||||
## `timeout_ms` for the reply, and decodes it into a caller-owned `*out`.
|
||||
## Returns 0 on success; non-zero with `err_buf` filled on failure. When the
|
||||
## reply owns heap memory the caller releases it with the generated
|
||||
## `<lib>_free_<Type>()` helper.
|
||||
let stripped = stripLibPrefix(m.procName, libName)
|
||||
let reqName = reqStructName(m)
|
||||
let retC = cReturnType(reg, m)
|
||||
let retFree = freeFn(reg, retC)
|
||||
let (params, assigns) = buildReqParams(reg, m.extraParams)
|
||||
|
||||
let head =
|
||||
"static inline int " & libName & "_ctx_" & stripped & "_sync(const " & ctxType &
|
||||
"* ctx, "
|
||||
let tail = retC & "* out, char* err_buf, size_t err_len, uint32_t timeout_ms) {"
|
||||
lines.add(
|
||||
if params.len > 0:
|
||||
head & params.join(", ") & ", " & tail
|
||||
else:
|
||||
head & tail
|
||||
)
|
||||
emitSyncSubmitPrologue(lines, libName, reqName, assigns)
|
||||
lines.add(
|
||||
" int ret = " & m.procName & "(ctx->ptr, nimffi_sync_cb, st, req_buf, req_len);"
|
||||
)
|
||||
lines.add(" free(req_buf);")
|
||||
lines.add(" if (ret == NIMFFI_RET_MISSING_CALLBACK) {")
|
||||
lines.add(
|
||||
" nimffi_copy_err(err_buf, err_len, \"RET_MISSING_CALLBACK (internal error)\");"
|
||||
)
|
||||
lines.add(" nimffi_sync_state_release(st);")
|
||||
lines.add(" nimffi_sync_state_release(st);")
|
||||
lines.add(" return -1;")
|
||||
lines.add(" }")
|
||||
lines.add(" if (!nimffi_sync_wait(st, timeout_ms)) {")
|
||||
lines.add(" nimffi_copy_err(err_buf, err_len, \"FFI call timed out\");")
|
||||
lines.add(" nimffi_sync_state_release(st);")
|
||||
lines.add(" return -1;")
|
||||
lines.add(" }")
|
||||
lines.add(" if (!st->ok) {")
|
||||
lines.add(
|
||||
" nimffi_copy_err(err_buf, err_len, st->err ? st->err : \"FFI call failed\");"
|
||||
)
|
||||
lines.add(" int rc = st->ret_code ? st->ret_code : -1;")
|
||||
lines.add(" nimffi_sync_state_release(st);")
|
||||
lines.add(" return rc;")
|
||||
lines.add(" }")
|
||||
lines.add(" memset(out, 0, sizeof(*out));")
|
||||
lines.add(" char* dec_err = NULL;")
|
||||
lines.add(
|
||||
" int dec = nimffi_decode_from_buf(" & libName & "_decv_" & cToken(retC) &
|
||||
", st->bytes, st->bytes_len, out, &dec_err);"
|
||||
)
|
||||
lines.add(" if (dec != 0) {")
|
||||
lines.add(
|
||||
" nimffi_copy_err(err_buf, err_len, dec_err ? dec_err : \"decode failed\");"
|
||||
)
|
||||
lines.add(" free(dec_err);")
|
||||
if retFree.len > 0:
|
||||
lines.add(" " & retFree & "(out);")
|
||||
lines.add(" nimffi_sync_state_release(st);")
|
||||
lines.add(" return -1;")
|
||||
lines.add(" }")
|
||||
lines.add(" nimffi_sync_state_release(st);")
|
||||
lines.add(" return 0;")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
proc newCTypeReg(
|
||||
libName, libType: string, types: seq[FFITypeMeta], procs: seq[FFIProcMeta]
|
||||
): CTypeReg =
|
||||
@ -769,6 +956,12 @@ func generateCCborHeader*(): string =
|
||||
## agnostic, so it too is emitted verbatim.
|
||||
CborHelpersTpl & "\n"
|
||||
|
||||
func generateCSyncHeader*(): string =
|
||||
## The `nim_ffi_sync.h` shared header: the blocking-call helper backing the
|
||||
## generated `_sync` wrappers. Includes the cbor header and is library-
|
||||
## agnostic, so it is emitted verbatim.
|
||||
SyncCallHelperTpl & "\n"
|
||||
|
||||
proc generateCLibHeader*(
|
||||
procs: seq[FFIProcMeta],
|
||||
types: seq[FFITypeMeta],
|
||||
@ -791,6 +984,7 @@ proc generateCLibHeader*(
|
||||
lines.add("#ifndef " & guard)
|
||||
lines.add("#define " & guard)
|
||||
lines.add("#include \"" & CborHeaderName & "\"")
|
||||
lines.add("#include \"" & SyncHeaderName & "\"")
|
||||
lines.add("")
|
||||
|
||||
lines.add("/* ============================================================ */")
|
||||
@ -869,6 +1063,7 @@ proc generateCLibHeader*(
|
||||
emitListenerApi(lines, ctxType, libType, libName, events)
|
||||
for m in methods:
|
||||
emitMethod(lines, reg, ctxType, libType, libName, m)
|
||||
emitMethodSync(lines, reg, ctxType, libName, m)
|
||||
|
||||
lines.add("#endif /* " & guard & " */")
|
||||
lines.join("\n") & "\n"
|
||||
@ -888,6 +1083,7 @@ proc generateCBindings*(
|
||||
createDir(outputDir)
|
||||
writeFile(outputDir / PreludeHeaderName, generateCPreludeHeader())
|
||||
writeFile(outputDir / CborHeaderName, generateCCborHeader())
|
||||
writeFile(outputDir / SyncHeaderName, generateCSyncHeader())
|
||||
writeFile(
|
||||
outputDir / (libName & ".h"), generateCLibHeader(procs, types, libName, events)
|
||||
)
|
||||
|
||||
@ -28,9 +28,9 @@ find_package(Threads REQUIRED)
|
||||
add_library({{LIB}}_headers INTERFACE)
|
||||
target_include_directories({{LIB}}_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_link_libraries({{LIB}}_headers INTERFACE {{LIB}} tinycbor Threads::Threads)
|
||||
# The generated header is async (no blocking helper), but consumer code that
|
||||
# waits on a result callback typically uses nanosleep / pthreads, which need a
|
||||
# POSIX feature level that strict `-std=c11` hides. Define it for consumers.
|
||||
# The generated `_sync` wrappers block on a pthread condvar, and consumer code
|
||||
# that polls a result callback typically uses nanosleep — both need a POSIX
|
||||
# feature level that strict `-std=c11` hides. Define it for consumers.
|
||||
target_compile_definitions({{LIB}}_headers INTERFACE _POSIX_C_SOURCE=200809L)
|
||||
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c")
|
||||
|
||||
192
ffi/codegen/templates/c/sync_call_helper.h.tpl
Normal file
192
ffi/codegen/templates/c/sync_call_helper.h.tpl
Normal file
@ -0,0 +1,192 @@
|
||||
#ifndef NIM_FFI_SYNC_HELPER_H_INCLUDED
|
||||
#define NIM_FFI_SYNC_HELPER_H_INCLUDED
|
||||
/* Blocking-call helper shared by the generated <lib>_ctx_<method>_sync wrappers.
|
||||
* Turns the async callback ABI into a synchronous request/reply: submit the
|
||||
* call, block on a condition variable until its single callback fires (or the
|
||||
* timeout elapses), then hand the raw CBOR payload / error text back so the
|
||||
* generated wrapper can decode it into a caller-owned out-param.
|
||||
*
|
||||
* The state is heap-allocated and reference-counted (2: the waiter and the
|
||||
* callback). A callback that fires after the caller has already timed out thus
|
||||
* writes into a still-live object and drops the last reference cleanly — no
|
||||
* use-after-free, no leak. Uses pthreads on POSIX and SRWLOCK/CONDITION_VARIABLE
|
||||
* on Win32, the same platform split the example programs use. */
|
||||
#include "nim_ffi_cbor.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
# include <windows.h>
|
||||
#else
|
||||
# include <pthread.h>
|
||||
# include <errno.h>
|
||||
# include <time.h>
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
#if defined(_WIN32)
|
||||
SRWLOCK lock;
|
||||
CONDITION_VARIABLE cv;
|
||||
#else
|
||||
pthread_mutex_t mtx;
|
||||
pthread_cond_t cv;
|
||||
#endif
|
||||
int done;
|
||||
int ok;
|
||||
int ret_code; /* raw `ret` from the callback (non-zero on error) */
|
||||
uint8_t* bytes; /* owned copy of the CBOR reply payload on success */
|
||||
size_t bytes_len;
|
||||
char* err; /* owned NUL-terminated error text on failure */
|
||||
int refs; /* guarded by lock/mtx; the release reaching 0 frees */
|
||||
} NimFfiSyncState;
|
||||
|
||||
static inline NimFfiSyncState* nimffi_sync_state_new(void) {
|
||||
NimFfiSyncState* s = (NimFfiSyncState*)calloc(1, sizeof(NimFfiSyncState));
|
||||
if (!s) {
|
||||
return NULL;
|
||||
}
|
||||
s->refs = 2;
|
||||
#if defined(_WIN32)
|
||||
InitializeSRWLock(&s->lock);
|
||||
InitializeConditionVariable(&s->cv);
|
||||
#else
|
||||
if (pthread_mutex_init(&s->mtx, NULL) != 0) {
|
||||
free(s);
|
||||
return NULL;
|
||||
}
|
||||
if (pthread_cond_init(&s->cv, NULL) != 0) {
|
||||
pthread_mutex_destroy(&s->mtx);
|
||||
free(s);
|
||||
return NULL;
|
||||
}
|
||||
#endif
|
||||
return s;
|
||||
}
|
||||
|
||||
static inline void nimffi_sync_state_release(NimFfiSyncState* s) {
|
||||
if (!s) {
|
||||
return;
|
||||
}
|
||||
int r;
|
||||
#if defined(_WIN32)
|
||||
AcquireSRWLockExclusive(&s->lock);
|
||||
r = --s->refs;
|
||||
ReleaseSRWLockExclusive(&s->lock);
|
||||
#else
|
||||
pthread_mutex_lock(&s->mtx);
|
||||
r = --s->refs;
|
||||
pthread_mutex_unlock(&s->mtx);
|
||||
#endif
|
||||
if (r != 0) {
|
||||
return;
|
||||
}
|
||||
#if !defined(_WIN32)
|
||||
pthread_mutex_destroy(&s->mtx);
|
||||
pthread_cond_destroy(&s->cv);
|
||||
#endif
|
||||
free(s->bytes);
|
||||
free(s->err);
|
||||
free(s);
|
||||
}
|
||||
|
||||
/* FFICallback-conforming sink: copies the reply/error out of the borrowed
|
||||
* msg/len buffer (owned by the binding only for this call), wakes the waiter,
|
||||
* then drops the callback's reference. */
|
||||
static inline void nimffi_sync_cb(int ret, const char* msg, size_t len, void* ud) {
|
||||
NimFfiSyncState* s = (NimFfiSyncState*)ud;
|
||||
#if defined(_WIN32)
|
||||
AcquireSRWLockExclusive(&s->lock);
|
||||
#else
|
||||
pthread_mutex_lock(&s->mtx);
|
||||
#endif
|
||||
s->ret_code = ret;
|
||||
s->ok = (ret == 0);
|
||||
if (msg && len > 0) {
|
||||
if (s->ok) {
|
||||
uint8_t* p = (uint8_t*)malloc(len);
|
||||
if (p) {
|
||||
memcpy(p, msg, len);
|
||||
s->bytes = p;
|
||||
s->bytes_len = len;
|
||||
}
|
||||
} else {
|
||||
s->err = nimffi_dup_cstr_n(msg, len);
|
||||
}
|
||||
}
|
||||
s->done = 1;
|
||||
#if defined(_WIN32)
|
||||
WakeConditionVariable(&s->cv);
|
||||
ReleaseSRWLockExclusive(&s->lock);
|
||||
#else
|
||||
pthread_cond_signal(&s->cv);
|
||||
pthread_mutex_unlock(&s->mtx);
|
||||
#endif
|
||||
nimffi_sync_state_release(s);
|
||||
}
|
||||
|
||||
/* Blocks until the callback marks the state done or `timeout_ms` elapses.
|
||||
* Returns 1 if the reply landed, 0 on timeout. The deadline is absolute so a
|
||||
* spurious wakeup cannot extend the wait past the requested budget. */
|
||||
static inline int nimffi_sync_wait(NimFfiSyncState* s, uint32_t timeout_ms) {
|
||||
int done;
|
||||
#if defined(_WIN32)
|
||||
ULONGLONG start = GetTickCount64();
|
||||
AcquireSRWLockExclusive(&s->lock);
|
||||
while (!s->done) {
|
||||
ULONGLONG elapsed = GetTickCount64() - start;
|
||||
if (elapsed >= timeout_ms) {
|
||||
break;
|
||||
}
|
||||
if (!SleepConditionVariableSRW(&s->cv, &s->lock,
|
||||
(DWORD)(timeout_ms - elapsed), 0)) {
|
||||
if (GetLastError() == ERROR_TIMEOUT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
done = s->done;
|
||||
ReleaseSRWLockExclusive(&s->lock);
|
||||
#else
|
||||
struct timespec deadline;
|
||||
clock_gettime(CLOCK_REALTIME, &deadline);
|
||||
deadline.tv_sec += (time_t)(timeout_ms / 1000u);
|
||||
deadline.tv_nsec += (long)(timeout_ms % 1000u) * 1000000L;
|
||||
if (deadline.tv_nsec >= 1000000000L) {
|
||||
deadline.tv_sec += 1;
|
||||
deadline.tv_nsec -= 1000000000L;
|
||||
}
|
||||
pthread_mutex_lock(&s->mtx);
|
||||
while (!s->done) {
|
||||
if (pthread_cond_timedwait(&s->cv, &s->mtx, &deadline) == ETIMEDOUT) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
done = s->done;
|
||||
pthread_mutex_unlock(&s->mtx);
|
||||
#endif
|
||||
return done;
|
||||
}
|
||||
|
||||
/* Bounded copy of `src` into `buf`, always NUL-terminating when err_len > 0. */
|
||||
static inline void nimffi_copy_err(char* buf, size_t err_len, const char* src) {
|
||||
if (!buf || err_len == 0) {
|
||||
return;
|
||||
}
|
||||
if (!src) {
|
||||
src = "";
|
||||
}
|
||||
size_t n = strlen(src);
|
||||
if (n >= err_len) {
|
||||
n = err_len - 1;
|
||||
}
|
||||
memcpy(buf, src, n);
|
||||
buf[n] = '\0';
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* NIM_FFI_SYNC_HELPER_H_INCLUDED */
|
||||
@ -3,12 +3,12 @@
|
||||
* multi-parameter requests, the error channel and the typed event listener —
|
||||
* and aborts (non-zero exit) on the first failure so ctest reports it.
|
||||
*
|
||||
* The binding is asynchronous: every call takes a result callback and the
|
||||
* reply/error are owned by the binding and valid only for the duration of that
|
||||
* callback. So each callback *copies out* what it needs into a waiter struct,
|
||||
* and the test polls a `done` flag (the same volatile-flag pattern the event
|
||||
* listener uses) to turn each async call back into a sequential check. The
|
||||
* caller never frees reply data or error strings — that is the whole point. */
|
||||
* Both call surfaces are covered. The async API takes a result callback whose
|
||||
* reply/error are owned by the binding and valid only for that callback, so the
|
||||
* checks below *copy out* what they need into a waiter struct and poll a `done`
|
||||
* flag to sequence each call. The blocking `_sync` API (further down) collapses
|
||||
* that dance into one call whose out-param is caller-owned and freed with the
|
||||
* generated helper. */
|
||||
#include "my_timer.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
@ -249,6 +249,133 @@ static void test_event(MyTimerCtx* ctx) {
|
||||
assert(my_timer_ctx_remove_event_listener(ctx, handle) == true);
|
||||
}
|
||||
|
||||
/* ── Blocking `_sync` path ──────────────────────────────────────────────────
|
||||
* The generated `_sync` wrappers collapse the submit + wait + copy-out dance
|
||||
* above into a single blocking call: on success `*out` is caller-owned and
|
||||
* released with the generated free helper; on failure the error text lands in
|
||||
* `err`. These mirror the async checks to prove both surfaces round-trip. */
|
||||
|
||||
#define SYNC_TIMEOUT_MS 5000
|
||||
|
||||
static MyTimerCtx* make_ctx_sync(void) {
|
||||
MyTimerCtx* ctx = NULL;
|
||||
char err[256] = {0};
|
||||
TimerConfig config = {nimffi_str("c-e2e-sync")};
|
||||
int rc = my_timer_ctx_create_sync(&config, &ctx, err, sizeof(err), SYNC_TIMEOUT_MS);
|
||||
if (rc != 0) {
|
||||
fprintf(stderr, "create_sync failed: %s\n", err[0] ? err : "?");
|
||||
}
|
||||
assert(rc == 0);
|
||||
assert(ctx != NULL);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
static void test_version_sync(MyTimerCtx* ctx) {
|
||||
NimFfiStr version = {0};
|
||||
char err[256] = {0};
|
||||
int rc = my_timer_ctx_version_sync(ctx, &version, err, sizeof(err), SYNC_TIMEOUT_MS);
|
||||
assert(rc == 0);
|
||||
assert(version.data != NULL);
|
||||
assert(strcmp(version.data, "nim-timer v0.1.0") == 0);
|
||||
nimffi_free_str(&version);
|
||||
}
|
||||
|
||||
static void test_echo_sync(MyTimerCtx* ctx) {
|
||||
EchoRequest req = {nimffi_str("hello-sync"), 10};
|
||||
EchoResponse resp = {0};
|
||||
char err[256] = {0};
|
||||
int rc = my_timer_ctx_echo_sync(ctx, &req, &resp, err, sizeof(err), SYNC_TIMEOUT_MS);
|
||||
assert(rc == 0);
|
||||
assert(resp.echoed.data && strcmp(resp.echoed.data, "hello-sync") == 0);
|
||||
assert(resp.timerName.data && strcmp(resp.timerName.data, "c-e2e-sync") == 0);
|
||||
my_timer_free_EchoResponse(&resp);
|
||||
}
|
||||
|
||||
static void test_complex_sync(MyTimerCtx* ctx) {
|
||||
EchoRequest items[2] = {{nimffi_str("one"), 1}, {nimffi_str("two"), 2}};
|
||||
NimFfiStr tags[2] = {nimffi_str("a"), nimffi_str("b")};
|
||||
ComplexRequest req;
|
||||
req.messages.data = items;
|
||||
req.messages.len = 2;
|
||||
req.tags.data = tags;
|
||||
req.tags.len = 2;
|
||||
req.note.has_value = true;
|
||||
req.note.value = nimffi_str("note");
|
||||
req.retries.has_value = false;
|
||||
req.retries.value = 0;
|
||||
|
||||
ComplexResponse resp = {0};
|
||||
char err[256] = {0};
|
||||
int rc =
|
||||
my_timer_ctx_complex_sync(ctx, &req, &resp, err, sizeof(err), SYNC_TIMEOUT_MS);
|
||||
assert(rc == 0);
|
||||
assert(resp.itemCount == 2);
|
||||
assert(resp.hasNote == true);
|
||||
assert(resp.summary.data && strstr(resp.summary.data, "note=note") != NULL);
|
||||
my_timer_free_ComplexResponse(&resp);
|
||||
}
|
||||
|
||||
static void test_schedule_ok_sync(MyTimerCtx* ctx) {
|
||||
NimFfiStr payload[1] = {nimffi_str("p")};
|
||||
JobSpec job;
|
||||
job.name = nimffi_str("rollup");
|
||||
job.payload.data = payload;
|
||||
job.payload.len = 1;
|
||||
job.priority = 1;
|
||||
|
||||
NimFfiStr retry_on[1] = {nimffi_str("timeout")};
|
||||
RetryPolicy retry;
|
||||
retry.maxAttempts = 3;
|
||||
retry.backoffMs = 100;
|
||||
retry.retryOn.data = retry_on;
|
||||
retry.retryOn.len = 1;
|
||||
|
||||
ScheduleConfig sched;
|
||||
sched.startAtMs = 1000;
|
||||
sched.intervalMs = 0;
|
||||
sched.jitter.has_value = false;
|
||||
sched.jitter.value = 0;
|
||||
|
||||
ScheduleResult resp = {0};
|
||||
char err[256] = {0};
|
||||
int rc = my_timer_ctx_schedule_sync(ctx, &job, &retry, &sched, &resp, err,
|
||||
sizeof(err), SYNC_TIMEOUT_MS);
|
||||
assert(rc == 0);
|
||||
assert(resp.jobId.data && strcmp(resp.jobId.data, "c-e2e-sync:rollup") == 0);
|
||||
assert(resp.willRunCount == 1);
|
||||
my_timer_free_ScheduleResult(&resp);
|
||||
}
|
||||
|
||||
static void test_schedule_error_sync(MyTimerCtx* ctx) {
|
||||
NimFfiStr payload[1] = {nimffi_str("p")};
|
||||
JobSpec job;
|
||||
job.name = nimffi_str(""); /* empty name → handler returns err */
|
||||
job.payload.data = payload;
|
||||
job.payload.len = 1;
|
||||
job.priority = 1;
|
||||
|
||||
NimFfiStr retry_on[1] = {nimffi_str("timeout")};
|
||||
RetryPolicy retry;
|
||||
retry.maxAttempts = 3;
|
||||
retry.backoffMs = 100;
|
||||
retry.retryOn.data = retry_on;
|
||||
retry.retryOn.len = 1;
|
||||
|
||||
ScheduleConfig sched;
|
||||
sched.startAtMs = 0;
|
||||
sched.intervalMs = 0;
|
||||
sched.jitter.has_value = false;
|
||||
sched.jitter.value = 0;
|
||||
|
||||
ScheduleResult resp = {0};
|
||||
char err[256] = {0};
|
||||
int rc = my_timer_ctx_schedule_sync(ctx, &job, &retry, &sched, &resp, err,
|
||||
sizeof(err), SYNC_TIMEOUT_MS);
|
||||
assert(rc != 0);
|
||||
assert(err[0] != '\0');
|
||||
assert(strstr(err, "job name") != NULL);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
MyTimerCtx* ctx = make_ctx();
|
||||
test_version(ctx);
|
||||
@ -258,6 +385,15 @@ int main(void) {
|
||||
test_schedule_error(ctx);
|
||||
test_event(ctx);
|
||||
my_timer_ctx_destroy(ctx);
|
||||
|
||||
MyTimerCtx* sctx = make_ctx_sync();
|
||||
test_version_sync(sctx);
|
||||
test_echo_sync(sctx);
|
||||
test_complex_sync(sctx);
|
||||
test_schedule_ok_sync(sctx);
|
||||
test_schedule_error_sync(sctx);
|
||||
my_timer_ctx_destroy(sctx);
|
||||
|
||||
printf("all C e2e checks passed\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
230
tests/e2e/rust/Cargo.lock
generated
Normal file
230
tests/e2e/rust/Cargo.lock
generated
Normal file
@ -0,0 +1,230 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"ciborium-ll",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-io"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
|
||||
|
||||
[[package]]
|
||||
name = "ciborium-ll"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
|
||||
dependencies = [
|
||||
"ciborium-io",
|
||||
"half",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
|
||||
dependencies = [
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "my_timer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ciborium",
|
||||
"flume",
|
||||
"serde",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_e2e"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"my_timer",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tokio-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.54"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
15
tests/e2e/rust/Cargo.toml
Normal file
15
tests/e2e/rust/Cargo.toml
Normal file
@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "rust_e2e"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# End-to-end tests for the generated Rust `my_timer` crate. Depends on the same
|
||||
# `rust_bindings` crate the examples use; its build.rs compiles the Nim dylib,
|
||||
# so `cargo test` here drives the full CBOR round-trip (Rust -> Nim FFI thread ->
|
||||
# chronos -> Rust) over both the blocking and the tokio-async wrappers.
|
||||
[dependencies]
|
||||
my_timer = { path = "../../../examples/timer/rust_bindings" }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time", "sync"] }
|
||||
28
tests/e2e/rust/README.md
Normal file
28
tests/e2e/rust/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Rust end-to-end tests
|
||||
|
||||
End-to-end tests for the auto-generated Rust `my_timer` crate (from
|
||||
`examples/timer/rust_bindings`), the Rust counterpart to `tests/e2e/c` and
|
||||
`tests/e2e/cpp`.
|
||||
|
||||
They link the same `rust_bindings` crate the examples use — its `build.rs`
|
||||
compiles the Nim shared library — and exercise the full FFI round-trip (CBOR
|
||||
encode → Nim FFI thread → chronos → CBOR decode) over **both** generated call
|
||||
surfaces:
|
||||
|
||||
- the **blocking** wrappers (`ctx.version()`, `ctx.echo(..)`, `ctx.schedule(..)`),
|
||||
which block on a condition variable until the reply lands, and
|
||||
- the **tokio-async** wrappers (`ctx.version_async().await`, …), which wake the
|
||||
awaiting task from the FFI callback without blocking a runtime thread.
|
||||
|
||||
## Running
|
||||
|
||||
```sh
|
||||
# From the repo root
|
||||
nimble test_rust_e2e
|
||||
|
||||
# Or directly
|
||||
cd tests/e2e/rust && cargo test
|
||||
```
|
||||
|
||||
To regenerate the `rust_bindings` crate under test, run `nimble genbindings_rust`
|
||||
from the repo root.
|
||||
22
tests/e2e/rust/build.rs
Normal file
22
tests/e2e/rust/build.rs
Normal file
@ -0,0 +1,22 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
// The `my_timer` bindings' build.rs compiles the Nim shared library into the
|
||||
// repo root and adds it to the link search path — but a dependency's
|
||||
// `rustc-link-arg` does not propagate to this crate's test binaries, so they
|
||||
// would fail to find `libmy_timer.so` at runtime. Embed an rpath to the repo
|
||||
// root here so `cargo test` works without setting LD_LIBRARY_PATH. (Windows has
|
||||
// no rpath; there the loader finds the DLL via PATH / the working directory.)
|
||||
fn main() {
|
||||
if cfg!(target_os = "windows") {
|
||||
return;
|
||||
}
|
||||
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let mut repo_root = manifest;
|
||||
while !repo_root.join("ffi.nimble").exists() {
|
||||
assert!(
|
||||
repo_root.pop(),
|
||||
"could not locate repo root (no ffi.nimble ancestor)"
|
||||
);
|
||||
}
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", repo_root.display());
|
||||
}
|
||||
326
tests/e2e/rust/tests/e2e.rs
Normal file
326
tests/e2e/rust/tests/e2e.rs
Normal file
@ -0,0 +1,326 @@
|
||||
//! End-to-end tests for the generated Rust `my_timer` bindings.
|
||||
//!
|
||||
//! Mirrors the C and C++ e2e suites: constructor, methods, nested seq/Option
|
||||
//! payloads, multi-parameter requests, the error channel and the typed event
|
||||
//! listener. Both generated call surfaces are covered — the blocking wrappers
|
||||
//! (`ctx.echo(..)`) and the tokio-async wrappers (`ctx.echo_async(..).await`).
|
||||
|
||||
use std::sync::mpsc;
|
||||
use std::time::Duration;
|
||||
|
||||
use my_timer::{
|
||||
ComplexRequest, EchoEvent, EchoRequest, JobSpec, MyTimerCtx, RetryPolicy, ScheduleConfig,
|
||||
TimerConfig,
|
||||
};
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
fn make_ctx(name: &str) -> MyTimerCtx {
|
||||
MyTimerCtx::create(TimerConfig { name: name.into() }, TIMEOUT).expect("create failed")
|
||||
}
|
||||
|
||||
// ── Blocking path ───────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn create_and_version_blocking() {
|
||||
let ctx = make_ctx("version-blocking");
|
||||
assert_eq!(ctx.version().expect("version failed"), "nim-timer v0.1.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_round_trips_message_and_timer_name() {
|
||||
let ctx = make_ctx("echo-ctx");
|
||||
let resp = ctx
|
||||
.echo(EchoRequest {
|
||||
message: "hello".into(),
|
||||
delay_ms: 10,
|
||||
})
|
||||
.expect("echo failed");
|
||||
assert_eq!(resp.echoed, "hello");
|
||||
assert_eq!(resp.timer_name, "echo-ctx");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn echo_honours_delay() {
|
||||
let ctx = make_ctx("echo-delay");
|
||||
let start = std::time::Instant::now();
|
||||
let resp = ctx
|
||||
.echo(EchoRequest {
|
||||
message: "waited".into(),
|
||||
delay_ms: 150,
|
||||
})
|
||||
.expect("echo failed");
|
||||
let elapsed = start.elapsed();
|
||||
assert_eq!(resp.echoed, "waited");
|
||||
assert!(
|
||||
elapsed >= Duration::from_millis(130),
|
||||
"echo returned too early: {elapsed:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_with_optional_note_present() {
|
||||
let ctx = make_ctx("complex-1");
|
||||
let resp = ctx
|
||||
.complex(ComplexRequest {
|
||||
messages: vec![
|
||||
EchoRequest {
|
||||
message: "a".into(),
|
||||
delay_ms: 1,
|
||||
},
|
||||
EchoRequest {
|
||||
message: "b".into(),
|
||||
delay_ms: 2,
|
||||
},
|
||||
],
|
||||
tags: vec!["tag1".into(), "tag2".into()],
|
||||
note: Some("a note".into()),
|
||||
retries: Some(2),
|
||||
})
|
||||
.expect("complex failed");
|
||||
assert_eq!(resp.item_count, 2);
|
||||
assert!(resp.has_note);
|
||||
assert!(
|
||||
resp.summary.contains("note=a note"),
|
||||
"summary: {}",
|
||||
resp.summary
|
||||
);
|
||||
assert!(
|
||||
resp.summary.contains("retries=2"),
|
||||
"summary: {}",
|
||||
resp.summary
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complex_with_optional_note_absent() {
|
||||
let ctx = make_ctx("complex-2");
|
||||
let resp = ctx
|
||||
.complex(ComplexRequest {
|
||||
messages: vec![],
|
||||
tags: vec![],
|
||||
note: None,
|
||||
retries: None,
|
||||
})
|
||||
.expect("complex failed");
|
||||
assert_eq!(resp.item_count, 0);
|
||||
assert!(!resp.has_note);
|
||||
assert!(
|
||||
resp.summary.contains("note=<none>"),
|
||||
"summary: {}",
|
||||
resp.summary
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schedule_three_complex_params() {
|
||||
let ctx = make_ctx("schedule-ctx");
|
||||
let resp = ctx
|
||||
.schedule(
|
||||
JobSpec {
|
||||
name: "nightly-rollup".into(),
|
||||
payload: vec!["rollup".into(), "v2".into()],
|
||||
priority: 10,
|
||||
},
|
||||
RetryPolicy {
|
||||
max_attempts: 3,
|
||||
backoff_ms: 500,
|
||||
retry_on: vec!["timeout".into(), "5xx".into()],
|
||||
},
|
||||
ScheduleConfig {
|
||||
start_at_ms: 1_000,
|
||||
interval_ms: 15_000,
|
||||
jitter: Some(250),
|
||||
},
|
||||
)
|
||||
.expect("schedule failed");
|
||||
assert_eq!(resp.job_id, "schedule-ctx:nightly-rollup");
|
||||
assert!(resp.will_run_count >= 1);
|
||||
}
|
||||
|
||||
// The error channel: an empty JobSpec.name makes the handler return
|
||||
// err("job name must not be empty"), surfaced as an Err(String).
|
||||
#[test]
|
||||
fn schedule_empty_name_is_an_error() {
|
||||
let ctx = make_ctx("schedule-err");
|
||||
let res = ctx.schedule(
|
||||
JobSpec {
|
||||
name: "".into(),
|
||||
payload: vec![],
|
||||
priority: 0,
|
||||
},
|
||||
RetryPolicy {
|
||||
max_attempts: 1,
|
||||
backoff_ms: 10,
|
||||
retry_on: vec![],
|
||||
},
|
||||
ScheduleConfig {
|
||||
start_at_ms: 0,
|
||||
interval_ms: 0,
|
||||
jitter: None,
|
||||
},
|
||||
);
|
||||
let err = res.expect_err("expected schedule to fail on empty job name");
|
||||
assert_eq!(err, "job name must not be empty");
|
||||
}
|
||||
|
||||
// Independent contexts keep their own state; an error on one must not poison it.
|
||||
#[test]
|
||||
fn independent_contexts_keep_their_own_state() {
|
||||
let a = make_ctx("alpha");
|
||||
let b = make_ctx("beta");
|
||||
assert_eq!(
|
||||
a.echo(EchoRequest {
|
||||
message: "x".into(),
|
||||
delay_ms: 5
|
||||
})
|
||||
.unwrap()
|
||||
.timer_name,
|
||||
"alpha"
|
||||
);
|
||||
assert_eq!(
|
||||
b.echo(EchoRequest {
|
||||
message: "x".into(),
|
||||
delay_ms: 5
|
||||
})
|
||||
.unwrap()
|
||||
.timer_name,
|
||||
"beta"
|
||||
);
|
||||
|
||||
// Trigger an error on `a`, then prove both contexts still work.
|
||||
let _ = a.schedule(
|
||||
JobSpec {
|
||||
name: "".into(),
|
||||
payload: vec![],
|
||||
priority: 0,
|
||||
},
|
||||
RetryPolicy {
|
||||
max_attempts: 1,
|
||||
backoff_ms: 10,
|
||||
retry_on: vec![],
|
||||
},
|
||||
ScheduleConfig {
|
||||
start_at_ms: 0,
|
||||
interval_ms: 0,
|
||||
jitter: None,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
a.echo(EchoRequest {
|
||||
message: "again".into(),
|
||||
delay_ms: 0
|
||||
})
|
||||
.unwrap()
|
||||
.echoed,
|
||||
"again"
|
||||
);
|
||||
assert_eq!(
|
||||
b.echo(EchoRequest {
|
||||
message: "still".into(),
|
||||
delay_ms: 0
|
||||
})
|
||||
.unwrap()
|
||||
.timer_name,
|
||||
"beta"
|
||||
);
|
||||
}
|
||||
|
||||
// ── Async path ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_and_version_async() {
|
||||
let ctx = MyTimerCtx::new_async(
|
||||
TimerConfig {
|
||||
name: "version-async".into(),
|
||||
},
|
||||
TIMEOUT,
|
||||
)
|
||||
.await
|
||||
.expect("new_async failed");
|
||||
assert_eq!(
|
||||
ctx.version_async().await.expect("version_async failed"),
|
||||
"nim-timer v0.1.0"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent_async_calls_are_independent() {
|
||||
let ctx = make_ctx("concurrent");
|
||||
let (r1, r2, r3) = tokio::join!(
|
||||
ctx.echo_async(EchoRequest {
|
||||
message: "one".into(),
|
||||
delay_ms: 80
|
||||
}),
|
||||
ctx.echo_async(EchoRequest {
|
||||
message: "two".into(),
|
||||
delay_ms: 40
|
||||
}),
|
||||
ctx.echo_async(EchoRequest {
|
||||
message: "three".into(),
|
||||
delay_ms: 20
|
||||
}),
|
||||
);
|
||||
assert_eq!(r1.unwrap().echoed, "one");
|
||||
assert_eq!(r2.unwrap().echoed, "two");
|
||||
assert_eq!(r3.unwrap().echoed, "three");
|
||||
}
|
||||
|
||||
// Chained async calls A->B->C preserve ordering and payload across hops.
|
||||
#[tokio::test]
|
||||
async fn triple_pipeline_async() {
|
||||
let ctx = make_ctx("pipeline");
|
||||
let a = ctx
|
||||
.echo_async(EchoRequest {
|
||||
message: "A".into(),
|
||||
delay_ms: 20,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let b = ctx
|
||||
.echo_async(EchoRequest {
|
||||
message: format!("{}->B", a.echoed),
|
||||
delay_ms: 10,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let c = ctx
|
||||
.echo_async(EchoRequest {
|
||||
message: format!("{}->C", b.echoed),
|
||||
delay_ms: 5,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(c.echoed, "A->B->C");
|
||||
assert_eq!(c.timer_name, "pipeline");
|
||||
}
|
||||
|
||||
// ── Typed events ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn typed_event_fires_after_echo() {
|
||||
let ctx = make_ctx("events");
|
||||
let (tx, rx) = mpsc::channel::<EchoEvent>();
|
||||
let handle = ctx.add_on_echo_fired_listener(move |evt: &EchoEvent| {
|
||||
let _ = tx.send(evt.clone());
|
||||
});
|
||||
assert_ne!(handle.id, 0, "listener registration returned zero id");
|
||||
|
||||
ctx.echo(EchoRequest {
|
||||
message: "event-msg".into(),
|
||||
delay_ms: 1,
|
||||
})
|
||||
.expect("echo failed");
|
||||
|
||||
let evt = rx
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("event never arrived");
|
||||
assert_eq!(evt.message, "event-msg");
|
||||
assert_eq!(evt.echo_count, 1);
|
||||
|
||||
assert!(ctx.remove_event_listener(handle));
|
||||
assert!(
|
||||
!ctx.remove_event_listener(handle),
|
||||
"double remove must report false"
|
||||
);
|
||||
}
|
||||
@ -143,10 +143,17 @@ suite "generateCLibHeader: ABI declarations and context API":
|
||||
check "timer_ctx_create(const EchoRequest* config, TimerCreateFn on_created, void* user_data)" in
|
||||
header
|
||||
|
||||
test "no blocking sync-call machinery or per-call timeout survives":
|
||||
check "nimffi_wait_result" notin header
|
||||
check "NimFfiCallState" notin header
|
||||
check "timeout_ms" notin header
|
||||
test "a blocking _sync wrapper is emitted alongside each async method":
|
||||
check "#include \"nim_ffi_sync.h\"" in header
|
||||
check "timer_ctx_version_sync(const TimerCtx* ctx, NimFfiStr* out, char* err_buf, size_t err_len, uint32_t timeout_ms)" in
|
||||
header
|
||||
# the wrapper drives the shared blocking helper and decodes into the out-param
|
||||
check "nimffi_sync_state_new()" in header
|
||||
check "nimffi_sync_wait(st, timeout_ms)" in header
|
||||
|
||||
test "the constructor gets a blocking _sync wrapper handing back an owned ctx":
|
||||
check "timer_ctx_create_sync(const EchoRequest* config, TimerCtx** out, char* err_buf, size_t err_len, uint32_t timeout_ms)" in
|
||||
header
|
||||
|
||||
test "an empty request envelope still encodes a (zero-length) map":
|
||||
check "_nimffi_empty" in header
|
||||
@ -278,7 +285,17 @@ suite "shared headers: prelude and cbor split":
|
||||
check "nimffi_enc_str" in cbor
|
||||
check "nimffi_decode_from_buf" in cbor
|
||||
|
||||
test "the sync header carries the blocking-call helper and pulls in the cbor header":
|
||||
let sync = generateCSyncHeader()
|
||||
check "#include \"nim_ffi_cbor.h\"" in sync
|
||||
check "NimFfiSyncState" in sync
|
||||
check "nimffi_sync_wait(" in sync
|
||||
# the platform split the example programs use: pthreads on POSIX, SRWLOCK on Win32
|
||||
check "pthread_cond_timedwait" in sync
|
||||
check "SleepConditionVariableSRW" in sync
|
||||
|
||||
test "each generated file is independently include-guarded":
|
||||
check "NIM_FFI_PRELUDE_H_INCLUDED" in generateCPreludeHeader()
|
||||
check "NIM_FFI_CBOR_HELPERS_H_INCLUDED" in generateCCborHeader()
|
||||
check "NIM_FFI_SYNC_HELPER_H_INCLUDED" in generateCSyncHeader()
|
||||
check "NIM_FFI_LIB_TIMER_H_INCLUDED" in generateCLibHeader(@[], @[], "timer")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user