mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-06-21 00:40:16 +00:00
docs(examples): add native (same-process) C example
The C codegen already emitted `my_timer.h` / `my_timer_cbor.h`, but the example had no runnable driver. Add `example.c` exercising the native ABI end-to-end (ctor with a struct param, string-returning version, struct-param echo, and a deeply nested ComplexRequest), plus a Makefile that builds the Nim dylib from the repo root — where the vendored Nimble deps resolve — and links the driver. Native is the same-process path; the companion CBOR headers are for crossing a process/machine boundary (see the forthcoming ipc example). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ad493d6f9d
commit
c5c7c373b4
3
examples/timer/c_bindings/.gitignore
vendored
Normal file
3
examples/timer/c_bindings/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/example
|
||||
/libmy_timer.dylib
|
||||
/libmy_timer.so
|
||||
40
examples/timer/c_bindings/Makefile
Normal file
40
examples/timer/c_bindings/Makefile
Normal file
@ -0,0 +1,40 @@
|
||||
# Build the native (same-process) C example for the timer library.
|
||||
#
|
||||
# make run # build the Nim dylib + the C driver, then run it
|
||||
# make clean
|
||||
#
|
||||
# The Nim library is compiled from the repository root so its vendored Nimble
|
||||
# dependencies resolve, exactly like the CMake-based C++ example does.
|
||||
|
||||
REPO_ROOT := $(abspath ../../..)
|
||||
NIM_SRC := $(REPO_ROOT)/examples/timer/timer.nim
|
||||
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S),Darwin)
|
||||
LIBNAME := libmy_timer.dylib
|
||||
RPATH := -Wl,-rpath,.
|
||||
else
|
||||
LIBNAME := libmy_timer.so
|
||||
RPATH := -Wl,-rpath,'$$ORIGIN'
|
||||
endif
|
||||
|
||||
CC ?= cc
|
||||
CFLAGS ?= -std=c11 -Wall -Wextra -O2
|
||||
NIMFLAGS := --mm:orc -d:chronicles_log_level=WARN --app:lib --noMain \
|
||||
--nimMainPrefix:libmy_timer
|
||||
|
||||
.PHONY: all run clean
|
||||
|
||||
all: example
|
||||
|
||||
$(LIBNAME):
|
||||
cd $(REPO_ROOT) && nim c $(NIMFLAGS) -o:$(CURDIR)/$(LIBNAME) $(NIM_SRC)
|
||||
|
||||
example: example.c my_timer.h $(LIBNAME)
|
||||
$(CC) $(CFLAGS) -I. example.c -L. -lmy_timer $(RPATH) -o example
|
||||
|
||||
run: example
|
||||
./example
|
||||
|
||||
clean:
|
||||
rm -f example $(LIBNAME)
|
||||
39
examples/timer/c_bindings/README.md
Normal file
39
examples/timer/c_bindings/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# C bindings — native (same-process) example
|
||||
|
||||
Generated C headers for the timer library plus a small driver that links the
|
||||
library directly and calls the **native** (zero-serialization) ABI.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `my_timer.h` | Native ABI: each `{.ffi.}` type is a plain C `struct`, passed by value to `int <name>(ctx, cb, ud, <args…>)`. Results arrive on the callback. Best for same-process callers — no serialization. |
|
||||
| `my_timer_cbor.h` | CBOR ABI (`<name>_cbor`): request/response as CBOR bytes. Use this when the call crosses a process or machine boundary. See [`../ipc`](../ipc). |
|
||||
| `example.c` | Native same-process driver: create → version → echo → complex → destroy. |
|
||||
| `Makefile` | Builds the Nim dylib (from the repo root) and the driver. |
|
||||
|
||||
The headers are regenerated by `nimble genbindings_c` (run from the repo root)
|
||||
and overwritten each time — don't edit them by hand.
|
||||
|
||||
## Build & run
|
||||
|
||||
```sh
|
||||
cd examples/timer/c_bindings
|
||||
make run
|
||||
```
|
||||
|
||||
This compiles `libmy_timer.{dylib,so}` and runs `./example`, which prints the
|
||||
library version and the round-tripped echo/complex responses. Every call is
|
||||
dispatched on the library's FFI thread, so the driver blocks on a condvar-backed
|
||||
callback for each result.
|
||||
|
||||
## Native vs CBOR
|
||||
|
||||
The native path passes `{.ffi.}` structs as flat C-POD values (`const char*` for
|
||||
strings, `{ T* ptr; size_t len }` for sequences, `{ int present; T }` for
|
||||
options). Arguments are **deep-copied** across the FFI-thread boundary, so the C
|
||||
caller's buffers can be freed immediately after the call returns. String returns
|
||||
come back raw; struct returns are CBOR-encoded inside the callback payload.
|
||||
|
||||
For the cross-process / cross-machine path, the same library is reached over a
|
||||
socket using the CBOR ABI — see [`../ipc`](../ipc).
|
||||
118
examples/timer/c_bindings/example.c
Normal file
118
examples/timer/c_bindings/example.c
Normal file
@ -0,0 +1,118 @@
|
||||
// Native (zero-serialization, same-process) C example for the timer library.
|
||||
//
|
||||
// This is the in-process path: the C host links libmy_timer directly and calls
|
||||
// the native `<name>` entry points, passing `{.ffi.}` types as plain C structs
|
||||
// by value. No CBOR — arguments are deep-copied across the FFI thread boundary
|
||||
// as flat C-POD graphs. Each call delivers its result to a callback that we
|
||||
// block on with a condvar (every call is dispatched on the library's FFI
|
||||
// thread, so the result is not ready until the callback fires).
|
||||
//
|
||||
// For the cross-process / cross-machine path (CBOR over a socket), see
|
||||
// ../ipc/.
|
||||
#include "my_timer.h"
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
// --- one-shot blocking response capture -------------------------------------
|
||||
typedef struct {
|
||||
int ret;
|
||||
char buf[2048];
|
||||
size_t len;
|
||||
int done;
|
||||
pthread_mutex_t mu;
|
||||
pthread_cond_t cv;
|
||||
} Resp;
|
||||
|
||||
static void resp_init(Resp *r) {
|
||||
memset(r, 0, sizeof(*r));
|
||||
pthread_mutex_init(&r->mu, NULL);
|
||||
pthread_cond_init(&r->cv, NULL);
|
||||
}
|
||||
|
||||
// Native ABI: on RET_OK (msg, len) is the raw return value (for a string-
|
||||
// returning proc, the bytes; for a struct-returning proc, its CBOR encoding);
|
||||
// on RET_ERR it is the raw error text. We copy it so it outlives the callback.
|
||||
static void on_result(int ret, const char *msg, size_t len, void *ud) {
|
||||
Resp *r = (Resp *)ud;
|
||||
pthread_mutex_lock(&r->mu);
|
||||
r->ret = ret;
|
||||
size_t n = len < sizeof(r->buf) - 1 ? len : sizeof(r->buf) - 1;
|
||||
if (msg && n) memcpy(r->buf, msg, n);
|
||||
r->buf[n] = '\0';
|
||||
r->len = len;
|
||||
r->done = 1;
|
||||
pthread_cond_signal(&r->cv);
|
||||
pthread_mutex_unlock(&r->mu);
|
||||
}
|
||||
|
||||
static void resp_wait(Resp *r) {
|
||||
pthread_mutex_lock(&r->mu);
|
||||
while (!r->done) pthread_cond_wait(&r->cv, &r->mu);
|
||||
pthread_mutex_unlock(&r->mu);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
// 1) Construct the library context. The ctor takes a TimerConfig by value;
|
||||
// its `name: string` field is a plain `const char*` on the C side.
|
||||
Resp cr;
|
||||
resp_init(&cr);
|
||||
TimerConfig cfg = {.name = "c-native-demo"};
|
||||
void *ctx = my_timer_create(cfg, on_result, &cr);
|
||||
resp_wait(&cr);
|
||||
if (!ctx || cr.ret != RET_OK) {
|
||||
fprintf(stderr, "create failed (ret=%d): %s\n", cr.ret, cr.buf);
|
||||
return 1;
|
||||
}
|
||||
printf("created timer ctx=%p\n", ctx);
|
||||
|
||||
// 2) Synchronous-shaped call: version returns a plain string, delivered raw.
|
||||
Resp vr;
|
||||
resp_init(&vr);
|
||||
if (my_timer_version(ctx, on_result, &vr) == RET_OK) {
|
||||
resp_wait(&vr);
|
||||
printf("version: %s\n", vr.buf);
|
||||
}
|
||||
|
||||
// 3) Struct param by value: EchoRequest { const char* message; int64 delayMs }.
|
||||
// The library sleeps delayMs on its chronos loop, then echoes the message.
|
||||
Resp er;
|
||||
resp_init(&er);
|
||||
EchoRequest req = {.message = "hello from C", .delayMs = 5};
|
||||
if (my_timer_echo(ctx, on_result, &er, req) == RET_OK) {
|
||||
resp_wait(&er);
|
||||
// EchoResponse is a struct return, delivered as CBOR on the native path;
|
||||
// the echoed message appears verbatim inside the payload bytes.
|
||||
printf("echo ret=%d (%zu-byte response, contains \"%s\")\n", er.ret, er.len,
|
||||
strstr(er.buf, "hello from C") ? "hello from C" : "<not found>");
|
||||
}
|
||||
|
||||
// 4) Deeply nested struct: seq<struct>, seq<string>, Option<string>, Option<int>.
|
||||
// Demonstrates that the whole graph is deep-copied across the boundary.
|
||||
Resp xr;
|
||||
resp_init(&xr);
|
||||
EchoRequest msgs[2] = {
|
||||
{.message = "one", .delayMs = 0},
|
||||
{.message = "two", .delayMs = 0},
|
||||
};
|
||||
const char *tags[1] = {"demo"};
|
||||
ComplexRequest creq = {
|
||||
.messages = msgs,
|
||||
.messages_len = 2,
|
||||
.tags = tags,
|
||||
.tags_len = 1,
|
||||
.note_present = 1,
|
||||
.note = "a note",
|
||||
.retries_present = 1,
|
||||
.retries = 3,
|
||||
};
|
||||
if (my_timer_complex(ctx, on_result, &xr, creq) == RET_OK) {
|
||||
resp_wait(&xr);
|
||||
printf("complex ret=%d (%zu-byte response)\n", xr.ret, xr.len);
|
||||
}
|
||||
|
||||
// 5) Tear down the context (joins the FFI thread).
|
||||
my_timer_destroy(ctx);
|
||||
printf("destroyed; done.\n");
|
||||
return 0;
|
||||
}
|
||||
116
examples/timer/c_bindings/my_timer.h
Normal file
116
examples/timer/c_bindings/my_timer.h
Normal file
@ -0,0 +1,116 @@
|
||||
// Generated by nim-ffi C codegen. Do not edit by hand.
|
||||
//
|
||||
// Native (zero-serialization) C ABI. Each call delivers its result to the
|
||||
// callback: on RET_OK, (msg, len) is the raw return value (for string-returning
|
||||
// procs, the string bytes — not NUL-terminated; use len); on RET_ERR, (msg, len)
|
||||
// is the raw error text. A `<name>_cbor` variant of each proc also exists for
|
||||
// generic/cross-language callers that prefer a CBOR request/response.
|
||||
#ifndef NIM_FFI_GEN_MY_TIMER_H
|
||||
#define NIM_FFI_GEN_MY_TIMER_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef NIM_FFI_RET_CODES
|
||||
#define NIM_FFI_RET_CODES
|
||||
#define RET_OK 0
|
||||
#define RET_ERR 1
|
||||
#define RET_MISSING_CALLBACK 2
|
||||
#endif
|
||||
|
||||
#ifndef NIM_FFI_CALLBACK_T
|
||||
#define NIM_FFI_CALLBACK_T
|
||||
typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData);
|
||||
#endif
|
||||
|
||||
|
||||
// --- {.ffi.}-annotated types, exposed as C structs ----------
|
||||
typedef struct {
|
||||
const char* name;
|
||||
} TimerConfig;
|
||||
|
||||
typedef struct {
|
||||
const char* message;
|
||||
int64_t delayMs;
|
||||
} EchoRequest;
|
||||
|
||||
typedef struct {
|
||||
const char* echoed;
|
||||
const char* timerName;
|
||||
} EchoResponse;
|
||||
|
||||
typedef struct {
|
||||
EchoRequest *messages;
|
||||
size_t messages_len;
|
||||
const char* *tags;
|
||||
size_t tags_len;
|
||||
int note_present;
|
||||
const char* note;
|
||||
int retries_present;
|
||||
int64_t retries;
|
||||
} ComplexRequest;
|
||||
|
||||
typedef struct {
|
||||
const char* summary;
|
||||
int64_t itemCount;
|
||||
int hasNote;
|
||||
} ComplexResponse;
|
||||
|
||||
typedef struct {
|
||||
const char* message;
|
||||
int64_t echoCount;
|
||||
} EchoEvent;
|
||||
|
||||
typedef struct {
|
||||
const char* name;
|
||||
const char* *payload;
|
||||
size_t payload_len;
|
||||
int64_t priority;
|
||||
} JobSpec;
|
||||
|
||||
typedef struct {
|
||||
int64_t maxAttempts;
|
||||
int64_t backoffMs;
|
||||
const char* *retryOn;
|
||||
size_t retryOn_len;
|
||||
} RetryPolicy;
|
||||
|
||||
typedef struct {
|
||||
int64_t startAtMs;
|
||||
int64_t intervalMs;
|
||||
int jitter_present;
|
||||
int64_t jitter;
|
||||
} ScheduleConfig;
|
||||
|
||||
typedef struct {
|
||||
const char* jobId;
|
||||
int64_t willRunCount;
|
||||
int64_t firstRunAtMs;
|
||||
int64_t effectiveBackoffMs;
|
||||
} ScheduleResult;
|
||||
|
||||
|
||||
void *my_timer_create(TimerConfig config, FFICallBack callback, void *userData);
|
||||
|
||||
int my_timer_echo(void *ctx, FFICallBack callback, void *userData, EchoRequest req);
|
||||
|
||||
int my_timer_version(void *ctx, FFICallBack callback, void *userData);
|
||||
|
||||
int my_timer_complex(void *ctx, FFICallBack callback, void *userData, ComplexRequest req);
|
||||
|
||||
int my_timer_schedule(void *ctx, FFICallBack callback, void *userData, JobSpec job, RetryPolicy retry, ScheduleConfig schedule);
|
||||
|
||||
int my_timer_destroy(void *ctx);
|
||||
|
||||
uint64_t my_timer_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData);
|
||||
int my_timer_remove_event_listener(void *ctx, uint64_t listenerId);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif /* NIM_FFI_GEN_MY_TIMER_H */
|
||||
178
examples/timer/c_bindings/my_timer_cbor.h
Normal file
178
examples/timer/c_bindings/my_timer_cbor.h
Normal file
@ -0,0 +1,178 @@
|
||||
// Generated by nim-ffi C codegen. Do not edit by hand.
|
||||
//
|
||||
// CBOR ABI (`<name>_cbor`). Use this for callers that cross a process or machine
|
||||
// boundary (the request has to be serialized anyway) or any generic / cross-
|
||||
// language caller. Build the request with the FfiCbor helpers below — a CBOR map
|
||||
// whose keys are the Nim parameter names (listed per proc) — call the matching
|
||||
// `<name>_cbor`, and decode the RET_OK response (a CBOR-encoded value; for
|
||||
// string-returning procs a CBOR text string) with ffi_decode_text. RET_ERR
|
||||
// delivers raw error text. For same-process callers, prefer the native `<name>`
|
||||
// ABI in the companion <lib>.h header.
|
||||
#ifndef NIM_FFI_GEN_MY_TIMER_CBOR_H
|
||||
#define NIM_FFI_GEN_MY_TIMER_CBOR_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef NIM_FFI_RET_CODES
|
||||
#define NIM_FFI_RET_CODES
|
||||
#define RET_OK 0
|
||||
#define RET_ERR 1
|
||||
#define RET_MISSING_CALLBACK 2
|
||||
#endif
|
||||
|
||||
#ifndef NIM_FFI_CALLBACK_T
|
||||
#define NIM_FFI_CALLBACK_T
|
||||
typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData);
|
||||
#endif
|
||||
|
||||
#ifndef NIM_FFI_CBOR_HELPERS
|
||||
#define NIM_FFI_CBOR_HELPERS
|
||||
// --- minimal growable CBOR request encoder --------------------------------
|
||||
typedef struct {
|
||||
uint8_t *buf;
|
||||
size_t cap;
|
||||
size_t len;
|
||||
} FfiCbor;
|
||||
|
||||
static inline FfiCbor ffi_cbor_new(void) {
|
||||
FfiCbor c;
|
||||
c.cap = 256;
|
||||
c.len = 0;
|
||||
c.buf = (uint8_t *)malloc(c.cap);
|
||||
return c;
|
||||
}
|
||||
static inline void ffi_cbor_free(FfiCbor *c) {
|
||||
free(c->buf);
|
||||
c->buf = NULL;
|
||||
}
|
||||
static inline void ffi_cbor_put(FfiCbor *c, uint8_t b) {
|
||||
if (c->len >= c->cap) {
|
||||
c->cap *= 2;
|
||||
c->buf = (uint8_t *)realloc(c->buf, c->cap);
|
||||
}
|
||||
c->buf[c->len++] = b;
|
||||
}
|
||||
static inline void ffi_cbor_head(FfiCbor *c, uint8_t major, uint64_t arg) {
|
||||
uint8_t mt = (uint8_t)(major << 5);
|
||||
if (arg < 24) {
|
||||
ffi_cbor_put(c, mt | (uint8_t)arg);
|
||||
} else if (arg <= 0xff) {
|
||||
ffi_cbor_put(c, mt | 24);
|
||||
ffi_cbor_put(c, (uint8_t)arg);
|
||||
} else if (arg <= 0xffff) {
|
||||
ffi_cbor_put(c, mt | 25);
|
||||
ffi_cbor_put(c, (uint8_t)(arg >> 8));
|
||||
ffi_cbor_put(c, (uint8_t)arg);
|
||||
} else if (arg <= 0xffffffffULL) {
|
||||
ffi_cbor_put(c, mt | 26);
|
||||
ffi_cbor_put(c, (uint8_t)(arg >> 24));
|
||||
ffi_cbor_put(c, (uint8_t)(arg >> 16));
|
||||
ffi_cbor_put(c, (uint8_t)(arg >> 8));
|
||||
ffi_cbor_put(c, (uint8_t)arg);
|
||||
} else {
|
||||
ffi_cbor_put(c, mt | 27);
|
||||
for (int s = 56; s >= 0; s -= 8) ffi_cbor_put(c, (uint8_t)(arg >> s));
|
||||
}
|
||||
}
|
||||
static inline void ffi_cbor_map(FfiCbor *c, size_t n) { ffi_cbor_head(c, 5, n); }
|
||||
static inline void ffi_cbor_text(FfiCbor *c, const char *s) {
|
||||
size_t n = s ? strlen(s) : 0;
|
||||
ffi_cbor_head(c, 3, n);
|
||||
for (size_t i = 0; i < n; i++) ffi_cbor_put(c, (uint8_t)s[i]);
|
||||
}
|
||||
static inline void ffi_cbor_kv_text(FfiCbor *c, const char *k, const char *v) {
|
||||
ffi_cbor_text(c, k);
|
||||
ffi_cbor_text(c, v);
|
||||
}
|
||||
static inline void ffi_cbor_kv_uint(FfiCbor *c, const char *k, uint64_t v) {
|
||||
ffi_cbor_text(c, k);
|
||||
ffi_cbor_head(c, 0, v);
|
||||
}
|
||||
static inline void ffi_cbor_kv_int(FfiCbor *c, const char *k, int64_t v) {
|
||||
ffi_cbor_text(c, k);
|
||||
if (v >= 0)
|
||||
ffi_cbor_head(c, 0, (uint64_t)v);
|
||||
else
|
||||
ffi_cbor_head(c, 1, (uint64_t)(-(v + 1)));
|
||||
}
|
||||
|
||||
// --- response decoding -----------------------------------------------------
|
||||
// Zero-copy view of a top-level CBOR text string (the RET_OK payload). Sets
|
||||
// *out/*outLen to point INTO `data` (no allocation; valid only while `data` is)
|
||||
// and returns 1; returns 0 for a non-text-string payload.
|
||||
static inline int ffi_text_view(const uint8_t *data, size_t len,
|
||||
const uint8_t **out, size_t *outLen) {
|
||||
if (len < 1 || (data[0] >> 5) != 3) return 0;
|
||||
uint8_t info = data[0] & 0x1f;
|
||||
size_t p = 1;
|
||||
uint64_t slen = 0;
|
||||
if (info < 24) {
|
||||
slen = info;
|
||||
} else if (info == 24) {
|
||||
if (len < p + 1) return 0;
|
||||
slen = data[p++];
|
||||
} else if (info == 25) {
|
||||
if (len < p + 2) return 0;
|
||||
slen = ((uint64_t)data[p] << 8) | data[p + 1];
|
||||
p += 2;
|
||||
} else if (info == 26) {
|
||||
if (len < p + 4) return 0;
|
||||
slen = ((uint64_t)data[p] << 24) | ((uint64_t)data[p + 1] << 16) |
|
||||
((uint64_t)data[p + 2] << 8) | data[p + 3];
|
||||
p += 4;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
if (len < p + slen) return 0;
|
||||
*out = data + p;
|
||||
*outLen = (size_t)slen;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Owning variant: malloc a NUL-terminated copy. NULL for a non-text payload.
|
||||
// Caller frees.
|
||||
static inline char *ffi_decode_text(const uint8_t *data, size_t len) {
|
||||
const uint8_t *view;
|
||||
size_t slen;
|
||||
if (!ffi_text_view(data, len, &view, &slen)) return NULL;
|
||||
char *out = (char *)malloc(slen + 1);
|
||||
if (!out) return NULL;
|
||||
memcpy(out, view, slen);
|
||||
out[slen] = '\0';
|
||||
return out;
|
||||
}
|
||||
#endif // NIM_FFI_CBOR_HELPERS
|
||||
|
||||
|
||||
// request map keys: {"config": TimerConfig}
|
||||
void *my_timer_create_cbor(const uint8_t *reqCbor, size_t reqCborLen, FFICallBack callback, void *userData);
|
||||
|
||||
// request map keys: {"req": EchoRequest}
|
||||
int my_timer_echo_cbor(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen);
|
||||
|
||||
// request: empty CBOR map (0xA0)
|
||||
int my_timer_version_cbor(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen);
|
||||
|
||||
// request map keys: {"req": ComplexRequest}
|
||||
int my_timer_complex_cbor(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen);
|
||||
|
||||
// request map keys: {"job": JobSpec, "retry": RetryPolicy, "schedule": ScheduleConfig}
|
||||
int my_timer_schedule_cbor(void *ctx, FFICallBack callback, void *userData, const uint8_t *reqCbor, size_t reqCborLen);
|
||||
|
||||
int my_timer_destroy(void *ctx);
|
||||
|
||||
uint64_t my_timer_add_event_listener(void *ctx, const char *eventName, FFICallBack callback, void *userData);
|
||||
int my_timer_remove_event_listener(void *ctx, uint64_t listenerId);
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
#endif
|
||||
|
||||
#endif /* NIM_FFI_GEN_MY_TIMER_CBOR_H */
|
||||
Loading…
x
Reference in New Issue
Block a user