nim-ffi/examples/nim_timer/nim_timer.nim

84 lines
3.2 KiB
Nim
Raw Permalink Normal View History

import ffi, chronos, options
type Maybe[T] = Option[T]
declareLibrary("nimtimer")
# The library's main state type. The FFI context owns one instance.
type NimTimer = object
name: string # set at creation time, read back in each response
2026-05-03 16:23:00 +02:00
type TimerConfig {.ffi.} = object
name: string
type EchoRequest {.ffi.} = object
message: string
delayMs: int # how long chronos sleeps before replying
type EchoResponse {.ffi.} = object
echoed: string
timerName: string # proves that the timer's own state is accessible
type ComplexRequest {.ffi.} = object
messages: seq[EchoRequest]
tags: seq[string]
note: Option[string]
retries: Maybe[int]
type ComplexResponse {.ffi.} = object
summary: string
itemCount: int
hasNote: bool
# --- Constructor -----------------------------------------------------------
# Called once from Rust. Creates the FFIContext + NimTimer.
# Uses chronos (await sleepAsync) so the body is async.
2026-05-03 16:36:37 +02:00
proc nimtimerCreate*(
config: TimerConfig
): Future[Result[NimTimer, string]] {.ffiCtor.} =
await sleepAsync(1.milliseconds) # proves chronos is live on the FFI thread
return ok(NimTimer(name: config.name))
# --- Async method ----------------------------------------------------------
# Waits `delayMs` milliseconds (non-blocking, on the chronos event loop)
# then echoes the message back with a request counter.
2026-05-03 16:36:37 +02:00
proc nimtimerEcho*(
timer: NimTimer, req: EchoRequest
): Future[Result[EchoResponse, string]] {.ffi.} =
await sleepAsync(req.delayMs.milliseconds)
return ok(EchoResponse(echoed: req.message, timerName: timer.name))
# --- Sync method -----------------------------------------------------------
# No await — the macro detects this and fires the callback inline,
# without going through the request channel.
2026-05-03 16:36:37 +02:00
proc nimtimerVersion*(timer: NimTimer): Future[Result[string, string]] {.ffi.} =
return ok("nim-timer v0.1.0")
2026-05-03 16:36:37 +02:00
proc nimtimerComplex*(
timer: NimTimer, req: ComplexRequest
): Future[Result[ComplexResponse, string]] {.ffi.} =
let note = if req.note.isSome: req.note.get else: "<none>"
let retries = if req.retries.isSome: req.retries.get else: 0
let count = req.messages.len
let summary =
"received " & $count & " messages, note=" & note & ", retries=" & $retries
return
ok(ComplexResponse(summary: summary, itemCount: count, hasNote: req.note.isSome))
# --- genBindings() must come AFTER every {.ffi.} / {.ffiCtor.} annotation ---
# Each pragma populates ffiProcRegistry / ffiTypeRegistry at compile time as
# the compiler processes the AST. genBindings() reads those registries to emit
# the binding files, so placing it any earlier would produce incomplete output.
# In a multi-file library, import all sub-modules first and call genBindings()
# once, at the bottom of the top-level compilation-root file.
# This call is a no-op unless -d:ffiGenBindings is passed to the compiler.
genBindings() # reads -d:ffiOutputDir, -d:ffiNimSrcRelPath, -d:targetLang from compile flags
Fix cpp vulnerabilities 1. No timeout → wait_for + 30 s default (ffi/codegen/cpp.nim) ffi_call_ now takes std::chrono::milliseconds timeout and uses cv.wait_for. All factory/method signatures carry a timeout parameter (default std::chrono::seconds{30}), mirroring the Rust blocking API. 2. Stack-allocated state → shared_ptr ownership (ffi/codegen/cpp.nim) ffi_cb_ now receives a heap-allocated std::shared_ptr<FfiCallState_>* as user_data. The refcount is 2 going in (one for ffi_call_, one for the callback). If ffi_call_ times out and returns, its copy drops — but the state stays alive (refcount 1) until Nim eventually calls back and delete sptr in ffi_cb_ drops the last reference. No more stack UAF. 3. Destructor + Rule of 5 (ffi/codegen/cpp.nim, examples/nim_timer/nim_timer.nim) Added nimtimer_destroy to nim_timer.nim with {.dynlib, exportc, cdecl, raises: [].} — joins the FFI and watchdog threads, frees the context Codegen now always emits void {libName}_destroy(void* ctx) in extern "C" and generates a destructor, deleted copy ctor/assignment, and move ctor/assignment for the context class timeout_ stored in the class; move transfers it, destructor uses it 4. Hardcoded TimerConfig in createAsync (ffi/codegen/cpp.nim) createAsync now uses the actual ctorParams list (same as create), so it's correct for any library, not just nim_timer. 5. Opaque exceptions → clear error messages (ffi/codegen/cpp.nim) deserializeFfiResult wraps nlohmann::json::parse + .get<T>() in a catch that rethrows as "FFI response deserialization failed: ...". The stoull in create() is also try-caught with "FFI create returned non-numeric address: " + raw.
2026-05-04 00:36:52 +02:00
proc nimtimer_destroy*(ctx: pointer) {.dynlib, exportc, cdecl, raises: [].} =
## Tears down the FFI context created by nimtimer_create.
## Blocks until the FFI thread and watchdog thread have joined.
try:
discard destroyFFIContext[NimTimer](cast[ptr FFIContext[NimTimer]](ctx))
except:
discard