docs(examples): add iOS (Swift) example over the native C ABI

A SwiftPM package wrapping the timer library's native ABI behind an idiomatic
`TimerNode` Swift class. `build-xcframework.sh` cross-compiles the Nim library
to a static MyTimer.xcframework with three slices — ios-arm64 (device),
ios-arm64-simulator, and macos-arm64 — assembling the .xcframework by hand so it
works without a functioning Simulator toolchain (CI-friendly).

The wrapper bridges the async FFI-thread callback to a synchronous Swift API
with a semaphore and reads the typed EchoResponse struct out of the callback.
The macos-arm64 slice makes the wrapper testable on the host: `swift test`
passes against it. Device/simulator slices are the real iOS deployment artifacts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ivan FB 2026-05-31 13:29:49 +02:00
parent c000a8467d
commit 014e1618ba
No known key found for this signature in database
GPG Key ID: DF0C67A04C543270
9 changed files with 629 additions and 0 deletions

4
examples/timer/ios/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/MyTimer.xcframework/
/.build-slices/
/.build/
*.o

View File

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

View File

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

View File

@ -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<CChar>?, _ 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<CChar>?,
_ len: Int, _ ud: UnsafeMutableRawPointer?) {
let box = Unmanaged<Box>.fromOpaque(ud!).takeUnretainedValue()
box.ret = ret
if ret != 0 { box.text = rawText(msg, len) }
box.sem.signal()
}
private func stringCallback(_ ret: Int32, _ msg: UnsafePointer<CChar>?,
_ len: Int, _ ud: UnsafeMutableRawPointer?) {
let box = Unmanaged<Box>.fromOpaque(ud!).takeUnretainedValue()
box.ret = ret
box.text = rawText(msg, len)
box.sem.signal()
}
private func echoCallback(_ ret: Int32, _ msg: UnsafePointer<CChar>?,
_ len: Int, _ ud: UnsafeMutableRawPointer?) {
let box = Unmanaged<EchoBox>.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()
}

View File

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

View File

@ -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 <name> <sdk> <min-flag>
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() { # <stage-name> <library-identifier>
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'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>LibraryIdentifier</key><string>ios-arm64</string>
<key>LibraryPath</key><string>libmy_timer.a</string>
<key>HeadersPath</key><string>Headers</string>
<key>SupportedArchitectures</key><array><string>arm64</string></array>
<key>SupportedPlatform</key><string>ios</string>
</dict>
<dict>
<key>LibraryIdentifier</key><string>ios-arm64-simulator</string>
<key>LibraryPath</key><string>libmy_timer.a</string>
<key>HeadersPath</key><string>Headers</string>
<key>SupportedArchitectures</key><array><string>arm64</string></array>
<key>SupportedPlatform</key><string>ios</string>
<key>SupportedPlatformVariant</key><string>simulator</string>
</dict>
<dict>
<key>LibraryIdentifier</key><string>macos-arm64</string>
<key>LibraryPath</key><string>libmy_timer.a</string>
<key>HeadersPath</key><string>Headers</string>
<key>SupportedArchitectures</key><array><string>arm64</string></array>
<key>SupportedPlatform</key><string>macos</string>
</dict>
</array>
<key>CFBundlePackageType</key><string>XFWK</string>
<key>XCFrameworkFormatVersion</key><string>1.0</string>
</dict>
</plist>
PLIST
echo ">> done. Slices:"
ls "$OUT"

View File

@ -0,0 +1,5 @@
module CMyTimer {
header "my_timer.h"
header "my_timer_cbor.h"
export *
}

View File

@ -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 <Type>*` (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 `<name>_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 <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 */

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