nim-ffi/examples/nim_timer/nim_timer.nim
Ivan FB 0305c1ace7
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

77 lines
2.7 KiB
Nim

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
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.
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.
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.
proc nimtimerVersion*(timer: NimTimer): Future[Result[string, string]] {.ffi.} =
return ok("nim-timer v0.1.0")
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() # reads -d:ffiOutputDir, -d:ffiNimSrcRelPath, -d:targetLang from compile flags
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