diff --git a/ffi.nim b/ffi.nim index 4ef72bd..7b073d4 100644 --- a/ffi.nim +++ b/ffi.nim @@ -1,7 +1,7 @@ import std/[atomics, tables] import chronos, chronicles import - ffi/internal/[ffi_library, ffi_macro, c_wire], + ffi/internal/[ffi_library, ffi_macro, ffi_export, c_wire], ffi/[ alloc, ffi_types, ffi_events, ffi_handles, ffi_context, ffi_context_pool, ffi_thread_request, cbor_serial, @@ -10,5 +10,5 @@ import export atomics, tables export chronos, chronicles export - atomics, alloc, ffi_library, ffi_macro, ffi_types, ffi_events, ffi_handles, + atomics, alloc, ffi_library, ffi_macro, ffi_export, ffi_types, ffi_events, ffi_handles, ffi_context, ffi_context_pool, ffi_thread_request, cbor_serial, c_wire diff --git a/ffi/internal/ffi_export.nim b/ffi/internal/ffi_export.nim new file mode 100644 index 0000000..ae4ee79 --- /dev/null +++ b/ffi/internal/ffi_export.nim @@ -0,0 +1,91 @@ +## Simple synchronous C export for a nim-ffi library. +## +## `{.ffi.}` / `{.ffiCtor.}` are the async, context-handle, CBOR-marshaled path — +## the right tool for stateful, multi-call libraries. `{.ffiExport.}` covers the +## other common case: a handful of DEAD-SIMPLE lifecycle/health entry points a host +## loads with plain `dlopen` + `dlsym` and calls synchronously — no context, no +## callback, no CBOR. The function's own return value crosses the ABI directly. +## +## You write NATIVE Nim types; `ffiExport` bridges to the C ABI for you: +## int / bool -> C int +## uint64 -> C unsigned long long +## string -> C const char* (kept alive in shared memory until the next call) +## (no return) -> C void +## and it injects the library's `initializeLibrary()` bootstrap so the Nim runtime +## is up on first call — the host never invokes NimMain itself. +## +## declareLibraryBase("myLib") # emits initializeLibrary() +## proc my_start(): int {.ffiExport.} = 0 # -> int my_start(void) +## proc my_alive(): uint64 {.ffiExport.} = beats # -> unsigned long long my_alive(void) +## proc my_error(): string {.ffiExport.} = lastErr # -> const char* my_error(void) +## +## Build the shared library with `--noMain --nimMainPrefix:libmyLib`. Arguments are +## not supported (these are no-arg lifecycle calls); use `{.ffi.}` for calls that +## take arguments. + +import std/macros + +proc cReturnType(t: NimNode): NimNode = + ## Native Nim return type -> the C-ABI type that actually crosses the boundary. + if t.kind == nnkEmpty: + return t # void + if t.kind == nnkIdent: + case $t + of "int", "int32", "bool": return ident("cint") + of "uint", "uint64": return ident("culonglong") + of "string": return ident("cstring") + else: discard + return t # already a C-compatible type + +macro ffiExport*(prc: untyped): untyped = + ## Mark a no-argument proc as a simple synchronous C export (see module doc): + ## native Nim return type, bridged to the C ABI, with the runtime bootstrapped. + prc.expectKind({nnkProcDef, nnkFuncDef}) + let nameNode = if prc.name.kind == nnkPostfix: prc.name[1] else: prc.name + let exportName = $nameNode + let params = prc.params + if params.len > 1: + error("ffiExport supports no-argument procs; use {.ffi.} for calls with arguments") + + let nativeRet = params[0] + let cRet = cReturnType(nativeRet) + + # The user's body becomes a private impl proc; the exported wrapper converts. + let implName = genSym(nskProc, exportName & "Impl") + var impl = copyNimTree(prc) + impl[0] = implName # rename + impl[4] = newEmptyNode() # drop pragmas (internal, not exported) + + let wrapName = ident(exportName) + let boot = quote do: + when declared(initializeLibrary): + initializeLibrary() + + var res = newStmtList(impl) + + if nativeRet.kind == nnkIdent and $nativeRet == "string": + # string -> const char*: keep the bytes alive in shared memory across the call. + let buf = genSym(nskVar, exportName & "Buf") + res.add quote do: + var `buf` {.global.}: pointer = nil + proc `wrapName`(): cstring {.exportc: `exportName`, cdecl, dynlib.} = + `boot` + let s = `implName`() + if `buf` != nil: deallocShared(`buf`) + `buf` = allocShared(s.len + 1) + if s.len > 0: copyMem(`buf`, unsafeAddr s[0], s.len) + cast[ptr char](cast[uint](`buf`) + uint(s.len))[] = '\0' + return cast[cstring](`buf`) + elif nativeRet.kind == nnkEmpty: + res.add quote do: + proc `wrapName`() {.exportc: `exportName`, cdecl, dynlib.} = + `boot` + `implName`() + else: + # scalar: convert the native result to the C return type (cint / culonglong / …). + res.add quote do: + proc `wrapName`(): `cRet` {.exportc: `exportName`, cdecl, dynlib.} = + `boot` + return `cRet`(`implName`()) + + return res