diff --git a/examples/timer/ios/.gitignore b/examples/timer/ios/.gitignore new file mode 100644 index 0000000..d488beb --- /dev/null +++ b/examples/timer/ios/.gitignore @@ -0,0 +1,4 @@ +/MyTimer.xcframework/ +/.build-slices/ +/.build/ +*.o diff --git a/examples/timer/ios/Package.swift b/examples/timer/ios/Package.swift new file mode 100644 index 0000000..d41a970 --- /dev/null +++ b/examples/timer/ios/Package.swift @@ -0,0 +1,22 @@ +// swift-tools-version:5.9 +import PackageDescription + +// SwiftPM package wrapping the timer library for iOS (and macOS, so the Swift +// wrapper is testable on the host with `swift test`). +// +// `MyTimer.xcframework` is produced by ./build-xcframework.sh and bundles the +// static library for ios-arm64 (device), ios-arm64-simulator, and macos-arm64, +// each with the C headers + module map. Run the build script before +// `swift build` / `swift test`. +let package = Package( + name: "MyTimer", + platforms: [.iOS(.v13), .macOS(.v12)], + products: [ + .library(name: "MyTimer", targets: ["MyTimer"]) + ], + targets: [ + .binaryTarget(name: "CMyTimer", path: "MyTimer.xcframework"), + .target(name: "MyTimer", dependencies: ["CMyTimer"]), + .testTarget(name: "MyTimerTests", dependencies: ["MyTimer"]), + ] +) diff --git a/examples/timer/ios/README.md b/examples/timer/ios/README.md new file mode 100644 index 0000000..97ea17f --- /dev/null +++ b/examples/timer/ios/README.md @@ -0,0 +1,55 @@ +# iOS example — Swift over the native C ABI + +A SwiftPM package that wraps the timer library's **native** (zero-serialization) +C ABI behind an idiomatic Swift class, `TimerNode`. The library is cross-compiled +to a static `.xcframework` and consumed from Swift; struct returns come back as +typed Swift values. + +```swift +let node = try TimerNode(name: "my-app") +print(try node.version()) // "nim-timer v0.1.0" +let r = try node.echo("hello", delayMs: 5) // EchoResult +print(r.echoed, r.timerName) +``` + +## Layout + +| Path | Description | +|------|-------------| +| `build-xcframework.sh` | Cross-compiles the Nim lib to `MyTimer.xcframework` (ios-arm64 device, ios-arm64 simulator, macos-arm64) and bundles the C headers + module map. | +| `cheaders/` | The native C header (`my_timer.h`), CBOR header, and `module.modulemap` (module `CMyTimer`). | +| `Sources/MyTimer/MyTimer.swift` | The Swift wrapper. Bridges the async FFI-thread callback to a synchronous Swift API with a semaphore; reads the typed `EchoResponse` struct out of the callback. | +| `Package.swift` | SwiftPM: a binary target (the xcframework) + the Swift wrapper + tests. | +| `Tests/` | Unit tests, runnable on the host via the macOS slice. | + +## Build & test + +```sh +cd examples/timer/ios +./build-xcframework.sh # builds the 3 slices into MyTimer.xcframework +swift test # runs on the host (macos-arm64 slice) +``` + +`build-xcframework.sh` assembles the `.xcframework` directly (no +`xcodebuild -create-xcframework`), so it works in headless / CI environments. +The **macos-arm64** slice exists only so the wrapper is testable with +`swift test`; real iOS deployment uses the **ios-arm64** (device) and +**ios-arm64-simulator** slices. + +## Use it in an iOS app + +1. Run `./build-xcframework.sh`. +2. Add this directory as a local Swift package (Xcode → *Add Package + Dependencies… → Add Local…*), or depend on it in your own `Package.swift`. +3. `import MyTimer` and use `TimerNode`. + +## Notes + +- This is the **native, same-process** path — the app links the library directly. + The CBOR ABI is for inter-process communication only (see [`../ipc`](../ipc)). +- Each call is dispatched on the library's background FFI thread; `TimerNode` + blocks on a semaphore until the result callback fires. A struct return (e.g. + `EchoResponse`) is read inside the callback — it is valid only for the + callback's lifetime — and copied into the returned Swift value. +- Regenerate `cheaders/my_timer.h` from the repo root with `nimble genbindings_c` + if the library's API changes. diff --git a/examples/timer/ios/Sources/MyTimer/MyTimer.swift b/examples/timer/ios/Sources/MyTimer/MyTimer.swift new file mode 100644 index 0000000..daadfdd --- /dev/null +++ b/examples/timer/ios/Sources/MyTimer/MyTimer.swift @@ -0,0 +1,126 @@ +// Idiomatic Swift wrapper over the timer library's native C ABI. +// +// Each call is dispatched on the library's background FFI thread and its result +// arrives on a callback; we bridge that to a synchronous Swift API with a +// semaphore. A struct return (EchoResponse) is read out of the typed C-POD +// inside the callback — it is valid only for the callback's lifetime. +import CMyTimer +import Foundation + +public enum TimerError: Error, CustomStringConvertible { + case failed(String) + public var description: String { + switch self { case let .failed(m): return m } + } +} + +public struct EchoResult: Equatable { + public let echoed: String + public let timerName: String +} + +public final class TimerNode { + private let ctx: UnsafeMutableRawPointer + + /// Creates the timer context (TimerConfig by value). + public init(name: String) throws { + let box = Box() + let ud = Unmanaged.passUnretained(box).toOpaque() + let cName = strdup(name) + defer { free(cName) } + var cfg = TimerConfig() + cfg.name = UnsafePointer(cName) + guard let c = my_timer_create(cfg, ackCallback, ud) else { + throw TimerError.failed("create returned null") + } + box.sem.wait() + guard box.ret == 0 else { throw TimerError.failed(box.text) } + ctx = c + } + + /// String-returning call: the raw bytes are the version string. + public func version() throws -> String { + let box = Box() + let ud = Unmanaged.passUnretained(box).toOpaque() + guard my_timer_version(ctx, stringCallback, ud) == 0 else { + throw TimerError.failed("version dispatch failed") + } + box.sem.wait() + guard box.ret == 0 else { throw TimerError.failed(box.text) } + return box.text + } + + /// Struct param in, typed struct (EchoResponse) out. + public func echo(_ message: String, delayMs: Int = 0) throws -> EchoResult { + let box = EchoBox() + let ud = Unmanaged.passUnretained(box).toOpaque() + let cMsg = strdup(message) + defer { free(cMsg) } + var req = EchoRequest() + req.message = UnsafePointer(cMsg) + req.delayMs = Int64(delayMs) + guard my_timer_echo(ctx, echoCallback, ud, req) == 0 else { + throw TimerError.failed("echo dispatch failed") + } + box.sem.wait() + guard box.ret == 0 else { throw TimerError.failed(box.text) } + return EchoResult(echoed: box.echoed, timerName: box.timerName) + } + + deinit { my_timer_destroy(ctx) } +} + +// MARK: - callback plumbing +// The library calls back on its FFI thread; we keep the Box alive on the caller +// stack (passUnretained) because the caller blocks on the semaphore until the +// callback fires. + +final class Box { + var ret: Int32 = -1 + var text = "" + let sem = DispatchSemaphore(value: 0) +} +final class EchoBox { + var ret: Int32 = -1 + var text = "" + var echoed = "" + var timerName = "" + let sem = DispatchSemaphore(value: 0) +} + +private func rawText(_ msg: UnsafePointer?, _ len: Int) -> String { + guard let m = msg, len > 0 else { return "" } + let bytes = UnsafeRawPointer(m).assumingMemoryBound(to: UInt8.self) + return String(decoding: UnsafeBufferPointer(start: bytes, count: len), as: UTF8.self) +} + +private func ackCallback(_ ret: Int32, _ msg: UnsafePointer?, + _ len: Int, _ ud: UnsafeMutableRawPointer?) { + let box = Unmanaged.fromOpaque(ud!).takeUnretainedValue() + box.ret = ret + if ret != 0 { box.text = rawText(msg, len) } + box.sem.signal() +} + +private func stringCallback(_ ret: Int32, _ msg: UnsafePointer?, + _ len: Int, _ ud: UnsafeMutableRawPointer?) { + let box = Unmanaged.fromOpaque(ud!).takeUnretainedValue() + box.ret = ret + box.text = rawText(msg, len) + box.sem.signal() +} + +private func echoCallback(_ ret: Int32, _ msg: UnsafePointer?, + _ len: Int, _ ud: UnsafeMutableRawPointer?) { + let box = Unmanaged.fromOpaque(ud!).takeUnretainedValue() + box.ret = ret + if ret == 0, let m = msg { + // Native ABI: msg is a const EchoResponse* (typed struct return). + let resp = UnsafeRawPointer(m).assumingMemoryBound(to: EchoResponse.self) + box.echoed = resp.pointee.echoed.map { String(cString: $0) } ?? "" + box.timerName = resp.pointee.timerName.map { String(cString: $0) } ?? "" + } else { + box.text = rawText(msg, len) + } + box.sem.signal() +} diff --git a/examples/timer/ios/Tests/MyTimerTests/MyTimerTests.swift b/examples/timer/ios/Tests/MyTimerTests/MyTimerTests.swift new file mode 100644 index 0000000..e2f4f4f --- /dev/null +++ b/examples/timer/ios/Tests/MyTimerTests/MyTimerTests.swift @@ -0,0 +1,22 @@ +import XCTest +@testable import MyTimer + +final class MyTimerTests: XCTestCase { + func testCreateVersionEcho() throws { + let node = try TimerNode(name: "ios-demo") + + XCTAssertEqual(try node.version(), "nim-timer v0.1.0") + + let r = try node.echo("hello from Swift", delayMs: 2) + XCTAssertEqual(r.echoed, "hello from Swift") + XCTAssertEqual(r.timerName, "ios-demo") // proves the lib's own state round-tripped + } + + func testManyEchoes() throws { + let node = try TimerNode(name: "loop") + for i in 0..<200 { + let r = try node.echo("m\(i)") + XCTAssertEqual(r.echoed, "m\(i)") + } + } +} diff --git a/examples/timer/ios/build-xcframework.sh b/examples/timer/ios/build-xcframework.sh new file mode 100755 index 0000000..083e56b --- /dev/null +++ b/examples/timer/ios/build-xcframework.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Build MyTimer.xcframework from the Nim timer library: +# - ios-arm64 (device) +# - ios-arm64-simulator (Apple-silicon simulator) +# - macos-arm64 (so the Swift wrapper is testable with `swift test`) +# +# Each slice is a static library cross-compiled by Nim with the matching SDK, +# then bundled with the C headers + module map. Requires Xcode + Nim. +# +# ./build-xcframework.sh +set -euo pipefail + +HERE="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$HERE/../../.." && pwd)" +NIM_SRC="$REPO_ROOT/examples/timer/timer.nim" +STAGE="$HERE/.build-slices" +OUT="$HERE/MyTimer.xcframework" + +CLANG="$(xcrun -f clang)" +COMMON_NIM=(--mm:orc -d:release -d:chronicles_log_level=WARN --threads:on + --os:macosx --cpu:arm64 --app:staticlib --noMain + --nimMainPrefix:libmy_timer --cc:clang + "--clang.exe:$CLANG" "--clang.linkerexe:$CLANG") + +# build_slice +build_slice() { + local name="$1" sdk="$2" minflag="$3" + local sysroot; sysroot="$(xcrun --sdk "$sdk" --show-sdk-path)" + local dir="$STAGE/$name" + mkdir -p "$dir" + echo ">> building slice: $name ($sdk)" + ( cd "$REPO_ROOT" && nim c "${COMMON_NIM[@]}" \ + --nimcache:"$dir/nimcache" \ + --passC:"-isysroot $sysroot -arch arm64 $minflag" \ + --passL:"-isysroot $sysroot -arch arm64 $minflag" \ + -o:"$dir/libmy_timer.a" "$NIM_SRC" >/dev/null ) +} + +rm -rf "$STAGE" "$OUT" +build_slice device iphoneos "-miphoneos-version-min=13.0" +build_slice simulator iphonesimulator "-mios-simulator-version-min=13.0" +build_slice macos macosx "-mmacosx-version-min=12.0" + +echo ">> assembling $OUT" +# Assemble the .xcframework by hand (a directory + Info.plist) rather than via +# `xcodebuild -create-xcframework` — same on-disk format, but no dependency on a +# working Simulator toolchain, so it builds in headless / CI environments too. +add_slice() { # + local dir="$OUT/$2" + mkdir -p "$dir/Headers" + cp "$STAGE/$1/libmy_timer.a" "$dir/libmy_timer.a" + cp "$HERE/cheaders/"* "$dir/Headers/" +} +mkdir -p "$OUT" +add_slice device ios-arm64 +add_slice simulator ios-arm64-simulator +add_slice macos macos-arm64 + +cat > "$OUT/Info.plist" <<'PLIST' + + + + + AvailableLibraries + + + LibraryIdentifierios-arm64 + LibraryPathlibmy_timer.a + HeadersPathHeaders + SupportedArchitecturesarm64 + SupportedPlatformios + + + LibraryIdentifierios-arm64-simulator + LibraryPathlibmy_timer.a + HeadersPathHeaders + SupportedArchitecturesarm64 + SupportedPlatformios + SupportedPlatformVariantsimulator + + + LibraryIdentifiermacos-arm64 + LibraryPathlibmy_timer.a + HeadersPathHeaders + SupportedArchitecturesarm64 + SupportedPlatformmacos + + + CFBundlePackageTypeXFWK + XCFrameworkFormatVersion1.0 + + +PLIST + +echo ">> done. Slices:" +ls "$OUT" diff --git a/examples/timer/ios/cheaders/module.modulemap b/examples/timer/ios/cheaders/module.modulemap new file mode 100644 index 0000000..038bc86 --- /dev/null +++ b/examples/timer/ios/cheaders/module.modulemap @@ -0,0 +1,5 @@ +module CMyTimer { + header "my_timer.h" + header "my_timer_cbor.h" + export * +} diff --git a/examples/timer/ios/cheaders/my_timer.h b/examples/timer/ios/cheaders/my_timer.h new file mode 100644 index 0000000..cd50e82 --- /dev/null +++ b/examples/timer/ios/cheaders/my_timer.h @@ -0,0 +1,121 @@ +// 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: +// - string-returning procs: (msg, len) is the raw string bytes (not +// NUL-terminated; use len). +// - struct-returning procs: msg is a pointer to the returned C struct — cast +// it to `const *` (len is sizeof). It is valid ONLY for the duration +// of the callback; copy out anything you need before returning. The library +// deep-frees it right after the callback (you free nothing). +// On RET_ERR, (msg, len) is the raw error text. A `_cbor` variant of each +// proc also exists for generic/cross-language callers that prefer CBOR. +#ifndef NIM_FFI_GEN_MY_TIMER_H +#define NIM_FFI_GEN_MY_TIMER_H + +#include +#include + +#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 */ \ No newline at end of file diff --git a/examples/timer/ios/cheaders/my_timer_cbor.h b/examples/timer/ios/cheaders/my_timer_cbor.h new file mode 100644 index 0000000..04e8c93 --- /dev/null +++ b/examples/timer/ios/cheaders/my_timer_cbor.h @@ -0,0 +1,178 @@ +// Generated by nim-ffi C codegen. Do not edit by hand. +// +// CBOR ABI (`_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 +// `_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 `` +// ABI in the companion .h header. +#ifndef NIM_FFI_GEN_MY_TIMER_CBOR_H +#define NIM_FFI_GEN_MY_TIMER_CBOR_H + +#include +#include +#include +#include + +#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 */ \ No newline at end of file