feat: generated sync wrappers

This commit is contained in:
Gabriel Cruz 2026-07-09 15:06:42 -03:00
parent 3e57751e3a
commit a7cba1a4a3
22 changed files with 2115 additions and 179 deletions

View File

@ -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).

View File

@ -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
View File

@ -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

View File

@ -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")

View File

@ -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 */

View 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 */

View File

@ -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")

View File

@ -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

View File

@ -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);

View File

@ -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 */

View 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 */

View File

@ -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":

View File

@ -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)
)

View File

@ -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")

View 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 */

View File

@ -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
View 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
View 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
View 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
View 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
View 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"
);
}

View File

@ -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")