From a9fc67df3fffa446fc616b6b93eebf1a251c2f6f Mon Sep 17 00:00:00 2001 From: Ivan FB Date: Sat, 8 Aug 2026 04:23:13 +0200 Subject: [PATCH] build(wasm)!: bump the edge to nim-ffi 0.3.0, matching master MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the last drift from master's 4a85db1b: the edge was still building against a vendored, threads-off fork of nim-ffi 0.1.3 while everything else had moved to 0.3.0. wasm-deps/ffi is re-vendored from the pinned 0.3.0 with a much smaller patch than the 0.1.3 one: rather than gating ~20 call sites (and re-gating them on every bump), ffi_singlethread.nim supplies API-compatible no-ops for ThreadSignalPtr and Thread, which are what --threads:off actually forbids. The upstream lifecycle code then compiles untouched. The only behavioural change is sendRequestToFFIThread, which runs the handler on the caller's chronos loop instead of enqueuing it for a worker that is never started. edge_lib.nim moves to the 0.3.0 surface: declareLibrary now takes the library type, contexts come from the generated FFIPool, and genBindings() closes the file. The five request procs use `{.ffiRaw: "abi = c".}` — the one annotation that keeps the explicit (ctx, callback, userData, ...cstring) C signature the browser already calls, so edge_new / edge_lightpush_publish / edge_filter_subscribe / edge_store_* / edge_stop keep their symbols and arity. Two things 0.3.0 changes that ARE visible, hence the `!`: - Request replies are CBOR text strings, not raw bytes. `abi = c` does not prevent it: ffiRaw expands to registerReqFFI, which pins the codec to CBOR. Short replies (a hash, "") looked fine; only storeQuery's JSON.parse caught it. Hosts must strip the 1-5 byte header — the demos and ld-edge.js do, and fall through for unframed replies so they still work against a 0.1.x binary. - FFIContext lost eventCallback/eventUserData in favour of a listener registry. logosdeliveryedge_set_event_callback keeps its exported signature and stores into a module-level slot instead, so events stay unframed and the JS is unchanged. A browser edge node has exactly one context. Request params must be `string`, not `cstring`: the request is CBOR-encoded, so a cstring field encodes the pointer and the multiaddr arrives empty. Verified against the Status staging fleet with lion-signet's selftest — 20 passed, 0 failed, 0 skipped: connect, lightpush v3, store query + paging, byte-identical payload round trip, history decrypt+verify, ns timestamps as strings, offline invite recovery, clean teardown. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012qDYE5r2t2dMry5XpWWySu --- examples/wasm/chat_demo_local.html | 20 +- library/edge/edge_lib.nim | 73 +- wasm-deps/ffi/ffi.nim | 10 +- wasm-deps/ffi/ffi.nimble | 312 ++- wasm-deps/ffi/ffi/alloc.nim | 46 +- wasm-deps/ffi/ffi/cbor_serial.nim | 46 + wasm-deps/ffi/ffi/codegen/c.nim | 1595 +++++++++++++ wasm-deps/ffi/ffi/codegen/c_cpp_common.nim | 25 + wasm-deps/ffi/ffi/codegen/cddl.nim | 190 ++ wasm-deps/ffi/ffi/codegen/consts.nim | 69 + wasm-deps/ffi/ffi/codegen/cpp.nim | 574 +++++ wasm-deps/ffi/ffi/codegen/meta.nim | 204 ++ wasm-deps/ffi/ffi/codegen/rust.nim | 825 +++++++ wasm-deps/ffi/ffi/codegen/string_helpers.nim | 96 + .../codegen/templates/c/CMakeLists.txt.tpl | 47 + .../templates/c/CMakeLists_abi.txt.tpl | 72 + .../codegen/templates/c/cbor_helpers.h.tpl | 352 +++ .../codegen/templates/c/header_prelude.h.tpl | 88 + .../codegen/templates/cpp/CMakeLists.txt.tpl | 50 + .../templates/cpp/cbor_helpers.hpp.tpl | 190 ++ .../templates/cpp/context_rule_of_5.hpp.tpl | 20 + .../templates/cpp/header_prelude.hpp.tpl | 40 + .../ffi/codegen/templates/cpp/result.hpp.tpl | 61 + .../templates/cpp/sync_call_helper.hpp.tpl | 66 + .../templates/cpp/vendor/tinycbor/LICENSE | 21 + .../templates/cpp/vendor/tinycbor/cbor.h | 724 ++++++ .../cpp/vendor/tinycbor/cborencoder.c | 689 ++++++ .../cborencoder_close_container_checked.c | 57 + .../cpp/vendor/tinycbor/cborerrorstrings.c | 188 ++ .../cpp/vendor/tinycbor/cborinternal_p.h | 316 +++ .../cpp/vendor/tinycbor/cborparser.c | 1529 ++++++++++++ .../vendor/tinycbor/cborparser_dup_string.c | 119 + .../cpp/vendor/tinycbor/compilersupport_p.h | 205 ++ .../cpp/vendor/tinycbor/tinycbor-version.h | 3 + .../templates/cpp/vendor/tinycbor/utf8_p.h | 104 + .../ffi/codegen/templates/nim_ffi_lib.cmake | 87 + wasm-deps/ffi/ffi/codegen/types_ir.nim | 129 ++ wasm-deps/ffi/ffi/event_thread.nim | 134 ++ wasm-deps/ffi/ffi/ffi_config.nim | 15 +- wasm-deps/ffi/ffi/ffi_context.nim | 513 ++-- wasm-deps/ffi/ffi/ffi_context_pool.nim | 142 ++ wasm-deps/ffi/ffi/ffi_events.nim | 330 +++ wasm-deps/ffi/ffi/ffi_handles.nim | 60 + wasm-deps/ffi/ffi/ffi_request_queue.nim | 92 + wasm-deps/ffi/ffi/ffi_singlethread.nim | 73 + wasm-deps/ffi/ffi/ffi_thread.nim | 295 +++ wasm-deps/ffi/ffi/ffi_thread_request.nim | 247 +- wasm-deps/ffi/ffi/ffi_types.nim | 27 +- .../ffi/ffi/internal/c_macro_helpers.nim | 950 ++++++++ wasm-deps/ffi/ffi/internal/c_wire.nim | 44 + .../ffi/ffi/internal/ffi_codegen_common.nim | 29 + wasm-deps/ffi/ffi/internal/ffi_export.nim | 169 ++ wasm-deps/ffi/ffi/internal/ffi_library.nim | 210 +- wasm-deps/ffi/ffi/internal/ffi_macro.nim | 2057 ++++++++++++++--- wasm-deps/ffi/ffi/internal/ffi_route.nim | 93 + wasm-deps/ffi/ffi/internal/ffi_scalar.nim | 154 ++ wasm-deps/ffi/ffi/logging.nim | 24 +- wasm-deps/ffi/nimblemeta.json | 63 +- 58 files changed, 14226 insertions(+), 737 deletions(-) create mode 100644 wasm-deps/ffi/ffi/cbor_serial.nim create mode 100644 wasm-deps/ffi/ffi/codegen/c.nim create mode 100644 wasm-deps/ffi/ffi/codegen/c_cpp_common.nim create mode 100644 wasm-deps/ffi/ffi/codegen/cddl.nim create mode 100644 wasm-deps/ffi/ffi/codegen/consts.nim create mode 100644 wasm-deps/ffi/ffi/codegen/cpp.nim create mode 100644 wasm-deps/ffi/ffi/codegen/meta.nim create mode 100644 wasm-deps/ffi/ffi/codegen/rust.nim create mode 100644 wasm-deps/ffi/ffi/codegen/string_helpers.nim create mode 100644 wasm-deps/ffi/ffi/codegen/templates/c/CMakeLists.txt.tpl create mode 100644 wasm-deps/ffi/ffi/codegen/templates/c/CMakeLists_abi.txt.tpl create mode 100644 wasm-deps/ffi/ffi/codegen/templates/c/cbor_helpers.h.tpl create mode 100644 wasm-deps/ffi/ffi/codegen/templates/c/header_prelude.h.tpl create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/CMakeLists.txt.tpl create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/cbor_helpers.hpp.tpl create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/context_rule_of_5.hpp.tpl create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/header_prelude.hpp.tpl create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/result.hpp.tpl create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/sync_call_helper.hpp.tpl create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/LICENSE create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cbor.h create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder.c create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder_close_container_checked.c create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborerrorstrings.c create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborinternal_p.h create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser.c create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser_dup_string.c create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/compilersupport_p.h create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/tinycbor-version.h create mode 100644 wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/utf8_p.h create mode 100644 wasm-deps/ffi/ffi/codegen/templates/nim_ffi_lib.cmake create mode 100644 wasm-deps/ffi/ffi/codegen/types_ir.nim create mode 100644 wasm-deps/ffi/ffi/event_thread.nim create mode 100644 wasm-deps/ffi/ffi/ffi_context_pool.nim create mode 100644 wasm-deps/ffi/ffi/ffi_events.nim create mode 100644 wasm-deps/ffi/ffi/ffi_handles.nim create mode 100644 wasm-deps/ffi/ffi/ffi_request_queue.nim create mode 100644 wasm-deps/ffi/ffi/ffi_singlethread.nim create mode 100644 wasm-deps/ffi/ffi/ffi_thread.nim create mode 100644 wasm-deps/ffi/ffi/internal/c_macro_helpers.nim create mode 100644 wasm-deps/ffi/ffi/internal/c_wire.nim create mode 100644 wasm-deps/ffi/ffi/internal/ffi_codegen_common.nim create mode 100644 wasm-deps/ffi/ffi/internal/ffi_export.nim create mode 100644 wasm-deps/ffi/ffi/internal/ffi_route.nim create mode 100644 wasm-deps/ffi/ffi/internal/ffi_scalar.nim diff --git a/examples/wasm/chat_demo_local.html b/examples/wasm/chat_demo_local.html index 01dfff139..3a3aa7252 100644 --- a/examples/wasm/chat_demo_local.html +++ b/examples/wasm/chat_demo_local.html @@ -128,13 +128,31 @@ const enc = new TextEncoder(), dec = new TextDecoder(); const cstr = (s) => { const b = enc.encode(s + "\0"); const p = M._malloc(b.length); M.HEAPU8.set(b, p); return p; }; const decode = (ptr, len) => dec.decode(M.HEAPU8.slice(ptr, ptr + len)); + // nim-ffi 0.3.0 frames request replies as a CBOR text string (major type + // 3), even under `abi = c` — the ffiRaw macro expands to registerReqFFI, + // which pins the codec to CBOR. Strip the 1-5 byte header. Anything that + // isn't a well-formed text string (e.g. a raw 0.1.x reply, or an event, + // which the library sends unframed) passes through untouched. + const decodeReply = (ptr, len) => { + if (!ptr || !len) return ""; + const b = M.HEAPU8.slice(ptr, ptr + len), ib = b[0], ai = ib & 0x1f; + if ((ib >> 5) !== 3) return dec.decode(b); + let head, size; + if (ai < 24) { head = 1; size = ai; } + else if (ai === 24) { head = 2; size = b[1]; } + else if (ai === 25) { head = 3; size = (b[1] << 8) | b[2]; } + else if (ai === 26) { head = 5; size = ((b[1] << 24) | (b[2] << 16) | (b[3] << 8) | b[4]) >>> 0; } + else return dec.decode(b); + if (head + size !== b.length) return dec.decode(b); + return dec.decode(b.subarray(head, head + size)); + }; // Every FFI request goes through here, so it is also where the pump learns // that there is work outstanding. const reqCb = () => { let resolve; const p = new Promise((r) => (resolve = r)); pump.enter(); const fn = M.addFunction((ret, msg, len) => { - M.removeFunction(fn); pump.leave(); resolve({ ret, msg: decode(msg, len) }); + M.removeFunction(fn); pump.leave(); resolve({ ret, msg: decodeReply(msg, len) }); }, "viiii"); return { fn, p }; }; diff --git a/library/edge/edge_lib.nim b/library/edge/edge_lib.nim index 1d5f7a769..843f8a73d 100644 --- a/library/edge/edge_lib.nim +++ b/library/edge/edge_lib.nim @@ -28,27 +28,59 @@ import logos_delivery/waku/waku_store/common, logos_delivery/waku/common/paging -declareLibrary("logosdeliveryedge") +declareLibrary("logosdeliveryedge", EdgeNode, defaultABIFormat = "c") # --- event callback wiring (filter push messages) ---------------------------- -var eventCallbackLock: Lock +# +# nim-ffi 0.3.0 dropped `eventCallback` / `eventUserData` from FFIContext in +# favour of a listener registry reached through `_add_event_listener`. We +# keep the single-callback surface instead: it is the ABI ld-edge.js already +# speaks, and a browser edge node has exactly one context, so a module-level +# slot is equivalent to a per-context one. +var + eventCallbackLock: Lock + gEventCallback: FFICallBack + gEventUserData: pointer initLock(eventCallbackLock) proc logosdeliveryedge_set_event_callback( ctx: ptr FFIContext[EdgeNode], callback: FFICallBack, userData: pointer ) {.exportc, cdecl.} = - if isNil(ctx): - echo "error: invalid context in logosdeliveryedge_set_event_callback" - return + ## `ctx` is unused — kept in the signature so the exported C symbol is + ## unchanged for existing callers. eventCallbackLock.acquire() defer: eventCallbackLock.release() - ctx[].eventCallback = cast[pointer](callback) - ctx[].eventUserData = userData + gEventCallback = callback + gEventUserData = userData + +proc emitEdgeEvent(eventName: string, payload: string) = + ## Hands `payload` to the registered callback verbatim, matching what + ## nim-ffi 0.1.x's `callEventCallback` put on the wire: RET_OK plus the raw + ## JSON bytes (NOT NUL-terminated), which is what ld-edge.js parses. + eventCallbackLock.acquire() + let + cb = gEventCallback + ud = gEventUserData + eventCallbackLock.release() + if cb.isNil: + chronicles.error "no event callback registered", event = eventName + return + try: + if payload.len == 0: + cb(RET_OK, nil, 0.csize_t, ud) + else: + cb(RET_OK, unsafeAddr payload[0], payload.len.csize_t, ud) + except Exception, CatchableError: + chronicles.error "event callback raised", + event = eventName, error = getCurrentExceptionMsg() # --- create node ------------------------------------------------------------- registerReqFFI(CreateEdgeNodeRequest, ctx: ptr FFIContext[EdgeNode]): - proc(serviceNode: cstring): Future[Result[string, string]] {.async.} = + # `string`, not `cstring`: 0.3.0 packs the request into a CBOR blob, and a + # cstring field would encode the pointer rather than the text (the multiaddr + # then arrives empty). The C entry point converts at the boundary. + proc(serviceNode: string): Future[Result[string, string]] {.async.} = echo "[edge] creating edge node…" let rng = crypto.newRng() let privKey = crypto.PrivateKey.random(PKScheme.Secp256k1, rng).valueOr: @@ -78,13 +110,15 @@ proc edge_new( if isNil(callback): echo "error: missing callback in edge_new" return nil - var ctx = ffi.createFFIContext[EdgeNode]().valueOr: + # 0.3.0 acquires from a fixed per-library pool that declareLibrary emits as + # FFIPool, rather than allocating a fresh context per call. + var ctx = ffi.createFFIContext(EdgeNodeFFIPool).valueOr: let msg = "Error in createFFIContext: " & $error callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) return nil ctx.userData = userData ffi.sendRequestToFFIThread( - ctx, CreateEdgeNodeRequest.ffiNewReq(callback, userData, serviceNode) + ctx, CreateEdgeNodeRequest.ffiNewReq(callback, userData, $serviceNode) ).isOkOr: let msg = "error in sendRequestToFFIThread: " & $error callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) @@ -100,7 +134,7 @@ proc edge_lightpush_publish( contentTopic: cstring, payload: cstring, metaB64: cstring, -) {.ffi.} = +) {.ffiRaw: "abi = c".} = ## Build a WakuMessage from a content topic + UTF-8 payload and lightpush it. `metaB64` is ## an optional base64 app-defined `meta` field (<=64 bytes) — e.g. a message signature. let metaBytes = @@ -128,11 +162,12 @@ proc edge_filter_subscribe( userData: pointer, pubsubTopic: cstring, contentTopics: cstring, -) {.ffi.} = +) {.ffiRaw: "abi = c".} = proc onPush(pubsubTopic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = echo "[edge] filter push received on ", msg.contentTopic, " (", msg.payload.len, " bytes)" - callEventCallback(ctx, "onReceivedMessage"): + emitEdgeEvent( + "onReceivedMessage", $( %*{ "pubsubTopic": string(pubsubTopic), @@ -140,7 +175,8 @@ proc edge_filter_subscribe( "payload": string.fromBytes(msg.payload), "meta": base64.encode(msg.meta), } - ) + ), + ) echo "[edge] filter subscribe → ", $contentTopics, " on ", $pubsubTopic ( @@ -164,7 +200,7 @@ proc edge_store_connect( callback: FFICallBack, userData: pointer, storeNode: cstring, -) {.ffi.} = +) {.ffiRaw: "abi = c".} = ## Dial a dedicated store peer. Only needed when the service node doesn't serve ## store itself (a bootstrap node typically doesn't). echo "[edge] dialing store node ", $storeNode @@ -185,7 +221,7 @@ proc edge_store_query( pageSize: cstring, forward: cstring, cursorHex: cstring, -) {.ffi.} = +) {.ffiRaw: "abi = c".} = ## One page of history. `startNs`/`endNs`/`cursorHex` are optional ("" = unset); ## `forward` is "true"/"false". Returns ## {"messages":[{hash,contentTopic,payload,meta,timestamp}], "cursor":"…"} @@ -252,7 +288,7 @@ proc edge_store_query( # --- teardown ---------------------------------------------------------------- proc edge_stop( ctx: ptr FFIContext[EdgeNode], callback: FFICallBack, userData: pointer -) {.ffi.} = +) {.ffiRaw: "abi = c".} = ## Stop the libp2p switch. Without this, "disconnect" in an app leaves the ## WebSocket to the service node open and the server still pushing filter ## messages into a dead callback, and a later reconnect builds a SECOND node. @@ -264,6 +300,9 @@ proc edge_stop( echo "[edge] switch stopped" return ok("") +# Emits nim-ffi's dispatch wrappers; must follow every {.ffiRaw.} above. +genBindings() + # Build as a wasm MAIN module (not a -shared SIDE module): drop --nimMainPrefix # (which made Nim treat this as a dynamic lib) and alias the NimMain symbol that # declareLibrary's initializeLibrary importc's. Nim emits a `main` (the module diff --git a/wasm-deps/ffi/ffi.nim b/wasm-deps/ffi/ffi.nim index 0ef64acd5..cf4f5d596 100644 --- a/wasm-deps/ffi/ffi.nim +++ b/wasm-deps/ffi/ffi.nim @@ -1,10 +1,14 @@ import std/[atomics, tables] import chronos, chronicles import - ffi/internal/[ffi_library, ffi_macro], - ffi/[alloc, ffi_types, ffi_context, ffi_thread_request] + 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, + ] export atomics, tables export chronos, chronicles export - atomics, alloc, ffi_library, ffi_macro, ffi_types, ffi_context, ffi_thread_request + 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/wasm-deps/ffi/ffi.nimble b/wasm-deps/ffi/ffi.nimble index dc39f4ca6..489343c4d 100644 --- a/wasm-deps/ffi/ffi.nimble +++ b/wasm-deps/ffi/ffi.nimble @@ -1,22 +1,314 @@ # ffi.nimble -version = "0.1.3" +version = "0.3.0" author = "Institute of Free Technology" description = "FFI framework with custom header generation" license = "MIT or Apache License 2.0" -packageName = "ffi" +packageName = "ffi" -requires "nim >= 2.2.4" +requires "nim >= 2.2.6" requires "chronos" requires "chronicles" requires "taskpools" +requires "cbor_serialization == 0.3.0" -# Source files to include -# srcDir = "src" -# installFiles = @["src/ffi.nim", "mylib.h"] +const nimFlagsOrc = "--mm:orc -d:chronicles_log_level=WARN" +const nimFlagsRefc = "--mm:refc -d:chronicles_log_level=WARN" -# # 💡 Custom build step before installation -# before install: -# echo "Generating custom C header..." -# exec "nim r tools/gen_header.nim" +const timerSrc = "examples/timer/timer.nim" +const echoSrc = "examples/echo/echo.nim" + +import std/[algorithm, os, strutils] + +proc discoverUnitTests(): seq[string] = + # `listFiles` returns both .nim sources and any compiled binaries left in + # the dir from prior local runs — filter to .nim so we don't run a test + # twice (and don't try to `nim c -r` a stale binary). + var names: seq[string] = @[] + for path in listFiles(thisDir() / "tests/unit"): + if path.endsWith(".nim"): + let name = path.extractFilename.changeFileExt("") + if name.startsWith("test_"): + names.add(name) + names.sort() + return names + +let unitTests = discoverUnitTests() + +proc runOrQuit(cmd: string) = + # Workaround for newer nimble (shipping with Nim 2.2.10+) printing the + # OSError from a failed `exec` but exiting 0, which causes CI to report + # green on actual build/test failures. Echo the command first so the log + # makes clear which step failed. + try: + exec cmd + except OSError as e: + echo "command failed: ", cmd + echo "error: ", e.msg + quit(QuitFailure) + +proc checkBindingsDiff(regenCmd: string, paths: openArray[string]) = + # On a diff, print a remediation hint instead of a bare diff wall. Re-quitting + # non-zero also dodges the nimble ≥2.2.10 exit-0-on-failure footgun (runOrQuit). + try: + exec "git diff --exit-code -- " & paths.join(" ") + except OSError: + echo "Checked-in bindings are stale. Run `" & regenCmd & "` and commit the result." + quit(QuitFailure) + +proc sanFlags(san: string): string = + # Each --passC / --passL adds one literal flag to the C compiler / linker + # invocation — avoids any quoting ambiguity that arises from putting + # space-separated flags inside a single --passC argument. + # + # `asan-ubsan` enables LeakSanitizer too: ASan includes LSan, so leaks are + # reported when ASAN_OPTIONS=detect_leaks=1 (set by the sanitizer CI job). + case san + of "none", "": + "" + of "asan-ubsan": + " --passC:-fsanitize=address,undefined" & " --passC:-fno-sanitize-recover=all" & + " --passC:-fno-omit-frame-pointer" & " --passC:-g" & + " --passL:-fsanitize=address,undefined" + of "tsan": + " --passC:-fsanitize=thread" & " --passC:-fno-omit-frame-pointer" & " --passC:-g" & + " --passC:-O1" & " --passL:-fsanitize=thread" + else: + raise newException(ValueError, "unknown NIM_FFI_SAN: " & san) + +proc mmModes(): seq[string] = + ## Memory-management modes to build under, selected by NIM_FFI_MM (empty = both). + case getEnv("NIM_FFI_MM", "") + of "orc": + @[nimFlagsOrc] + of "refc": + @[nimFlagsRefc] + else: + @[nimFlagsOrc, nimFlagsRefc] + +proc applyTsanSuppressions() = + ## Adds tsan.supp to TSAN_OPTIONS without clobbering options the CI job set. + let suppPath = thisDir() & "/tsan.supp" + let existing = getEnv("TSAN_OPTIONS") + if existing == "": + putEnv("TSAN_OPTIONS", "suppressions=" & suppPath) + elif "suppressions=" notin existing: + putEnv("TSAN_OPTIONS", existing & ":suppressions=" & suppPath) + +proc genBindingsCmd(flags, src: string, langs = "rust", outDir = ""): string = + ## One `nim c` that emits `langs` (comma-separated) from `src`. Output dir and + ## embedded source path default to `_bindings/` next to `src`; `outDir` + ## overrides every language. `--compileOnly` is enough because the binding + ## files are written during macro expansion — nothing is linked. + var cmd = + "nim c " & flags & " -d:ffiGenBindings -d:targetLang=" & langs & " --compileOnly" + if outDir.len > 0: + cmd.add " -d:ffiOutputDir=" & outDir + cmd.add " " & src + cmd + +proc removeStaleEchoLib() = + ## CMake keys the shared `libecho.so` rebuild on echo.nim's mtime, not on + ## `-d:ffiEchoAbiC`, so a stale lib from the other ABI is reused and segfaults. + ## Every echo e2e task deletes it first to force a fresh rebuild. + for name in ["libecho.so", "libecho.dylib", "echo.dll"]: + let path = thisDir() / name + if fileExists(path): + rmFile(path) + +task buildffi, "Compile the library": + exec "nim c " & nimFlagsOrc & " --app:lib --noMain ffi.nim" + +task test, "Run all tests under --mm:orc and --mm:refc": + for flags in [nimFlagsOrc, nimFlagsRefc]: + for t in unitTests: + exec "nim c -r " & flags & " tests/unit/" & t & ".nim" + +task test_alloc, "Run alloc unit tests under --mm:orc and --mm:refc": + exec "nim c -r " & nimFlagsOrc & " tests/unit/test_alloc.nim" + exec "nim c -r " & nimFlagsRefc & " tests/unit/test_alloc.nim" + +task test_ffi, "Run FFI context integration tests under --mm:orc and --mm:refc": + exec "nim c -r " & nimFlagsOrc & " tests/unit/test_ffi_context.nim" + exec "nim c -r " & nimFlagsRefc & " tests/unit/test_ffi_context.nim" + +task test_serial, "Run CBOR codec unit tests": + exec "nim c -r " & nimFlagsOrc & " tests/unit/test_serial.nim" + exec "nim c -r " & nimFlagsRefc & " tests/unit/test_serial.nim" + +task bench_codec, "Microbenchmark: cbor vs c (cwire) wire-format codecs": + # Built with -d:danger so the numbers reflect optimized codegen, not the + # debug build. Not part of `test` — timing is a measurement, not a gate. + exec "nim c -r " & nimFlagsOrc & " -d:danger tests/bench/bench_codec.nim" + +task bench_ffi_submit, + "Concurrent-submit stress + scaling gate for sendRequestToFFIThread": + # Honors NIM_FFI_SAN / NIM_FFI_MM like test_sanitized so CI drives it under + # asan-ubsan and tsan; FFI_SUBMIT_PER_THREAD sets per-thread volume. + let san = getEnv("NIM_FFI_SAN", "none") + let extra = sanFlags(san) + if san == "tsan": + applyTsanSuppressions() + for flags in mmModes(): + exec "nim c -r " & flags & " -d:danger" & extra & " tests/bench/bench_ffi_submit.nim" + +task test_cpp_e2e, "Build and run the C++ end-to-end tests for the timer example": + # Regenerate the C++ bindings so the suite always runs against fresh codegen. + runOrQuit "nimble genbindings_cpp" + runOrQuit "nimble genbindings_cpp_echo" + # Force a fresh CBOR libecho: a prior abi=c run leaves a same-named dylib that + # cmake would otherwise reuse, mismatching the CBOR bindings (segfault). + removeStaleEchoLib() + runOrQuit "cmake -S tests/e2e/cpp -B tests/e2e/cpp/build" + runOrQuit "cmake --build tests/e2e/cpp/build --config Debug" + # `-C Debug` is required on Windows multi-config generators because + # gtest_discover_tests(PRE_TEST) loads per-config include files; harmless on + # single-config generators (Make/Ninja) on Linux/macOS. + runOrQuit "ctest --test-dir tests/e2e/cpp/build --output-on-failure -C Debug" + +task test_c_e2e, "Build and run the C end-to-end tests for the timer example": + # Regenerate the C bindings so the suite always runs against fresh codegen. + runOrQuit "nimble genbindings_c" + runOrQuit "cmake -S tests/e2e/c -B tests/e2e/c/build" + runOrQuit "cmake --build tests/e2e/c/build --config Debug" + runOrQuit "ctest --test-dir tests/e2e/c/build --output-on-failure -C Debug" + +task test_c_abi_e2e, "Build and run the CBOR-free abi=c C end-to-end test (echo)": + runOrQuit "nimble genbindings_c_abi_echo" + removeStaleEchoLib() + runOrQuit "cmake -S tests/e2e/c_abi -B tests/e2e/c_abi/build" + runOrQuit "cmake --build tests/e2e/c_abi/build --config Debug" + runOrQuit "ctest --test-dir tests/e2e/c_abi/build --output-on-failure -C Debug" + +task test_sanitized, + "Run all unit tests under a sanitizer (NIM_FFI_SAN) and mm (NIM_FFI_MM)": + let san = getEnv("NIM_FFI_SAN", "none") + let extra = sanFlags(san) + if san == "tsan": + applyTsanSuppressions() + for flags in mmModes(): + for t in unitTests: + exec "nim c -r " & flags & extra & " tests/unit/" & t & ".nim" + +task test_cpp_e2e_sanitized, + "Build and run the C++ e2e tests with a sanitizer (NIM_FFI_SAN) and mm (NIM_FFI_MM)": + let mm = getEnv("NIM_FFI_MM", "orc") + let san = getEnv("NIM_FFI_SAN", "none") + runOrQuit "nimble genbindings_cpp" + runOrQuit "nimble genbindings_cpp_echo" + # See test_cpp_e2e: force a fresh CBOR libecho so a prior abi=c dylib can't be + # reused against the CBOR bindings. + removeStaleEchoLib() + runOrQuit "cmake -S tests/e2e/cpp -B tests/e2e/cpp/build" & " -DNIM_FFI_MM=" & mm & + " -DNIM_FFI_SANITIZER=" & san + runOrQuit "cmake --build tests/e2e/cpp/build --config Debug -j" + runOrQuit "ctest --test-dir tests/e2e/cpp/build --output-on-failure -C Debug" + +task test_c_e2e_sanitized, + "Build and run the C e2e tests with a sanitizer (NIM_FFI_SAN) and mm (NIM_FFI_MM)": + let mm = getEnv("NIM_FFI_MM", "orc") + let san = getEnv("NIM_FFI_SAN", "none") + runOrQuit "nimble genbindings_c" + runOrQuit "cmake -S tests/e2e/c -B tests/e2e/c/build" & " -DNIM_FFI_MM=" & mm & + " -DNIM_FFI_SANITIZER=" & san + runOrQuit "cmake --build tests/e2e/c/build --config Debug -j" + runOrQuit "ctest --test-dir tests/e2e/c/build --output-on-failure -C Debug" + +task test_c_abi_e2e_sanitized, + "Build and run the abi=c C e2e test with a sanitizer (NIM_FFI_SAN)": + let san = getEnv("NIM_FFI_SAN", "none") + runOrQuit "nimble genbindings_c_abi_echo" + removeStaleEchoLib() + runOrQuit "cmake -S tests/e2e/c_abi -B tests/e2e/c_abi/build" & " -DNIM_FFI_SANITIZER=" & + san + runOrQuit "cmake --build tests/e2e/c_abi/build --config Debug -j" + runOrQuit "ctest --test-dir tests/e2e/c_abi/build --output-on-failure -C Debug" + +task genbindings_example, "Generate Rust bindings for the timer example": + exec genBindingsCmd(nimFlagsOrc, timerSrc) + exec genBindingsCmd(nimFlagsRefc, timerSrc) + +task genbindings_rust, "Generate Rust bindings for the timer example": + exec genBindingsCmd(nimFlagsOrc, timerSrc, "rust") + exec genBindingsCmd(nimFlagsRefc, timerSrc, "rust") + +task genbindings_cddl, "Generate CDDL schema for the timer example": + exec genBindingsCmd(nimFlagsOrc, timerSrc, "cddl") + +task genbindings_cpp, "Generate C++ bindings for the timer example": + exec genBindingsCmd(nimFlagsOrc, timerSrc, "cpp") + exec genBindingsCmd(nimFlagsRefc, timerSrc, "cpp") + +task genbindings_cpp_echo, "Generate C++ bindings for the echo example": + exec genBindingsCmd(nimFlagsOrc, echoSrc, "cpp") + exec genBindingsCmd(nimFlagsRefc, echoSrc, "cpp") + +task genbindings_c, "Generate C bindings for the timer example": + exec genBindingsCmd(nimFlagsOrc, timerSrc, "c") + exec genBindingsCmd(nimFlagsRefc, timerSrc, "c") + +task genbindings_c_echo, "Generate C bindings for the echo example": + exec genBindingsCmd(nimFlagsOrc, echoSrc, "c") + exec genBindingsCmd(nimFlagsRefc, echoSrc, "c") + +task genbindings_c_abi_echo, "Generate CBOR-free abi=c C bindings for the echo example": + # abiOut forces output beside the CBOR `c_bindings/` instead of overwriting it. + const abiOut = "examples/echo/c_abi_bindings" + const abiFlags = " -d:ffiEchoAbiC -d:ffiSrcPath=../echo.nim" + exec genBindingsCmd(nimFlagsOrc & abiFlags, echoSrc, "c", abiOut) + exec genBindingsCmd(nimFlagsRefc & abiFlags, echoSrc, "c", abiOut) + +task check_bindings_rust, "Verify checked-in Rust bindings match Nim source": + runOrQuit "nimble genbindings_rust" + checkBindingsDiff( + "nimble genbindings_rust", + [ + "examples/timer/rust_bindings/Cargo.toml", + "examples/timer/rust_bindings/build.rs", "examples/timer/rust_bindings/src", + ], + ) + +task check_bindings_cpp, "Verify checked-in C++ bindings match Nim source": + runOrQuit "nimble genbindings_cpp" + runOrQuit "nimble genbindings_cpp_echo" + checkBindingsDiff( + "nimble genbindings_cpp && nimble genbindings_cpp_echo", + [ + "examples/timer/cpp_bindings/my_timer.hpp", + "examples/timer/cpp_bindings/CMakeLists.txt", + "examples/echo/cpp_bindings/echo.hpp", "examples/echo/cpp_bindings/CMakeLists.txt", + ], + ) + +task check_bindings_c, "Verify checked-in C bindings match Nim source": + runOrQuit "nimble genbindings_c" + runOrQuit "nimble genbindings_c_echo" + checkBindingsDiff( + "nimble genbindings_c && nimble genbindings_c_echo", + [ + "examples/timer/c_bindings/my_timer.h", + "examples/timer/c_bindings/nim_ffi_prelude.h", + "examples/timer/c_bindings/nim_ffi_cbor.h", + "examples/timer/c_bindings/CMakeLists.txt", "examples/echo/c_bindings/echo.h", + "examples/echo/c_bindings/nim_ffi_prelude.h", + "examples/echo/c_bindings/nim_ffi_cbor.h", + "examples/echo/c_bindings/CMakeLists.txt", + ], + ) + +task check_bindings_c_abi, "Verify checked-in abi=c C bindings match Nim source": + runOrQuit "nimble genbindings_c_abi_echo" + checkBindingsDiff( + "nimble genbindings_c_abi_echo", + [ + "examples/echo/c_abi_bindings/echo.h", + "examples/echo/c_abi_bindings/CMakeLists.txt", + ], + ) + +task check_bindings, "Verify all checked-in example bindings match Nim source": + exec "nimble check_bindings_rust" + exec "nimble check_bindings_cpp" + exec "nimble check_bindings_c" + exec "nimble check_bindings_c_abi" diff --git a/wasm-deps/ffi/ffi/alloc.nim b/wasm-deps/ffi/ffi/alloc.nim index 1a6f118b5..3bbb59388 100644 --- a/wasm-deps/ffi/ffi/alloc.nim +++ b/wasm-deps/ffi/ffi/alloc.nim @@ -1,41 +1,57 @@ -## Can be shared safely between threads +## Cross-thread allocation helpers backed by libc `malloc`/`free`. +## Avoids Nim `allocShared` whose TLS-owned MemRegion segfaults when freed from a +## thread other than the one that allocated (and may have since exited); libc is process-global. + +import system/ansi_c + type SharedSeq*[T] = tuple[data: ptr UncheckedArray[T], len: int] proc alloc*(str: cstring): cstring = - # Byte allocation from the given address. - # There should be the corresponding manual deallocation with deallocShared ! + ## Fresh null-terminated `c_malloc` copy of `str`; free with `dealloc(cstring)`. if str.isNil(): - var ret = cast[cstring](allocShared(1)) # Allocate memory for the null terminator - ret[0] = '\0' # Set the null terminator + var ret = cast[cstring](c_malloc(1)) + ret[0] = '\0' return ret - let ret = cast[cstring](allocShared(len(str) + 1)) + let ret = cast[cstring](c_malloc(csize_t(len(str) + 1))) copyMem(ret, str, len(str) + 1) return ret proc alloc*(str: string): cstring = - ## Byte allocation from the given address. - ## There should be the corresponding manual deallocation with deallocShared ! - var ret = cast[cstring](allocShared(str.len + 1)) + var ret = cast[cstring](c_malloc(csize_t(str.len + 1))) let s = cast[seq[char]](str) for i in 0 ..< str.len: ret[i] = s[i] ret[str.len] = '\0' return ret +proc dealloc*(p: cstring) {.inline.} = + ## Frees an `alloc(...)` buffer. Nil-safe. + if not p.isNil(): + c_free(cast[pointer](p)) + +proc allocBox*(size: int): pointer = + ## `c_malloc` block for a cross-thread callback box; free with `freeBox`. + c_malloc(csize_t(size)) + +proc freeBox*(p: pointer) = + if not p.isNil(): + c_free(p) + proc allocSharedSeq*[T](s: seq[T]): SharedSeq[T] = - let data = allocShared(sizeof(T) * s.len) - if s.len != 0: - copyMem(data, unsafeAddr s[0], s.len) + if s.len == 0: + return (cast[ptr UncheckedArray[T]](nil), 0) + + let data = c_malloc(csize_t(sizeof(T) * s.len)) + copyMem(data, unsafeAddr s[0], sizeof(T) * s.len) return (cast[ptr UncheckedArray[T]](data), s.len) proc deallocSharedSeq*[T](s: var SharedSeq[T]) = - deallocShared(s.data) + if not s.data.isNil(): + c_free(s.data) s.len = 0 proc toSeq*[T](s: SharedSeq[T]): seq[T] = - ## Creates a seq[T] from a SharedSeq[T]. No explicit dealloc is required - ## as req[T] is a GC managed type. var ret = newSeq[T]() for i in 0 ..< s.len: ret.add(s.data[i]) diff --git a/wasm-deps/ffi/ffi/cbor_serial.nim b/wasm-deps/ffi/ffi/cbor_serial.nim new file mode 100644 index 000000000..24b83d06a --- /dev/null +++ b/wasm-deps/ffi/ffi/cbor_serial.nim @@ -0,0 +1,46 @@ +## `cbor_serialization` wrapper adapting its exception API to `Result[T, string]` for the FFI layer. +## `.ffi.` payloads (plain `object` and `ref T`) cross as value copies; raw `pointer`/`ptr T` are +## rejected at macro-expansion time (see `rejectRawPtrType`). + +import system/ansi_c +import cbor_serialization, cbor_serialization/std/options, results + +export cbor_serialization, options, results + +const CborNullByte*: byte = 0xf6'u8 + ## CBOR `null` — wire sentinel for empty OK payloads. + +proc cborEncode*[T](x: T): seq[byte] = + return Cbor.encode(x) + +proc cborEncodeShared*[T](x: T): tuple[data: ptr UncheckedArray[byte], len: int] = + ## Encodes `x` into a caller-owned `c_malloc` buffer (free via `cborFreeShared`). + ## Empty payloads return `(nil, 0)` without allocating. + let bytes = Cbor.encode(x) + if bytes.len == 0: + return (nil, 0) + let buf = cast[ptr UncheckedArray[byte]](c_malloc(csize_t(bytes.len))) + copyMem(buf, unsafeAddr bytes[0], bytes.len) + return (buf, bytes.len) + +proc cborFreeShared*(data: var ptr UncheckedArray[byte]) = + ## Frees a `cborEncodeShared` buffer and nils the pointer. Nil-safe. + if not data.isNil(): + c_free(data) + data = nil + +proc cborDecode*[T](data: openArray[byte], _: typedesc[T]): Result[T, string] = + ## Decode `data` into a `T`, mapping any exception to `Result.err`. + try: + let v = Cbor.decode(data, T) + return ok(v) + except CatchableError as exc: + return err(exc.msg) + +proc cborDecodePtr*[T]( + data: ptr UncheckedArray[byte], dataLen: int, _: typedesc[T] +): Result[T, string] = + ## Convenience for ptr+len buffers. + if dataLen <= 0: + return cborDecode(default(seq[byte]), T) + cborDecode(toOpenArray(data, 0, dataLen - 1), T) diff --git a/wasm-deps/ffi/ffi/codegen/c.nim b/wasm-deps/ffi/ffi/codegen/c.nim new file mode 100644 index 000000000..4568594da --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/c.nim @@ -0,0 +1,1595 @@ +## C99 binding generator. `abi = cbor` (default) emits three CBOR headers; +## `abi = c` emits one header whose structs are the C ABI directly. Lacking +## generics, each distinct `seq[T]`/`Option[T]` is monomorphised per type. + +import std/[os, strutils, tables, sets, options] +import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir, ./consts + +## Fixed 64-bit wire type for any Nim `ptr T`/`pointer` (mirrors CppPtrType). +const CPtrType* = "uint64_t" + +const + HeaderPreludeTpl = staticRead("templates/c/header_prelude.h.tpl") + CborHelpersTpl = staticRead("templates/c/cbor_helpers.h.tpl") + CMakeListsTpl = staticRead("templates/c/CMakeLists.txt.tpl") + + # Shared header names; must match the include guards baked into the templates. + PreludeHeaderName* = "nim_ffi_prelude.h" + CborHeaderName* = "nim_ffi_cbor.h" + +const scalarCInfoTable: array[ScalarKind, tuple[cType, suffix: string]] = [ + skBool: ("bool", "bool"), + skI8: ("int8_t", "i8"), + skI16: ("int16_t", "i16"), + skI32: ("int32_t", "i32"), + skI64: ("int64_t", "i64"), + skU8: ("uint8_t", "u8"), + skU16: ("uint16_t", "u16"), + skU32: ("uint32_t", "u32"), + skU64: ("uint64_t", "u64"), + skF32: ("float", "f32"), + skF64: ("double", "f64"), +] + +func leafSuffix(cType: string): string = + ## Leaf codec suffix for `cType`; "" for composites. + for s in ScalarKind: + if scalarCInfoTable[s].cType == cType: + return scalarCInfoTable[s].suffix + return + case cType + of "NimFfiStr": "str" + of "NimFfiBytes": "bytes" + else: "" + +func cToken(cType: string): string = + ## PascalCase token for monomorphised names. + let suffix = leafSuffix(cType) + if suffix.len > 0: + return capitalizeFirstLetter(suffix) + return cType + +type CTypeReg = object + libName: string ## snake_case symbol prefix + libType: string ## PascalCase container-name prefix + typeTable: Table[string, FFITypeMeta] + emitted: HashSet[string] + owns: Table[string, bool] ## C type name → owns-heap-memory + decls: seq[string] + codecs: seq[string] + +func encFn(reg: CTypeReg, cType: string): string = + let suffix = leafSuffix(cType) + if suffix.len > 0: + return "nimffi_enc_" & suffix + return reg.libName & "_enc_" & cType + +func decFn(reg: CTypeReg, cType: string): string = + let suffix = leafSuffix(cType) + if suffix.len > 0: + return "nimffi_dec_" & suffix + return reg.libName & "_dec_" & cType + +func freeFn(reg: CTypeReg, cType: string): string = + ## Free-function name for `cType`, or "" when it owns no heap memory. + return + case cType + of "NimFfiStr": + "nimffi_free_str" + of "NimFfiBytes": + "nimffi_free_bytes" + else: + if leafSuffix(cType).len > 0: + "" + elif reg.owns.getOrDefault(cType, false): + reg.libName & "_free_" & cType + else: + "" + +proc emitSeqType(reg: var CTypeReg, name, elemC: string) = + let eEnc = encFn(reg, elemC) + let eDec = decFn(reg, elemC) + let eFree = freeFn(reg, elemC) + reg.decls.add( + "typedef struct {\n " & elemC & "* data;\n size_t len;\n} " & name & ";" + ) + var body: seq[string] = @[] + body.add("static inline CborError " & reg.libName & "_enc_" & name & "(") + body.add(" CborEncoder* e, const " & name & "* v) {") + body.add(" CborEncoder arr;") + body.add(" CborError err = cbor_encoder_create_array(e, &arr, v->len);") + body.add(" if (err) return err;") + body.add(" for (size_t i = 0; i < v->len; i++) {") + body.add(" err = " & eEnc & "(&arr, &v->data[i]);") + body.add(" if (err) return err;") + body.add(" }") + body.add(" return cbor_encoder_close_container(e, &arr);") + body.add("}") + body.add("static inline CborError " & reg.libName & "_dec_" & name & "(") + body.add(" CborValue* it, " & name & "* out) {") + body.add(" if (!cbor_value_is_array(it)) return CborErrorImproperValue;") + body.add(" size_t len = 0;") + body.add(" CborError err = cbor_value_get_array_length(it, &len);") + body.add(" if (err) return err;") + body.add( + " out->data = (" & elemC & "*)calloc(len ? len : 1, sizeof(" & elemC & "));" + ) + body.add(" if (!out->data) return CborErrorOutOfMemory;") + body.add(" out->len = len;") + body.add(" CborValue inner;") + body.add(" err = cbor_value_enter_container(it, &inner);") + body.add(" if (err) return err;") + body.add(" for (size_t i = 0; i < len; i++) {") + body.add(" err = " & eDec & "(&inner, &out->data[i]);") + body.add(" if (err) return err;") + body.add(" }") + body.add(" return cbor_value_leave_container(it, &inner);") + body.add("}") + body.add( + "static inline void " & reg.libName & "_free_" & name & "(" & name & "* v) {" + ) + body.add(" if (!v || !v->data) return;") + if eFree.len > 0: + body.add(" for (size_t i = 0; i < v->len; i++) " & eFree & "(&v->data[i]);") + body.add(" free(v->data);") + body.add(" v->data = NULL;") + body.add(" v->len = 0;") + body.add("}") + reg.codecs.add(body.join("\n")) + reg.owns[name] = true + +proc emitOptType(reg: var CTypeReg, name, elemC: string, elemOwns: bool) = + let eEnc = encFn(reg, elemC) + let eDec = decFn(reg, elemC) + let eFree = freeFn(reg, elemC) + reg.decls.add( + "typedef struct {\n bool has_value;\n " & elemC & " value;\n} " & name & ";" + ) + var body: seq[string] = @[] + body.add("static inline CborError " & reg.libName & "_enc_" & name & "(") + body.add(" CborEncoder* e, const " & name & "* v) {") + body.add(" if (!v->has_value) return cbor_encode_null(e);") + body.add(" return " & eEnc & "(e, &v->value);") + body.add("}") + body.add("static inline CborError " & reg.libName & "_dec_" & name & "(") + body.add(" CborValue* it, " & name & "* out) {") + body.add(" if (cbor_value_is_null(it)) {") + body.add(" out->has_value = false;") + body.add(" memset(&out->value, 0, sizeof(out->value));") + body.add(" return cbor_value_advance(it);") + body.add(" }") + body.add(" out->has_value = true;") + body.add(" return " & eDec & "(it, &out->value);") + body.add("}") + if elemOwns and eFree.len > 0: + body.add( + "static inline void " & reg.libName & "_free_" & name & "(" & name & "* v) {" + ) + body.add(" if (!v || !v->has_value) return;") + body.add(" " & eFree & "(&v->value);") + body.add(" v->has_value = false;") + body.add("}") + reg.codecs.add(body.join("\n")) + reg.owns[name] = elemOwns + +proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool] + +func enumConstName*(typeName, valueName: string): string = + ## C/CDDL-safe constant name for an enum value, e.g. ("Color", "cRed") → COLOR_C_RED. + return identToUpperSnake(typeName) & "_" & identToUpperSnake(valueName) + +proc emitEnumType(reg: var CTypeReg, t: FFITypeMeta) = + ## A `{.ffi.}` enum becomes a C enum whose codec maps to the CBOR text form + ## (the value's Nim symbol name, or its associated string) that + ## cbor_serialization writes. + var members: seq[string] = @[] + for v in t.enumValues: + members.add(" " & enumConstName(t.name, v.name) & " = " & $v.ord & ",") + members[^1].removeSuffix(',') + reg.decls.add("typedef enum {\n" & members.join("\n") & "\n} " & t.name & ";") + + var longest = 0 + for v in t.enumValues: + longest = max(longest, v.wire.len) + + var body: seq[string] = @[] + body.add("static inline CborError " & reg.libName & "_enc_" & t.name & "(") + body.add(" CborEncoder* e, const " & t.name & "* v) {") + body.add(" switch (*v) {") + for v in t.enumValues: + body.add( + " case " & enumConstName(t.name, v.name) & + ": return cbor_encode_text_stringz(e, \"" & v.wire & "\");" + ) + body.add(" }") + body.add(" return CborErrorImproperValue;") + body.add("}") + + body.add("static inline CborError " & reg.libName & "_dec_" & t.name & "(") + body.add(" CborValue* it, " & t.name & "* out) {") + body.add(" if (!cbor_value_is_text_string(it)) return CborErrorImproperValue;") + body.add(" size_t len = 0;") + body.add(" CborError err = cbor_value_get_string_length(it, &len);") + body.add(" if (err) return err;") + body.add(" char buf[" & $(longest + 1) & "];") + body.add(" if (len >= sizeof(buf)) return CborErrorImproperValue;") + body.add(" size_t copied = sizeof(buf);") + body.add(" err = cbor_value_copy_text_string(it, buf, &copied, NULL);") + body.add(" if (err) return err;") + body.add(" buf[len] = '\\0';") + for v in t.enumValues: + body.add( + " if (strcmp(buf, \"" & v.wire & "\") == 0) { *out = " & + enumConstName(t.name, v.name) & "; return cbor_value_advance(it); }" + ) + body.add(" return CborErrorImproperValue;") + body.add("}") + + reg.codecs.add(body.join("\n")) + reg.owns[t.name] = false + +proc emitStructType(reg: var CTypeReg, t: FFITypeMeta) = + var fieldDecls: seq[string] = @[] + var members: seq[tuple[name, cType: string, owns: bool]] = @[] + for f in t.fields: + let (cType, owns) = ensureCType(reg, f.typeName) + fieldDecls.add(" " & cType & " " & f.name & ";") + members.add((f.name, cType, owns)) + if members.len == 0: + fieldDecls.add(" char _nimffi_empty; /* C forbids empty structs */") + reg.decls.add("typedef struct {\n" & fieldDecls.join("\n") & "\n} " & t.name & ";") + + var body: seq[string] = @[] + body.add("static inline CborError " & reg.libName & "_enc_" & t.name & "(") + body.add(" CborEncoder* e, const " & t.name & "* v) {") + if members.len == 0: + body.add(" (void)v;") + body.add(" CborEncoder m;") + body.add(" CborError err = cbor_encoder_create_map(e, &m, " & $members.len & ");") + body.add(" if (err) return err;") + for mem in members: + body.add(" err = cbor_encode_text_stringz(&m, \"" & mem.name & "\");") + body.add(" if (err) return err;") + body.add(" err = " & encFn(reg, mem.cType) & "(&m, &v->" & mem.name & ");") + body.add(" if (err) return err;") + body.add(" return cbor_encoder_close_container(e, &m);") + body.add("}") + + body.add("static inline CborError " & reg.libName & "_dec_" & t.name & "(") + body.add(" CborValue* it, " & t.name & "* out) {") + body.add(" if (!cbor_value_is_map(it)) return CborErrorImproperValue;") + if members.len == 0: + body.add(" (void)out;") + body.add(" return cbor_value_advance(it);") + else: + body.add(" CborValue field;") + body.add(" CborError err;") + for mem in members: + body.add(" err = cbor_value_map_find_value(it, \"" & mem.name & "\", &field);") + body.add(" if (err) return err;") + body.add(" if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;") + body.add( + " err = " & decFn(reg, mem.cType) & "(&field, &out->" & mem.name & ");" + ) + body.add(" if (err) return err;") + body.add(" return cbor_value_advance(it);") + body.add("}") + + var owns = false + for mem in members: + if mem.owns: + owns = true + if owns: + body.add( + "static inline void " & reg.libName & "_free_" & t.name & "(" & t.name & "* v) {" + ) + body.add(" if (!v) return;") + for mem in members: + let ff = freeFn(reg, mem.cType) + if mem.owns and ff.len > 0: + body.add(" " & ff & "(&v->" & mem.name & ");") + body.add("}") + reg.codecs.add(body.join("\n")) + reg.owns[t.name] = owns + +proc ensureCType(reg: var CTypeReg, t: FFIType): tuple[cType: string, owns: bool] = + ## Lowers an `FFIType` to a C type, monomorphising each `seq[T]`/`Option[T]` + ## on first sight. `owns` marks a type the caller must free. + case t.kind + of ftPtr: + return (CPtrType, false) + of ftScalar: + return (scalarCInfoTable[t.scalar].cType, false) + of ftStr: + return ("NimFfiStr", true) + of ftBytes: + return ("NimFfiBytes", true) + of ftSeq: + let (elemC, _) = ensureCType(reg, t.elem) + let name = reg.libType & "Seq_" & cToken(elemC) + if name notin reg.emitted: + reg.emitted.incl(name) + emitSeqType(reg, name, elemC) + return (name, true) + of ftOpt: + let (elemC, elemOwns) = ensureCType(reg, t.elem) + let name = reg.libType & "Opt_" & cToken(elemC) + if name notin reg.emitted: + reg.emitted.incl(name) + emitOptType(reg, name, elemC, elemOwns) + return (name, reg.owns.getOrDefault(name, false)) + of ftStruct: + let name = t.name + if name notin reg.emitted: + reg.emitted.incl(name) + if name in reg.typeTable: + let meta = reg.typeTable[name] + if meta.isEnum(): + emitEnumType(reg, meta) + else: + emitStructType(reg, meta) + else: + reg.decls.add("/* unknown type referenced: " & name & " */") + return (name, reg.owns.getOrDefault(name, false)) + +proc ensureCType(reg: var CTypeReg, nimType: string): tuple[cType: string, owns: bool] = + return ensureCType(reg, parseFFIType(nimType)) + +proc reqTypeMeta(p: FFIProcMeta): FFITypeMeta = + ## Synthesises the per-proc Req struct; pointer/handle params ride as uint64. + var fields: seq[FFIFieldMeta] = @[] + for ep in p.extraParams: + let typeName = if ep.ridesAsPtr(): "pointer" else: ep.typeName + fields.add(FFIFieldMeta(name: ep.name, typeName: typeName)) + return FFITypeMeta(name: reqStructName(p), fields: fields) + +func paramByValue(reg: CTypeReg, nimType: string, ridesAsPtr: bool): bool = + ## Scalars/pointers/string views and enums pass by value; aggregates by const pointer. + if ridesAsPtr: + return true + let t = parseFFIType(nimType) + if t.kind == ftStruct and reg.typeTable.getOrDefault(t.name).isEnum(): + return true + return t.kind in {ftScalar, ftStr, ftPtr} + +proc cReturnType(reg: var CTypeReg, p: FFIProcMeta): string = + if p.returnRidesAsPtr(): + return CPtrType + return ensureCType(reg, p.returnTypeName).cType + +proc buildReqParams( + reg: var CTypeReg, eps: seq[FFIParamMeta] +): tuple[params, assigns: seq[string]] = + var params: seq[string] = @[] + var assigns: seq[string] = @[] + for ep in eps: + let rides = ep.ridesAsPtr() + let cType = + if rides: + CPtrType + else: + ensureCType(reg, ep.typeName).cType + if paramByValue(reg, ep.typeName, rides): + params.add(cType & " " & ep.name) + assigns.add(" ffi_req." & ep.name & " = " & ep.name & ";") + else: + params.add("const " & cType & "* " & ep.name) + assigns.add(" ffi_req." & ep.name & " = *" & ep.name & ";") + return (params, assigns) + +proc evNames( + libType, libName: string, ev: FFIEventMeta +): tuple[fnType, boxType, tramp, regName: string] = + let pascal = capitalizeFirstLetter(ev.nimProcName) + let snake = camelToSnakeCase(ev.nimProcName) + return ( + libType & pascal & "Fn", + libType & pascal & "Box", + libName & "_" & snake & "_trampoline", + libName & "_ctx_add_" & snake & "_listener", + ) + +proc emitEventMachinery( + lines: var seq[string], + reg: CTypeReg, + libType, libName: string, + events: seq[FFIEventMeta], +) = + if events.len == 0: + return + lines.add("/* Event listener machinery */") + for ev in events: + let n = evNames(libType, libName, ev) + let payC = ev.payloadTypeName + let payFree = freeFn(reg, payC) + lines.add( + "typedef void (*" & n.fnType & ")(const " & payC & "* evt, void* user_data);" + ) + lines.add( + "typedef struct { " & n.fnType & " fn; void* user_data; } " & n.boxType & ";" + ) + lines.add( + "static void " & n.tramp & "(int ret, const char* msg, size_t len, void* ud) {" + ) + lines.add(" if (!ud || ret != 0 || !msg || len == 0) return;") + lines.add(" " & n.boxType & "* box = (" & n.boxType & "*)ud;") + lines.add(" if (!box->fn) return;") + lines.add(" CborParser parser;") + lines.add(" CborValue it;") + lines.add( + " if (cbor_parser_init((const uint8_t*)msg, len, 0, &parser, &it) != CborNoError) return;" + ) + lines.add(" if (!cbor_value_is_map(&it)) return;") + lines.add(" CborValue payloadField;") + lines.add( + " if (cbor_value_map_find_value(&it, \"payload\", &payloadField) != CborNoError) return;" + ) + lines.add(" " & payC & " payload;") + lines.add(" memset(&payload, 0, sizeof(payload));") + lines.add( + " if (" & decFn(reg, payC) & "(&payloadField, &payload) != CborNoError) return;" + ) + lines.add(" box->fn(&payload, box->user_data);") + if payFree.len > 0: + lines.add(" " & payFree & "(&payload);") + lines.add("}") + lines.add("") + +proc emitContextStruct( + lines: var seq[string], ctxType: string, events: seq[FFIEventMeta] +) = + lines.add("/* ============================================================ */") + lines.add("/* High-level context wrapper */") + lines.add("/* ============================================================ */") + if events.len > 0: + lines.add("typedef struct {") + lines.add(" uint64_t id;") + lines.add(" void* box;") + lines.add("} " & ctxType & "Listener;") + lines.add("") + lines.add("typedef struct {") + lines.add(" void* ptr;") + if events.len > 0: + lines.add(" " & ctxType & "Listener* listeners;") + lines.add(" size_t listeners_len;") + lines.add(" size_t listeners_cap;") + lines.add("} " & ctxType & ";") + lines.add("") + +proc emitCallBox(lines: var seq[string], fnType, boxType: string) = + lines.add("typedef struct { " & fnType & " fn; void* user_data; } " & boxType & ";") + +proc emitReplyTrampolineHead(lines: var seq[string], tramp, boxType, fallback: string) = + ## Opens a reply trampoline: recover the box, fail if no callback, deliver a + ## non-zero `ret` as an error (msg/len isn't NUL-terminated, so copy it). + lines.add( + "static void " & tramp & "(int ret, const char* msg, size_t len, void* ud) {" + ) + lines.add(" " & boxType & "* box = (" & boxType & "*)ud;") + lines.add( + " /* Non-terminal progress ping: keep the box for the terminal reply. */" + ) + lines.add(" if (ret == NIMFFI_RET_STALE_WARN) return;") + lines.add(" if (!box->fn) {") + lines.add(" free(box);") + lines.add(" return;") + lines.add(" }") + lines.add(" if (ret != 0) {") + lines.add(" char* em = nimffi_dup_cstr_n(msg ? msg : \"\", msg ? len : 0);") + lines.add( + " box->fn(ret, NULL, em ? em : \"" & fallback & "\", box->user_data);" + ) + lines.add(" free(em);") + lines.add(" free(box);") + lines.add(" return;") + lines.add(" }") + +proc emitConstructors( + lines: var seq[string], + reg: var CTypeReg, + ctxType, libType, libName: string, + ctors: seq[FFIProcMeta], +) = + if ctors.len == 0: + return + let fnType = libType & "CreateFn" + let boxType = libType & "CreateBox" + let tramp = libName & "_create_trampoline" + lines.add( + "typedef void (*" & fnType & ")(int err_code, " & ctxType & + "* ctx, const char* err_msg, void* user_data);" + ) + emitCallBox(lines, fnType, boxType) + emitReplyTrampolineHead(lines, tramp, boxType, "FFI create failed") + lines.add(" char* err = NULL;") + lines.add(" NimFfiStr addr;") + lines.add(" memset(&addr, 0, sizeof(addr));") + lines.add( + " if (nimffi_decode_from_buf(" & libName & + "_decv_Str, (const uint8_t*)msg, len, &addr, &err) != 0) {" + ) + lines.add(" box->fn(-1, NULL, err ? err : \"decode failed\", box->user_data);") + lines.add(" free(err);") + lines.add(" free(box);") + lines.add(" return;") + lines.add(" }") + lines.add(" char* endp = NULL;") + lines.add( + " unsigned long long a = addr.data ? strtoull(addr.data, &endp, 10) : 0;" + ) + lines.add(" bool ok = addr.data && addr.len > 0 && endp && *endp == '\\0';") + lines.add(" nimffi_free_str(&addr);") + lines.add(" if (!ok) {") + lines.add( + " box->fn(-1, NULL, \"FFI create returned non-numeric address\", box->user_data);" + ) + lines.add(" free(box);") + lines.add(" return;") + lines.add(" }") + lines.add( + " " & ctxType & "* ctx = (" & ctxType & "*)calloc(1, sizeof(" & ctxType & "));" + ) + lines.add(" if (!ctx) {") + lines.add(" box->fn(-1, NULL, \"out of memory\", box->user_data);") + lines.add(" free(box);") + lines.add(" return;") + lines.add(" }") + lines.add(" ctx->ptr = (void*)(uintptr_t)a;") + lines.add(" box->fn(NIMFFI_RET_OK, ctx, NULL, box->user_data);") + lines.add(" free(box);") + lines.add("}") + lines.add("") + for ctor in ctors: + let reqName = reqStructName(ctor) + let (params, assigns) = buildReqParams(reg, ctor.extraParams) + let head = "static inline int " & libName & "_ctx_create(" + let sig = + if params.len > 0: + head & params.join(", ") & ", " & fnType & " on_created, void* user_data) {" + else: + head & fnType & " on_created, void* user_data) {" + lines.add(renderBlockDocComment(ctor.doc)) + lines.add(sig) + lines.add(" " & reqName & " ffi_req;") + lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));") + for a in assigns: + lines.add(a) + lines.add(" uint8_t* req_buf = NULL;") + lines.add(" size_t req_len = 0;") + lines.add(" char* err = NULL;") + lines.add( + " if (nimffi_encode_to_buf(" & libName & "_encv_" & cToken(reqName) & + ", &ffi_req, &req_buf, &req_len, &err) != 0) {" + ) + lines.add( + " if (on_created) on_created(-1, NULL, err ? err : \"encode failed\", user_data);" + ) + lines.add(" free(err);") + lines.add(" return -1;") + lines.add(" }") + lines.add( + " " & boxType & "* box = (" & boxType & "*)malloc(sizeof(" & boxType & "));" + ) + lines.add(" if (!box) {") + lines.add(" free(req_buf);") + lines.add( + " if (on_created) on_created(-1, NULL, \"out of memory\", user_data);" + ) + lines.add(" return -1;") + lines.add(" }") + lines.add(" box->fn = on_created;") + lines.add(" box->user_data = user_data;") + lines.add(" (void)" & ctor.procName & "(req_buf, req_len, " & tramp & ", box);") + lines.add(" free(req_buf);") + lines.add(" return 0;") + lines.add("}") + lines.add("") + +proc emitDestructor( + lines: var seq[string], + ctxType, libName: string, + dtor: Option[FFIProcMeta], + events: seq[FFIEventMeta], +) = + if dtor.isSome(): + lines.add(renderBlockDocComment(dtor.get().doc)) + lines.add("static inline int " & libName & "_ctx_destroy(" & ctxType & "* ctx) {") + lines.add(" if (!ctx) return NIMFFI_RET_OK;") + lines.add(" int rc = NIMFFI_RET_OK;") + if dtor.isSome(): + lines.add( + " if (ctx->ptr) { rc = " & dtor.get().procName & + "(ctx->ptr); ctx->ptr = NULL; }" + ) + if events.len > 0: + # A failed teardown leaves the worker threads live (ffi_context.nim: + # stopAndJoinThreads), and they still hold each box as callback user_data. + # Leaking a box beats handing a running event thread a dangling pointer. + lines.add(" if (rc == NIMFFI_RET_OK) {") + lines.add( + " for (size_t i = 0; i < ctx->listeners_len; i++) free(ctx->listeners[i].box);" + ) + lines.add(" }") + lines.add(" free(ctx->listeners);") + lines.add(" free(ctx);") + lines.add(" return rc;") + lines.add("}") + lines.add("") + +proc emitListenerApi( + lines: var seq[string], ctxType, libType, libName: string, events: seq[FFIEventMeta] +) = + if events.len == 0: + return + for ev in events: + let n = evNames(libType, libName, ev) + lines.add(renderBlockDocComment(ev.doc)) + lines.add( + "static inline uint64_t " & n.regName & "(" & ctxType & "* ctx, " & n.fnType & + " fn, void* user_data) {" + ) + lines.add( + " " & n.boxType & "* box = (" & n.boxType & "*)malloc(sizeof(" & n.boxType & + "));" + ) + lines.add(" if (!box) return 0;") + lines.add(" box->fn = fn;") + lines.add(" box->user_data = user_data;") + lines.add( + " uint64_t id = " & libName & "_add_event_listener(ctx->ptr, \"" & ev.wireName & + "\", " & n.tramp & ", box);" + ) + lines.add(" if (id == 0) { free(box); return 0; }") + lines.add(" if (ctx->listeners_len == ctx->listeners_cap) {") + lines.add(" size_t ncap = ctx->listeners_cap ? ctx->listeners_cap * 2 : 4;") + lines.add( + " " & ctxType & "Listener* grown = (" & ctxType & + "Listener*)realloc(ctx->listeners, ncap * sizeof(" & ctxType & "Listener));" + ) + lines.add( + " if (!grown) { " & libName & + "_remove_event_listener(ctx->ptr, id); free(box); return 0; }" + ) + lines.add(" ctx->listeners = grown;") + lines.add(" ctx->listeners_cap = ncap;") + lines.add(" }") + lines.add(" ctx->listeners[ctx->listeners_len].id = id;") + lines.add(" ctx->listeners[ctx->listeners_len].box = box;") + lines.add(" ctx->listeners_len++;") + lines.add(" return id;") + lines.add("}") + lines.add("") + lines.add( + "static inline bool " & libName & "_ctx_remove_event_listener(" & ctxType & + "* ctx, uint64_t id) {" + ) + lines.add(" if (id == 0) return false;") + lines.add(" int rc = " & libName & "_remove_event_listener(ctx->ptr, id);") + lines.add(" for (size_t i = 0; i < ctx->listeners_len; i++) {") + lines.add(" if (ctx->listeners[i].id == id) {") + lines.add(" free(ctx->listeners[i].box);") + lines.add(" ctx->listeners[i] = ctx->listeners[ctx->listeners_len - 1];") + lines.add(" ctx->listeners_len--;") + lines.add(" break;") + lines.add(" }") + lines.add(" }") + lines.add(" return rc == 0;") + lines.add("}") + lines.add("") + +proc emitProcWrapper( + lines: var seq[string], + reg: var CTypeReg, + ctxType, libType, libName: string, + m: FFIProcMeta, +) = + ## Reply trampoline + wrapper: `_ctx_`, or `_static_` for a + ## static; `_` itself is the raw symbol the dylib exports. + let isStatic = m.isStatic() + let stripped = stripLibPrefix(m.procName, libName) + let reqName = reqStructName(m) + let retC = cReturnType(reg, m) + let retFree = freeFn(reg, retC) + let (params, assigns) = buildReqParams(reg, m.extraParams) + let methodPascal = snakeToPascalCase(stripped) + let fnType = libType & methodPascal & "ReplyFn" + let boxType = libType & methodPascal & "CallBox" + let tramp = libName & "_" & stripped & "_reply_trampoline" + + lines.add( + "typedef void (*" & fnType & ")(int err_code, const " & retC & + "* reply, const char* err_msg, void* user_data);" + ) + emitCallBox(lines, fnType, boxType) + emitReplyTrampolineHead(lines, tramp, boxType, "FFI call failed") + lines.add(" char* err = NULL;") + lines.add(" " & retC & " out;") + lines.add(" memset(&out, 0, sizeof(out));") + lines.add( + " int dec = nimffi_decode_from_buf(" & libName & "_decv_" & cToken(retC) & + ", (const uint8_t*)msg, len, &out, &err);" + ) + lines.add(" if (dec != 0) {") + lines.add(" box->fn(-1, NULL, err ? err : \"decode failed\", box->user_data);") + lines.add(" free(err);") + # Reclaim fields a partial decode allocated (out is zeroed). + if retFree.len > 0: + lines.add(" " & retFree & "(&out);") + lines.add(" free(box);") + lines.add(" return;") + lines.add(" }") + lines.add(" box->fn(NIMFFI_RET_OK, &out, NULL, box->user_data);") + if retFree.len > 0: + lines.add(" " & retFree & "(&out);") + lines.add(" free(box);") + lines.add("}") + + let head = + if isStatic: + "static inline int " & libName & "_static_" & stripped & "(" + else: + "static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType & + "* ctx, " + let sig = + if params.len > 0: + head & params.join(", ") & ", " & fnType & " on_reply, void* user_data) {" + else: + head & fnType & " on_reply, void* user_data) {" + lines.add(renderBlockDocComment(m.doc)) + lines.add(sig) + lines.add(" " & reqName & " ffi_req;") + lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));") + for a in assigns: + lines.add(a) + lines.add(" uint8_t* req_buf = NULL;") + lines.add(" size_t req_len = 0;") + lines.add(" char* err = NULL;") + lines.add( + " if (nimffi_encode_to_buf(" & libName & "_encv_" & cToken(reqName) & + ", &ffi_req, &req_buf, &req_len, &err) != 0) {" + ) + lines.add( + " if (on_reply) on_reply(-1, NULL, err ? err : \"encode failed\", user_data);" + ) + lines.add(" free(err);") + lines.add(" return -1;") + lines.add(" }") + lines.add( + " " & boxType & "* box = (" & boxType & "*)malloc(sizeof(" & boxType & "));" + ) + lines.add(" if (!box) {") + lines.add(" free(req_buf);") + lines.add(" if (on_reply) on_reply(-1, NULL, \"out of memory\", user_data);") + lines.add(" return -1;") + lines.add(" }") + lines.add(" box->fn = on_reply;") + lines.add(" box->user_data = user_data;") + let ctxArg = if isStatic: "" else: "ctx->ptr, " + lines.add( + " int ret = " & m.procName & "(" & ctxArg & tramp & ", box, req_buf, req_len);" + ) + lines.add(" free(req_buf);") + lines.add(" if (ret == NIMFFI_RET_MISSING_CALLBACK) {") + lines.add( + " if (on_reply) on_reply(-1, NULL, \"RET_MISSING_CALLBACK (internal error)\", user_data);" + ) + lines.add(" free(box);") + lines.add(" return -1;") + lines.add(" }") + lines.add(" return 0;") + lines.add("}") + lines.add("") + +proc newCTypeReg( + libName, libType: string, types: seq[FFITypeMeta], procs: seq[FFIProcMeta] +): CTypeReg = + var reg = CTypeReg(libName: libName, libType: libType) + for t in types: + reg.typeTable[t.name] = t + for p in procs: + if p.kind != FFIKind.DTOR: + let rt = reqTypeMeta(p) + reg.typeTable[rt.name] = rt + return reg + +proc monomorphiseAll( + reg: var CTypeReg, + types: seq[FFITypeMeta], + procs, replyProcs: seq[FFIProcMeta], + events: seq[FFIEventMeta], +): tuple[reqTypes, respTypes: seq[string]] = + ## Runs every type, Req, return type and event payload through ensureCType, + ## returning the Req and response C type names the buffer adapters need. + for t in types: + discard ensureCType(reg, t.name) + var reqTypes: seq[string] = @[] + for p in procs: + if p.kind != FFIKind.DTOR: + let n = reqStructName(p) + discard ensureCType(reg, n) + reqTypes.add(n) + var respTypes: seq[string] = @[] + for p in replyProcs: + respTypes.add(cReturnType(reg, p)) + for ev in events: + discard ensureCType(reg, ev.payloadTypeName) + return (reqTypes, respTypes) + +func constDeclLines(consts: seq[FFIConstMeta]): seq[string] = + ## `{.ffiConst.}` values as typed `static const` definitions; shared by the + ## CBOR and `abi = c` headers. + if consts.len == 0: + return @[] + var lines = @[ + "/* ============================================================ */", + "/* Generated constants */", + "/* ============================================================ */", "", + ] + for c in consts: + let t = parseFFIType(c.typeName) + let name = identToUpperSnake(c.name) + let value = cConstValue(t, c.value) + case t.kind + of ftStr: + lines.add("static const char* const " & name & " = " & value & ";") + of ftScalar: + lines.add( + "static const " & scalarCInfoTable[t.scalar].cType & " " & name & " = " & value & + ";" + ) + else: + discard + lines.add("") + return lines + +func generateCPreludeHeader*(): string = + ## The library-agnostic `nim_ffi_prelude.h`, emitted verbatim. + return HeaderPreludeTpl & "\n" + +func generateCCborHeader*(): string = + ## The library-agnostic `nim_ffi_cbor.h`, emitted verbatim. + return CborHelpersTpl & "\n" + +proc generateCLibHeader*( + procs: seq[FFIProcMeta], + types: seq[FFITypeMeta], + libName: string, + events: seq[FFIEventMeta] = @[], + consts: seq[FFIConstMeta] = @[], +): string = + ## The `.h` header: library structs, monomorphised codecs and async API. + let classified = classifyProcs(procs) + let ctors = classified.ctors + let libType = libTypeName(ctors, libName) + let ctxType = libType & "Ctx" + + var reg = newCTypeReg(libName, libType, types, procs) + let (reqTypes, respTypes) = + monomorphiseAll(reg, types, procs, classified.replyProcs(), events) + + let guard = "NIM_FFI_LIB_" & libName.toUpperAscii() & "_H_INCLUDED" + var lines: seq[string] = @[] + lines.add("#ifndef " & guard) + lines.add("#define " & guard) + lines.add("#include \"" & CborHeaderName & "\"") + lines.add("") + + lines.add(constDeclLines(consts)) + + lines.add("/* ============================================================ */") + lines.add("/* Generated types (user-declared + per-proc request envelopes) */") + lines.add("/* ============================================================ */") + lines.add("") + for decl in reg.decls: + lines.add(decl) + lines.add("") + for codec in reg.codecs: + lines.add(codec) + lines.add("") + + lines.add("/* ============================================================ */") + lines.add("/* C ABI declarations (symbols exported by the Nim dylib) */") + lines.add("/* ============================================================ */") + lines.add("#ifdef __cplusplus") + lines.add("extern \"C\" {") + lines.add("#endif") + lines.add("") + for p in procs: + lines.add(renderBlockDocComment(p.doc)) + case p.kind + of FFIKind.FFI: + lines.add( + "int " & p.procName & "(void* ctx, FFICallback callback, void* user_data, " & + "const uint8_t* req_cbor, size_t req_cbor_len);" + ) + of FFIKind.STATIC: + lines.add( + "int " & p.procName & "(FFICallback callback, void* user_data, " & + "const uint8_t* req_cbor, size_t req_cbor_len);" + ) + of FFIKind.CTOR: + lines.add( + "void* " & p.procName & "(const uint8_t* req_cbor, size_t req_cbor_len, " & + "FFICallback callback, void* user_data);" + ) + of FFIKind.DTOR: + lines.add("int " & p.procName & "(void* ctx);") + lines.add( + "uint64_t " & libName & "_add_event_listener(void* ctx, const char* event_name, " & + "FFICallback callback, void* user_data);" + ) + lines.add( + "int " & libName & "_remove_event_listener(void* ctx, uint64_t listener_id);" + ) + lines.add("") + lines.add("#ifdef __cplusplus") + lines.add("} /* extern \"C\" */") + lines.add("#endif") + lines.add("") + + # Per-Req encode / per-response decode void* adapters for the buffer drivers. + var adaptersDone = initHashSet[string]() + lines.add("/* CBOR buffer adapters (typed codec → void* driver signature) */") + for n in reqTypes: + let tok = cToken(n) + if ("enc" & tok) notin adaptersDone: + adaptersDone.incl("enc" & tok) + lines.add( + "static inline CborError " & libName & "_encv_" & tok & + "(CborEncoder* e, const void* v) { return " & reg.libName & "_enc_" & n & + "(e, (const " & n & "*)v); }" + ) + var respSet = respTypes + respSet.add("NimFfiStr") # ctor address payload + for n in respSet: + let tok = cToken(n) + if ("dec" & tok) notin adaptersDone: + adaptersDone.incl("dec" & tok) + lines.add( + "static inline CborError " & libName & "_decv_" & tok & + "(CborValue* it, void* v) { return " & decFn(reg, n) & "(it, (" & n & "*)v); }" + ) + lines.add("") + + emitEventMachinery(lines, reg, libType, libName, events) + emitContextStruct(lines, ctxType, events) + emitConstructors(lines, reg, ctxType, libType, libName, ctors) + emitDestructor(lines, ctxType, libName, classified.dtor, events) + emitListenerApi(lines, ctxType, libType, libName, events) + for m in classified.replyProcs(): + emitProcWrapper(lines, reg, ctxType, libType, libName, m) + + lines.add("#endif /* " & guard & " */") + return lines.join("\n") & "\n" + +proc generateCCMakeLists*(libName, nimSrcRelPath: string): string = + let src = nimSrcRelPath.replace("\\", "/") + return CMakeListsTpl.multiReplace(("{{LIB}}", libName), ("{{SRC}}", src)) + +# `abi = c` binding: structs are the C ABI directly (no CBOR), matching the Nim-side wire layout byte-for-byte. + +const AbiCPtrType = "void*" +const AbiCMakeListsTpl = staticRead("templates/c/CMakeLists_abi.txt.tpl") + +func abiLeafCType(t: string): tuple[ok: bool, cType: string] = + ## Nim leaf type → `abi = c` wire C type; `ok` is false for composites. + return + case t + of "int", "int64": + (true, "int64_t") + of "int32": + (true, "int32_t") + of "int16": + (true, "int16_t") + of "int8": + (true, "int8_t") + of "uint", "uint64": + (true, "uint64_t") + of "uint32": + (true, "uint32_t") + of "uint16": + (true, "uint16_t") + of "uint8", "byte": + (true, "uint8_t") + of "bool": + (true, "bool") + of "float", "float64": + (true, "double") + of "float32": + (true, "float") + of "pointer": + (true, AbiCPtrType) + of "string", "cstring": + (true, "const char*") + else: + (false, "") + +type AbiReg = object + typeTable: Table[string, FFITypeMeta] + emitted: HashSet[string] + decls: seq[string] + +proc ensureAbiStruct(reg: var AbiReg, typeName: string) + +proc abiWireValueCType(reg: var AbiReg, nimType: string): string = + ## `abi = c` C type for a value-position field (a top-level `seq` splits in two). + let t = nimType.strip() + if t.startsWith("ptr ") or t == "pointer": + return AbiCPtrType + let leaf = abiLeafCType(t) + if leaf.ok: + return leaf.cType + var optInner = genericInnerType(t, "Option[") + if optInner.len == 0: + optInner = genericInnerType(t, "Maybe[") + if optInner.len > 0: + return abiWireValueCType(reg, optInner.strip()) & "*" + if genericInnerType(t, "seq[").len > 0: + raise newException( + ValueError, "abi = c: `seq` has no single-field wire form, so it can't nest: " & t + ) + if genericInnerType(t, "array[").len > 0: + raise newException( + ValueError, "abi = c: array fields are not yet supported by the C backend: " & t + ) + if t in reg.typeTable: + ensureAbiStruct(reg, t) + return t + raise newException(ValueError, "abi = c: unknown field type: " & t) + +proc abiFieldDecls(reg: var AbiReg, name, nimType: string): seq[string] = + let seqInner = genericInnerType(nimType.strip(), "seq[") + if seqInner.len > 0: + let elemC = abiWireValueCType(reg, seqInner.strip()) + return @[elemC & "* " & name & "_items;", "ptrdiff_t " & name & "_len;"] + return @[abiWireValueCType(reg, nimType) & " " & name & ";"] + +proc emitAbiStruct(reg: var AbiReg, t: FFITypeMeta) = + var members: seq[string] = @[] + for f in t.fields: + for line in abiFieldDecls(reg, f.name, f.typeName): + members.add(" " & line) + if members.len == 0: + members.add(" uint8_t _placeholder; /* C forbids empty structs */") + reg.decls.add("typedef struct {\n" & members.join("\n") & "\n} " & t.name & ";") + +proc ensureAbiStruct(reg: var AbiReg, typeName: string) = + if typeName in reg.emitted: + return + reg.emitted.incl(typeName) + if typeName in reg.typeTable: + emitAbiStruct(reg, reg.typeTable[typeName]) + else: + reg.decls.add("/* unknown type referenced: " & typeName & " */") + +proc newAbiReg(types: seq[FFITypeMeta], procs: seq[FFIProcMeta]): AbiReg = + var reg = AbiReg() + for t in types: + reg.typeTable[t.name] = t + for p in procs: + if p.kind != FFIKind.DTOR and not p.scalarFastPath: + let rt = reqTypeMeta(p) + reg.typeTable[rt.name] = rt + return reg + +func abiParamByValue(nimType: string, ridesAsPtr: bool): bool = + ## Scalars/pointers/string views pass by value; aggregates by const pointer. + if ridesAsPtr: + return true + return abiLeafCType(nimType.strip()).ok + +proc abiReqParamsAndAssigns( + reg: var AbiReg, extraParams: seq[FFIParamMeta] +): tuple[params, assigns: seq[string]] = + var params, assigns: seq[string] = @[] + for ep in extraParams: + let rides = ep.ridesAsPtr() + let cType = + if rides: + AbiCPtrType + else: + abiWireValueCType(reg, ep.typeName) + if abiParamByValue(ep.typeName, rides): + params.add(cType & " " & ep.name) + assigns.add(" ffi_req." & ep.name & " = " & ep.name & ";") + else: + params.add("const " & cType & "* " & ep.name) + assigns.add(" ffi_req." & ep.name & " = *" & ep.name & ";") + return (params, assigns) + +proc abiMethodReplyInfo( + reg: var AbiReg, libType: string, m: FFIProcMeta +): tuple[fnType, replyParam: string] = + ## Reply-callback typedef name plus the C type of its `reply` argument. + let pascal = snakeToPascalCase(stripLibPrefix(m.procName, m.libName)) + let fnType = libType & pascal & "ReplyFn" + if m.returnRidesAsPtr(): + raise newException( + ValueError, + "abi = c: handle/pointer returns are not yet supported by the C backend: " & + m.procName, + ) + let rt = m.returnTypeName.strip() + let leaf = abiLeafCType(rt) + let replyParam = + if rt == "string" or rt == "cstring": + "const char*" + elif leaf.ok: + "const " & leaf.cType & "*" + else: + ensureAbiStruct(reg, rt) + "const " & rt & "*" + return (fnType, replyParam) + +proc emitAbiReplyTypedefs( + lines: var seq[string], reg: var AbiReg, libType: string, methods: seq[FFIProcMeta] +) = + for m in methods: + let info = abiMethodReplyInfo(reg, libType, m) + lines.add( + "typedef void (*" & info.fnType & ")(int err_code, " & info.replyParam & + " reply, const char* err_msg, void* user_data);" + ) + +func abiScalarRawFnName(libType: string): string = + ## Raw-bytes callback typedef a scalar-fast-path export takes. + return libType & "ScalarRawFn" + +const abiScalarDupCStr = "nimffi_abi_dup_cstr_n" + +func abiScalarDupHelper(): seq[string] = + ## CBOR-free twin of `nimffi_dup_cstr_n`, include-guarded so two `abi = c` + ## headers can co-exist in one TU. + return @[ + "#ifndef NIMFFI_ABI_DUP_CSTR_N", + "#define NIMFFI_ABI_DUP_CSTR_N", + "/* NUL-terminated copy of a length-delimited byte run; NULL if it can't. */", + "static inline char* " & abiScalarDupCStr & "(const char* s, size_t n) {", + " if (n == SIZE_MAX) return NULL;", + " char* p = (char*)malloc(n + 1);", + " if (p) {", + " if (n > 0) memcpy(p, s, n);", + " p[n] = '\\0';", + " }", + " return p;", + "}", + "#endif", + ] + +func abiScalarArgParams(m: FFIProcMeta): seq[string] = + var params: seq[string] = @[] + for ep in m.extraParams: + params.add(abiLeafCType(ep.typeName.strip()).cType & " " & ep.name) + return params + +proc emitAbiExternDecls( + lines: var seq[string], + reg: var AbiReg, + libName, libType: string, + procs: seq[FFIProcMeta], +) = + let createRawFn = libType & "CreateRawFn" + var haveCtor, haveScalar = false + for p in procs: + if p.kind == FFIKind.CTOR: + haveCtor = true + if p.scalarFastPath: + haveScalar = true + if haveCtor: + lines.add( + "typedef void (*" & createRawFn & + ")(int err_code, const char* ctx_addr, const char* err_msg, void* user_data);" + ) + if haveScalar: + lines.add( + "/* Raw reply of a scalar-fast-path export: `msg`/`len` are bytes (a string" + ) + lines.add(" return's UTF-8, or the 8-byte native-endian scalar image), not") + lines.add(" NUL-terminated and valid only for the duration of the call. */") + lines.add( + "typedef void (*" & abiScalarRawFnName(libType) & + ")(int caller_ret, char* msg, size_t len, void* user_data);" + ) + for l in abiScalarDupHelper(): + lines.add(l) + lines.add("#ifdef __cplusplus") + lines.add("extern \"C\" {") + lines.add("#endif") + lines.add("") + for p in procs: + lines.add(renderBlockDocComment(p.doc)) + case p.kind + of FFIKind.FFI: + if p.scalarFastPath: + var params = + @["void* ctx", abiScalarRawFnName(libType) & " callback", "void* user_data"] + params.add(abiScalarArgParams(p)) + lines.add("int " & p.procName & "(" & params.join(", ") & ");") + else: + let info = abiMethodReplyInfo(reg, libType, p) + lines.add( + "int " & p.procName & "(void* ctx, " & info.fnType & + " on_reply, void* user_data, const " & reqStructName(p) & "* req);" + ) + of FFIKind.STATIC: + let info = abiMethodReplyInfo(reg, libType, p) + lines.add( + "int " & p.procName & "(" & info.fnType & " on_reply, void* user_data, const " & + reqStructName(p) & "* req);" + ) + of FFIKind.CTOR: + lines.add( + "void* " & p.procName & "(const " & reqStructName(p) & "* req, " & createRawFn & + " on_created, void* user_data);" + ) + of FFIKind.DTOR: + lines.add("int " & p.procName & "(void* ctx);") + lines.add("") + lines.add("#ifdef __cplusplus") + lines.add("} /* extern \"C\" */") + lines.add("#endif") + lines.add("") + +proc emitAbiCtxAndCtor( + lines: var seq[string], + reg: var AbiReg, + libName, libType, ctxType: string, + ctors: seq[FFIProcMeta], +) = + lines.add("typedef struct {") + lines.add(" void* ptr;") + lines.add("} " & ctxType & ";") + lines.add("") + if ctors.len == 0: + return + let createFn = libType & "CreateFn" + let createBox = libType & "CreateBox" + let createRawFn = libType & "CreateRawFn" + let tramp = libName & "_create_trampoline" + lines.add( + "typedef void (*" & createFn & ")(int err_code, " & ctxType & + "* ctx, const char* err_msg, void* user_data);" + ) + lines.add( + "typedef struct { " & createFn & " fn; void* user_data; } " & createBox & ";" + ) + lines.add( + "static void " & tramp & + "(int ret, const char* ctx_addr, const char* err_msg, void* ud) {" + ) + lines.add(" " & createBox & "* box = (" & createBox & "*)ud;") + lines.add(" if (!box) return;") + lines.add(" if (ret == NIMFFI_RET_STALE_WARN) return;") + lines.add(" if (!box->fn) { free(box); return; }") + lines.add(" if (ret != 0) {") + lines.add( + " box->fn(ret, NULL, err_msg ? err_msg : \"FFI create failed\", box->user_data);" + ) + lines.add(" free(box);") + lines.add(" return;") + lines.add(" }") + lines.add(" char* endp = NULL;") + lines.add(" unsigned long long a = ctx_addr ? strtoull(ctx_addr, &endp, 10) : 0;") + lines.add(" bool ok = ctx_addr && *ctx_addr && endp && *endp == '\\0';") + lines.add(" if (!ok) {") + lines.add( + " box->fn(-1, NULL, \"FFI create returned non-numeric address\", box->user_data);" + ) + lines.add(" free(box);") + lines.add(" return;") + lines.add(" }") + lines.add( + " " & ctxType & "* ctx = (" & ctxType & "*)calloc(1, sizeof(" & ctxType & "));" + ) + lines.add(" if (!ctx) {") + lines.add(" box->fn(-1, NULL, \"out of memory\", box->user_data);") + lines.add(" free(box);") + lines.add(" return;") + lines.add(" }") + lines.add(" ctx->ptr = (void*)(uintptr_t)a;") + lines.add(" box->fn(NIMFFI_RET_OK, ctx, NULL, box->user_data);") + lines.add(" free(box);") + lines.add("}") + lines.add("") + for ctor in ctors: + let reqStruct = reqStructName(ctor) + let (params, assigns) = abiReqParamsAndAssigns(reg, ctor.extraParams) + let head = "static inline int " & libName & "_ctx_create(" + let sig = + if params.len > 0: + head & params.join(", ") & ", " & createFn & " on_created, void* user_data) {" + else: + head & createFn & " on_created, void* user_data) {" + lines.add(renderBlockDocComment(ctor.doc)) + lines.add(sig) + lines.add(" " & reqStruct & " ffi_req;") + lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));") + for a in assigns: + lines.add(a) + lines.add( + " " & createBox & "* box = (" & createBox & "*)malloc(sizeof(" & createBox & + "));" + ) + lines.add(" if (!box) {") + lines.add( + " if (on_created) on_created(-1, NULL, \"out of memory\", user_data);" + ) + lines.add(" return -1;") + lines.add(" }") + lines.add(" box->fn = on_created;") + lines.add(" box->user_data = user_data;") + lines.add(" (void)" & ctor.procName & "(&ffi_req, " & tramp & ", box);") + lines.add(" return 0;") + lines.add("}") + lines.add("") + +proc emitAbiProcWrapper( + lines: var seq[string], + reg: var AbiReg, + ctxType, libName, libType: string, + m: FFIProcMeta, +) = + let isStatic = m.isStatic() + let stripped = stripLibPrefix(m.procName, m.libName) + let reqStruct = reqStructName(m) + let info = abiMethodReplyInfo(reg, libType, m) + let (params, assigns) = abiReqParamsAndAssigns(reg, m.extraParams) + let head = + if isStatic: + "static inline int " & libName & "_static_" & stripped & "(" + else: + "static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType & + "* ctx, " + let sig = + if params.len > 0: + head & params.join(", ") & ", " & info.fnType & " on_reply, void* user_data) {" + else: + head & info.fnType & " on_reply, void* user_data) {" + lines.add(renderBlockDocComment(m.doc)) + lines.add(sig) + lines.add(" " & reqStruct & " ffi_req;") + lines.add(" memset(&ffi_req, 0, sizeof(ffi_req));") + for a in assigns: + lines.add(a) + let ctxArg = if isStatic: "" else: "ctx->ptr, " + lines.add( + " return " & m.procName & "(" & ctxArg & "on_reply, user_data, &ffi_req);" + ) + lines.add("}") + lines.add("") + +func abiScalarOkLines(m: FFIProcMeta, fnType: string): seq[string] = + ## Trampoline RET_OK branch. A string return rides as its own UTF-8; every + ## other scalar is the 8-byte image `ffiRawRetBytes` packs (ints + ## sign-extended, floats widened to double, bool as 0/1). + let rt = m.returnTypeName.strip() + if rt == "string" or rt == "cstring": + return @[ + " char* reply = " & abiScalarDupCStr & "(msg ? msg : \"\", msg ? len : 0);", + " if (!reply) {", + " fn(NIMFFI_RET_ERR, \"\", \"out of memory\", user_data);", + " return;", " }", " fn(NIMFFI_RET_OK, reply, \"\", user_data);", + " free(reply);", + ] + var lines = @[ + " uint64_t slot = 0;", " if (!msg || len != sizeof(slot)) {", + " fn(NIMFFI_RET_ERR, NULL, \"scalar reply: unexpected payload size\", user_data);", + " return;", " }", " memcpy(&slot, msg, sizeof(slot));", + ] + let cType = abiLeafCType(rt).cType + case rt + of "int", "int64": + lines.add(" int64_t reply;") + lines.add(" memcpy(&reply, &slot, sizeof(reply));") + of "int8", "int16", "int32": + lines.add(" int64_t wide;") + lines.add(" memcpy(&wide, &slot, sizeof(wide));") + lines.add(" " & cType & " reply = (" & cType & ")wide;") + of "uint", "uint64": + lines.add(" uint64_t reply = slot;") + of "uint8", "uint16", "uint32", "byte": + lines.add(" " & cType & " reply = (" & cType & ")slot;") + of "bool": + lines.add(" bool reply = slot != 0;") + of "float", "float64": + lines.add(" double reply;") + lines.add(" memcpy(&reply, &slot, sizeof(reply));") + of "float32": + lines.add(" double wide;") + lines.add(" memcpy(&wide, &slot, sizeof(wide));") + lines.add(" float reply = (float)wide;") + else: + raise newException( + ValueError, "abi = c: unexpected scalar-fast-path return type: " & rt + ) + lines.add(" fn(NIMFFI_RET_OK, &reply, \"\", user_data);") + return lines + +proc emitAbiScalarMethod( + lines: var seq[string], + reg: var AbiReg, + ctxType, libName, libType: string, + m: FFIProcMeta, +) = + ## Args go inline to the raw export and a trampoline adapts the raw-bytes + ## reply into the typed `ReplyFn` surface. The trampoline frees the callback + ## box, relying on the dylib invoking it exactly once on every path. + let stripped = stripLibPrefix(m.procName, m.libName) + let pascal = snakeToPascalCase(stripped) + let info = abiMethodReplyInfo(reg, libType, m) + let boxType = libType & pascal & "ScalarBox" + let tramp = m.procName & "_scalar_reply" + let isStr = m.returnTypeName.strip() in ["string", "cstring"] + let errReply = if isStr: "\"\"" else: "NULL" + lines.add( + "typedef struct { " & info.fnType & " fn; void* user_data; } " & boxType & ";" + ) + lines.add( + "static void " & tramp & "(int caller_ret, char* msg, size_t len, void* ud) {" + ) + lines.add(" " & boxType & "* box = (" & boxType & "*)ud;") + lines.add(" if (!box) return;") + lines.add(" " & info.fnType & " fn = box->fn;") + lines.add(" void* user_data = box->user_data;") + lines.add(" free(box);") + lines.add(" if (!fn) return;") + lines.add(" if (caller_ret != NIMFFI_RET_OK) {") + lines.add( + " char* em = " & abiScalarDupCStr & "(msg ? msg : \"\", msg ? len : 0);" + ) + lines.add( + " fn(caller_ret, " & errReply & ", em ? em : \"FFI call failed\", user_data);" + ) + lines.add(" free(em);") + lines.add(" return;") + lines.add(" }") + for l in abiScalarOkLines(m, info.fnType): + lines.add(l) + lines.add("}") + lines.add("") + let params = abiScalarArgParams(m) + let head = + "static inline int " & libName & "_ctx_" & stripped & "(const " & ctxType & "* ctx, " + let sig = + if params.len > 0: + head & params.join(", ") & ", " & info.fnType & " on_reply, void* user_data) {" + else: + head & info.fnType & " on_reply, void* user_data) {" + lines.add(renderBlockDocComment(m.doc)) + lines.add(sig) + lines.add( + " " & boxType & "* box = (" & boxType & "*)malloc(sizeof(" & boxType & "));" + ) + lines.add(" if (!box) {") + lines.add( + " if (on_reply) on_reply(-1, " & errReply & ", \"out of memory\", user_data);" + ) + lines.add(" return -1;") + lines.add(" }") + lines.add(" box->fn = on_reply;") + lines.add(" box->user_data = user_data;") + var callArgs = @["ctx->ptr", tramp, "box"] + for ep in m.extraParams: + callArgs.add(ep.name) + lines.add(" return " & m.procName & "(" & callArgs.join(", ") & ");") + lines.add("}") + lines.add("") + +proc generateCAbiLibHeader*( + procs: seq[FFIProcMeta], + types: seq[FFITypeMeta], + libName: string, + events: seq[FFIEventMeta] = @[], + consts: seq[FFIConstMeta] = @[], +): string = + if events.len > 0: + raise newException( + ValueError, "abi = c: the C backend does not yet support {.ffiEvent.} listeners" + ) + let classified = classifyProcs(procs) + let libType = libTypeName(classified.ctors, libName) + let ctxType = libType & "Ctx" + + var reg = newAbiReg(types, procs) + for t in types: + ensureAbiStruct(reg, t.name) + for p in procs: + if p.kind != FFIKind.DTOR and not p.scalarFastPath: + ensureAbiStruct(reg, reqStructName(p)) + + let guard = "NIM_FFI_LIB_" & libName.toUpperAscii() & "_C_ABI_H_INCLUDED" + var lines: seq[string] = @[] + lines.add("#ifndef " & guard) + lines.add("#define " & guard) + lines.add("#include ") + lines.add("#include ") + lines.add("#include ") + lines.add("#include ") + lines.add("#include ") + lines.add("") + lines.add("#define NIMFFI_RET_OK 0") + lines.add("#define NIMFFI_RET_ERR 1") + lines.add("#define NIMFFI_RET_MISSING_CALLBACK 2") + lines.add("/* Non-terminal: the request is still running. Fires every ~5s with `msg`") + lines.add( + " carrying the elapsed milliseconds as decimal text; always followed by a" + ) + lines.add(" terminal RET_OK/RET_ERR. Ignore it unless you want progress. */") + lines.add("#define NIMFFI_RET_STALE_WARN 3") + lines.add("") + lines.add(constDeclLines(consts)) + lines.add( + "/* `abi = c` wire structs — the C ABI. Strings are borrowed, NUL-terminated" + ) + lines.add(" `const char*` valid only for the duration of the call they cross. */") + for decl in reg.decls: + lines.add(decl) + lines.add("") + + emitAbiReplyTypedefs(lines, reg, libType, classified.replyProcs()) + lines.add("") + emitAbiExternDecls(lines, reg, libName, libType, procs) + + lines.add("/* High-level context wrapper */") + emitAbiCtxAndCtor(lines, reg, libName, libType, ctxType, classified.ctors) + # abi = c has no events, so the destructor is the CBOR one minus the listener sweep. + emitDestructor(lines, ctxType, libName, classified.dtor, @[]) + # A static is never scalar-fast-path (`isScalarOnly` gates on FFIKind.FFI). + for m in classified.replyProcs(): + if m.scalarFastPath: + emitAbiScalarMethod(lines, reg, ctxType, libName, libType, m) + else: + emitAbiProcWrapper(lines, reg, ctxType, libName, libType, m) + + lines.add("#endif /* " & guard & " */") + return lines.join("\n") & "\n" + +proc generateCAbiCMakeLists*(libName, nimSrcRelPath: string): string = + let src = nimSrcRelPath.replace("\\", "/") + return AbiCMakeListsTpl.multiReplace(("{{LIB}}", libName), ("{{SRC}}", src)) + +func libWireFormat(procs: seq[FFIProcMeta], types: seq[FFITypeMeta]): ABIFormat = + ## The single wire format the C header targets (no mixing in one header). + var seen: set[ABIFormat] = {} + for p in procs: + if p.kind != FFIKind.DTOR: + seen.incl(p.abiFormat) + if seen.len == 0: + for t in types: + seen.incl(t.abiFormat) + if seen.len > 1: + raise newException( + ValueError, + "abi = c/cbor mismatch: a C library must use one ABI format for all its " & + "procs and types; a mixed header is not supported", + ) + return (if ABIFormat.C in seen: ABIFormat.C else: ABIFormat.Cbor) + +proc generateCBindings*( + procs: seq[FFIProcMeta], + types: seq[FFITypeMeta], + libName: string, + outputDir: string, + nimSrcRelPath: string, + events: seq[FFIEventMeta] = @[], + consts: seq[FFIConstMeta] = @[], +) = + ## Emits the C binding for `libName`, picking the `abi = c` or CBOR shape. + createDir(outputDir) + case libWireFormat(procs, types) + of ABIFormat.C: + writeFile( + outputDir / (libName & ".h"), + generateCAbiLibHeader(procs, types, libName, events, consts), + ) + writeFile( + outputDir / "CMakeLists.txt", generateCAbiCMakeLists(libName, nimSrcRelPath) + ) + of ABIFormat.Cbor: + writeFile(outputDir / PreludeHeaderName, generateCPreludeHeader()) + writeFile(outputDir / CborHeaderName, generateCCborHeader()) + writeFile( + outputDir / (libName & ".h"), + generateCLibHeader(procs, types, libName, events, consts), + ) + writeFile(outputDir / "CMakeLists.txt", generateCCMakeLists(libName, nimSrcRelPath)) diff --git a/wasm-deps/ffi/ffi/codegen/c_cpp_common.nim b/wasm-deps/ffi/ffi/codegen/c_cpp_common.nim new file mode 100644 index 000000000..3a26c8e31 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/c_cpp_common.nim @@ -0,0 +1,25 @@ +## Helpers shared by the C/C++ binding generators (cpp.nim, c.nim). + +import std/strutils +import ./meta, ./string_helpers + +proc stripLibPrefix*(procName, libName: string): string = + ## Drops the `_` prefix from an exported C symbol. + let prefix = libName & "_" + if procName.startsWith(prefix): + return procName[prefix.len .. ^1] + return procName + +proc reqStructName*(p: FFIProcMeta): string = + ## Per-proc wire envelope name: `Req` (`...CtorReq` for ctors). + let camel = snakeToPascalCase(p.procName) + if p.kind == FFIKind.CTOR: + camel & "CtorReq" + else: + camel & "Req" + +proc libTypeName*(ctors: seq[FFIProcMeta], libName: string): string = + ## The library type name, from the first ctor or derived from `libName`. + if ctors.len > 0: + return ctors[0].libTypeName + capitalizeFirstLetter(libName) diff --git a/wasm-deps/ffi/ffi/codegen/cddl.nim b/wasm-deps/ffi/ffi/codegen/cddl.nim new file mode 100644 index 000000000..c05f87038 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/cddl.nim @@ -0,0 +1,190 @@ +## CDDL (RFC 8610) schema generator mirroring the CBOR wire format from +## ffi/cbor_serial.nim: types become rules, procs get request/response rules. + +import std/[os, strutils, unicode] +import ./meta, ./string_helpers + +proc innerOf(typeName, prefix: string): string = + if typeName.startsWith(prefix) and typeName.endsWith("]"): + return typeName[prefix.len .. ^2] + return "" + +proc capitalizeFirstLetter(s: string): string = + if s.len == 0: + return s + return s.capitalize() + +proc toCamelCase(s: string): string = + ## "testlib_create" → "TestlibCreate" + var parts = s.split('_') + var res = "" + for p in parts: + res.add capitalizeFirstLetter(p) + return res + +proc nimTypeToCddl*(typeName: string): string = + ## Nim type name → CDDL equivalent; unknown names pass through as rule refs. + let t = typeName.strip() + let seqI = innerOf(t, "seq[") + if seqI.len > 0: + let inner = seqI.strip() + if inner == "byte" or inner == "uint8": + # seq[byte] rides the wire as a CBOR byte string. + return "bytes" + return "[* " & nimTypeToCddl(inner) & "]" + let arrI = innerOf(t, "array[") + if arrI.len > 0: + # Emit an unbounded array of the element type (CDDL lacks a fixed-length literal). + let commaIdx = arrI.find(',') + let elemT = + if commaIdx >= 0: + arrI[commaIdx + 1 .. ^1].strip() + else: + arrI + return "[* " & nimTypeToCddl(elemT) & "]" + let optI = innerOf(t, "Option[") + if optI.len > 0: + return nimTypeToCddl(optI) & " / nil" + let mayI = innerOf(t, "Maybe[") + if mayI.len > 0: + return nimTypeToCddl(mayI) & " / nil" + case t + of "bool": "bool" + of "int", "int64", "int32", "int16", "int8": "int" + of "uint", "uint64", "uint32", "uint16", "uint8", "byte": "uint" + of "string", "cstring": "tstr" + of "float", "float64": "float64" + of "float32": "float32" + of "pointer": "uint" + else: t + +proc reqStructName(p: FFIProcMeta): string = + ## Mirrors the Nim macro: {Ctor}Req. + let camel = toCamelCase(p.procName) + if p.kind == FFIKind.CTOR: + camel & "CtorReq" + else: + camel & "Req" + +proc emitMap( + fields: openArray[tuple[name: string, typeName: string, isPtr: bool]] +): string = + if fields.len == 0: + return "{ }" + var parts: seq[string] = @[] + for f in fields: + let cddlType = + if f.isPtr: + "uint" + else: + nimTypeToCddl(f.typeName) + parts.add(f.name & ": " & cddlType) + "{ " & parts.join(", ") & " }" + +proc emitEnumAlternatives(t: FFITypeMeta): string = + ## An enum rides as the CBOR text `$value` yields, so the rule is a choice of + ## string literals. + var alts: seq[string] = @[] + for v in t.enumValues: + alts.add("\"" & v.wire & "\"") + alts.join(" / ") + +proc emitObjectFields(t: FFITypeMeta): string = + var fields: seq[tuple[name: string, typeName: string, isPtr: bool]] = @[] + for f in t.fields: + fields.add((name: f.name, typeName: f.typeName, isPtr: false)) + emitMap(fields) + +proc emitReqFields(p: FFIProcMeta): string = + var fields: seq[tuple[name: string, typeName: string, isPtr: bool]] = @[] + for ep in p.extraParams: + fields.add((name: ep.name, typeName: ep.typeName, isPtr: ep.ridesAsPtr())) + emitMap(fields) + +proc responseRule(p: FFIProcMeta): string = + ## CDDL shape of the success payload; error payloads are raw UTF-8, absent here. + case p.kind + of FFIKind.CTOR: + # Ctor returns the FFI context address as a CBOR decimal string. + "tstr" + of FFIKind.DTOR: + # Dtor payload is a CBOR null sentinel. + "nil" + of FFIKind.FFI, FFIKind.STATIC: + if p.returnRidesAsPtr(): + "uint" + else: + nimTypeToCddl(p.returnTypeName) + +proc generateCddlSchema*( + procs: seq[FFIProcMeta], + types: seq[FFITypeMeta], + libName: string, + nimSrcRelPath: string, +): string = + var L: seq[string] = @[] + L.add("; CDDL schema for `" & libName & "` — auto-generated from " & nimSrcRelPath) + L.add("; Wire format: CBOR (RFC 8949). Errors return raw UTF-8 (not CBOR) and") + L.add("; are intentionally absent from this schema.") + L.add("") + + if types.len > 0: + L.add( + "; ─── User-declared FFI types ──────────────────────────────────────" + ) + for t in types: + let rule = + if t.isEnum(): + emitEnumAlternatives(t) + else: + emitObjectFields(t) + L.add(t.name & " = " & rule) + L.add("") + + # Per-proc request envelopes (one CBOR blob per request). + let nonDtor = block: + var r: seq[FFIProcMeta] = @[] + for p in procs: + if p.kind != FFIKind.DTOR: + r.add(p) + r + if nonDtor.len > 0: + L.add( + "; ─── Request envelopes (one CBOR blob per request) ────────────────" + ) + for p in nonDtor: + L.add(reqStructName(p) & " = " & emitReqFields(p)) + L.add("") + + # Per-proc request/response rules. + L.add( + "; ─── Procs ─────────────────────────────────────────────────────────" + ) + for p in procs: + let kindTag = + case p.kind + of FFIKind.CTOR: "ctor" + of FFIKind.DTOR: "dtor" + of FFIKind.FFI: "ffi" + of FFIKind.STATIC: "ffiStatic" + L.add("; " & p.procName & " (" & kindTag & ")") + L.add(renderDocComment(p.doc, "", "; ")) + if p.kind != FFIKind.DTOR: + L.add(p.procName & "-request = " & reqStructName(p)) + L.add(p.procName & "-response = " & responseRule(p)) + L.add("") + + return L.join("\n") + +proc generateCddlBindings*( + procs: seq[FFIProcMeta], + types: seq[FFITypeMeta], + libName: string, + outputDir: string, + nimSrcRelPath: string, +) = + createDir(outputDir) + writeFile( + outputDir / (libName & ".cddl"), + generateCddlSchema(procs, types, libName, nimSrcRelPath), + ) diff --git a/wasm-deps/ffi/ffi/codegen/consts.nim b/wasm-deps/ffi/ffi/codegen/consts.nim new file mode 100644 index 000000000..713ed9fc7 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/consts.nim @@ -0,0 +1,69 @@ +## Literal rendering for `{.ffiConst.}` values, shared by the C/C++/Rust generators. +## The registry stores Nim's `$value`; each backend re-quotes it for its syntax. + +import std/strutils +import ./types_ir + +func cByteEscape(ch: char): string = + ## 3-digit octal: C caps an octal escape at 3 digits, so a following digit + ## can't be swallowed into it the way it can with `\x`. + return "\\" & toOct(ord(ch), 3) + +func rustByteEscape(ch: char): string = + return "\\x" & toHex(ord(ch), 2) + +func escapeLit( + s: string, byteEscape: proc(ch: char): string {.noSideEffect, nimcall.} +): string = + var escaped = "" + for ch in s: + case ch + of '"': + escaped.add("\\\"") + of '\\': + escaped.add("\\\\") + of '\n': + escaped.add("\\n") + of '\r': + escaped.add("\\r") + of '\t': + escaped.add("\\t") + else: + if ch < ' ' or ch == '\x7F': + escaped.add(byteEscape(ch)) + else: + escaped.add(ch) + return escaped + +func cEscapeStringLit*(s: string): string = + return escapeLit(s, cByteEscape) + +func rustEscapeStringLit*(s: string): string = + return escapeLit(s, rustByteEscape) + +func cConstValue*(t: FFIType, value: string): string = + ## C/C++ literal. Every emission site is a typed declaration, so the declared + ## type already fixes the width; only the two cases the type can't rescue get + ## a suffix — `ULL` because a decimal above `INT64_MAX` fits no signed type, + ## and `f` because a bare `1.5` is a double and narrowing it warns. + case t.kind + of ftStr: + return "\"" & cEscapeStringLit(value) & "\"" + of ftScalar: + case t.scalar + of skU64: + return value & "ULL" + of skF32: + return value & "f" + else: + return value + else: + return value + +func rustConstValue*(t: FFIType, value: string): string = + ## Rust literal; the declared type annotation carries the width, so no suffix. + case t.kind + of ftStr: + return "\"" & rustEscapeStringLit(value) & "\"" + else: + return value diff --git a/wasm-deps/ffi/ffi/codegen/cpp.nim b/wasm-deps/ffi/ffi/codegen/cpp.nim new file mode 100644 index 000000000..b806c6887 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/cpp.nim @@ -0,0 +1,574 @@ +## C++ binding generator: header-only binding + CMakeLists, CBOR over the wire. + +import std/[os, strutils] +import ./meta, ./string_helpers, ./c_cpp_common, ./types_ir, ./consts + +## Fixed 64-bit wire type for any Nim `ptr T` / `pointer`. +const CppPtrType* = "uint64_t" + +## Trailing param of every call that can't inherit a ctx's `timeout_`. +const CppTimeoutParam = "std::chrono::milliseconds timeout = std::chrono::seconds{30}" + +const + HeaderPreludeTpl = staticRead("templates/cpp/header_prelude.hpp.tpl") + ResultTpl = staticRead("templates/cpp/result.hpp.tpl") + CborHelpersTpl = staticRead("templates/cpp/cbor_helpers.hpp.tpl") + SyncCallHelperTpl = staticRead("templates/cpp/sync_call_helper.hpp.tpl") + ContextRuleOf5Tpl = staticRead("templates/cpp/context_rule_of_5.hpp.tpl") + CMakeListsTpl = staticRead("templates/cpp/CMakeLists.txt.tpl") + +func cppScalar(s: ScalarKind): string = + case s + of skBool: "bool" + of skI8: "int8_t" + of skI16: "int16_t" + of skI32: "int32_t" + of skI64: "int64_t" + of skU8: "uint8_t" + of skU16: "uint16_t" + of skU32: "uint32_t" + of skU64: "uint64_t" + of skF32: "float" + of skF64: "double" + +func cppSeq(elem: string): string = + "std::vector<" & elem & ">" + +func cppOpt(elem: string): string = + "std::optional<" & elem & ">" + +const cppMap = NativeTypeMap( + scalar: cppScalar, + str: "std::string", + bytes: "std::vector", + ptrType: CppPtrType, + seqOf: cppSeq, + optOf: cppOpt, +) ## structName omitted: C++ uses the user type name verbatim + +proc nimTypeToCpp*(typeName: string): string = + renderNative(cppMap, parseFFIType(typeName)) + +proc emitEnumCborCodec(lines: var seq[string], t: FFITypeMeta) = + ## Appends the `enum class` plus its TinyCBOR codec pair. The wire form is the + ## CBOR text `$value` yields on the Nim side, so the codec maps name ↔ value. + lines.add("enum class $1 {" % [t.name]) + for v in t.enumValues: + lines.add(" $1 = $2," % [v.name, $v.ord]) + lines.add("};") + + lines.add("inline CborError encode_cbor(CborEncoder& e, const $1& v) {" % [t.name]) + lines.add(" switch (v) {") + for v in t.enumValues: + lines.add( + " case $1::$2: return cbor_encode_text_stringz(&e, \"$3\");" % + [t.name, v.name, v.wire] + ) + lines.add(" }") + lines.add(" return CborErrorImproperValue;") + lines.add("}") + + lines.add("inline CborError decode_cbor(CborValue& it, $1& v) {" % [t.name]) + lines.add(" std::string name;") + lines.add(" CborError err = decode_cbor(it, name);") + lines.add(" if (err) return err;") + for v in t.enumValues: + lines.add( + " if (name == \"$1\") { v = $2::$3; return CborNoError; }" % + [v.wire, t.name, v.name] + ) + lines.add(" return CborErrorImproperValue;") + lines.add("}") + lines.add("") + +proc emitStructCborCodec( + lines: var seq[string], structName: string, fields: seq[(string, string)] +) = + ## Appends per-struct TinyCBOR encode_cbor + decode_cbor functions emitting a + ## text-keyed CBOR map. The C++ type in `fields` is unused (overloads dispatch). + let n = fields.len + if n == 0: + lines.add( + "inline CborError encode_cbor(CborEncoder& e, const $1&) {" % [structName] + ) + else: + lines.add( + "inline CborError encode_cbor(CborEncoder& e, const $1& v) {" % [structName] + ) + lines.add(" CborEncoder m;") + lines.add(" CborError err = cbor_encoder_create_map(&e, &m, $1);" % [$n]) + lines.add(" if (err) return err;") + for (name, _) in fields: + lines.add( + " err = cbor_encode_text_stringz(&m, \"$1\"); if (err) return err;" % [name] + ) + lines.add( + " err = encode_cbor(m, v.$1); if (err) return err;" % [name] + ) + lines.add(" return cbor_encoder_close_container(&e, &m);") + lines.add("}") + if n == 0: + lines.add("inline CborError decode_cbor(CborValue& it, $1&) {" % [structName]) + lines.add(" if (!cbor_value_is_map(&it)) return CborErrorImproperValue;") + lines.add(" return cbor_value_advance(&it);") + lines.add("}") + return + lines.add("inline CborError decode_cbor(CborValue& it, $1& v) {" % [structName]) + lines.add(" if (!cbor_value_is_map(&it)) return CborErrorImproperValue;") + lines.add(" CborValue field;") + lines.add(" CborError err;") + for (name, _) in fields: + lines.add( + " err = cbor_value_map_find_value(&it, \"$1\", &field); if (err) return err;" % + [name] + ) + lines.add(" if (!cbor_value_is_valid(&field)) return CborErrorImproperValue;") + lines.add(" err = decode_cbor(field, v.$1); if (err) return err;" % [name]) + lines.add(" return cbor_value_advance(&it);") + lines.add("}") + +proc cppBracedInit(structName: string, fieldNames: seq[string]): string = + ## C++ braced-init for a Req struct, e.g. `TimerEchoReq{message, count}`. + return structName & "{" & fieldNames.join(", ") & "}" + +proc emitEventDispatcher( + lines: var seq[string], ctxTypeName, libName: string, events: seq[FFIEventMeta] +) = + ## Emits the public per-event `addOnListener` / `removeEventListener` API. + ## Callables are owned by `listeners_` (unique_ptr keyed by id); the raw + ## pointer is the dylib's `user_data`, stable until removal. + if events.len == 0: + return + lines.add( + " // ── Event listener API ──────────────────────────────────" + ) + lines.add(" struct ListenerHandle { std::uint64_t id = 0; };") + lines.add("") + for ev in events: + let methodName = + "addOn" & capitalizeFirstLetter(ev.nimProcName).substr(2) & "Listener" + lines.add(renderMemberDocComment(ev.doc)) + lines.add( + " ListenerHandle $1(std::function handler) {" % + [methodName, ev.payloadTypeName] + ) + lines.add( + " auto owned = std::make_unique>(std::move(handler));" % + [ev.payloadTypeName] + ) + lines.add(" auto* raw = owned.get();") + lines.add(" const auto id = $1_add_event_listener(" % [libName]) + lines.add( + " ptr_, \"$1\", &$2::typedTrampoline<$3>, raw);" % + [ev.wireName, ctxTypeName, ev.payloadTypeName] + ) + lines.add(" if (id == 0) return ListenerHandle{0};") + lines.add(" listeners_.emplace(id, std::move(owned));") + lines.add(" return ListenerHandle{id};") + lines.add(" }") + lines.add("") + lines.add(" bool removeEventListener(ListenerHandle handle) {") + lines.add(" if (handle.id == 0) return false;") + lines.add( + " const auto rc = $1_remove_event_listener(ptr_, handle.id);" % [libName] + ) + lines.add(" listeners_.erase(handle.id);") + lines.add(" return rc == 0;") + lines.add(" }") + lines.add("") + +proc emitEventTrampoline(lines: var seq[string], events: seq[FFIEventMeta]) = + ## Private listener machinery for `emitEventDispatcher`: polymorphic + ## `ListenerBase`, `TypedListener` and the `typedTrampoline` decoder. + if events.len == 0: + return + lines.add(" struct ListenerBase {") + lines.add(" virtual ~ListenerBase() = default;") + lines.add(" };") + lines.add("") + lines.add(" template ") + lines.add(" struct TypedListener : ListenerBase {") + lines.add(" std::function fn;") + lines.add( + " explicit TypedListener(std::function f) : fn(std::move(f)) {}" + ) + lines.add(" };") + lines.add("") + lines.add(" template ") + lines.add( + " static void typedTrampoline(int ret, const char* msg, std::size_t len, void* ud) {" + ) + lines.add(" if (!ud || ret != 0 || !msg || len == 0) return;") + lines.add(" auto* listener = static_cast*>(ud);") + lines.add(" if (!listener->fn) return;") + lines.add(" CborParser parser; CborValue it;") + lines.add( + " if (cbor_parser_init(reinterpret_cast(msg), len, 0, &parser, &it) != CborNoError) return;" + ) + lines.add(" if (!cbor_value_is_map(&it)) return;") + lines.add(" CborValue payloadField;") + lines.add( + " if (cbor_value_map_find_value(&it, \"payload\", &payloadField) != CborNoError) return;" + ) + lines.add(" T payload{};") + lines.add(" if (decode_cbor(payloadField, payload) != CborNoError) return;") + lines.add(" listener->fn(payload);") + lines.add(" }") + lines.add("") + +proc generateCppHeader*( + procs: seq[FFIProcMeta], + types: seq[FFITypeMeta], + libName: string, + events: seq[FFIEventMeta] = @[], + consts: seq[FFIConstMeta] = @[], +): string = + var lines: seq[string] = @[] + + lines.add(HeaderPreludeTpl) + if events.len > 0: + lines.add("#include ") + + lines.add(ResultTpl) + + # Generic CBOR overloads must precede the non-template struct codecs that call them (parse-time name lookup). + lines.add(CborHelpersTpl) + + if consts.len > 0: + lines.add("// ============================================================") + lines.add("// Generated constants") + lines.add("// ============================================================") + lines.add("") + for c in consts: + let t = parseFFIType(c.typeName) + # A string const is a `const char*`, not std::string: constexpr can't own a heap value. + let cppType = + if t.kind == ftStr: + "const char*" + else: + nimTypeToCpp(c.typeName) + lines.add( + "constexpr $1 $2 = $3;" % + [cppType, identToUpperSnake(c.name), cConstValue(t, c.value)] + ) + lines.add("") + + # Enums first: a struct codec that takes one must see its overload already declared. + var structTypes: seq[FFITypeMeta] = @[] + for t in types: + if t.isEnum(): + emitEnumCborCodec(lines, t) + else: + structTypes.add(t) + + if structTypes.len > 0: + lines.add("// ============================================================") + lines.add("// User-declared FFI types") + lines.add("// ============================================================") + lines.add("") + for t in structTypes: + lines.add("struct $1 {" % [t.name]) + for f in t.fields: + lines.add(" $1 $2;" % [nimTypeToCpp(f.typeName), f.name]) + lines.add("};") + var fields: seq[(string, string)] = @[] + for f in t.fields: + fields.add((f.name, nimTypeToCpp(f.typeName))) + emitStructCborCodec(lines, t.name, fields) + lines.add("") + + lines.add("// ============================================================") + lines.add("// Per-proc request envelopes (CBOR encoded on the wire)") + lines.add("// ============================================================") + lines.add("") + for p in procs: + if p.kind == FFIKind.DTOR: + continue + let reqName = reqStructName(p) + lines.add("struct $1 {" % [reqName]) + for ep in p.extraParams: + let cppType = + if ep.ridesAsPtr(): + CppPtrType + else: + nimTypeToCpp(ep.typeName) + lines.add(" $1 $2;" % [cppType, ep.name]) + lines.add("};") + var fields: seq[(string, string)] = @[] + for ep in p.extraParams: + let cppType = + if ep.ridesAsPtr(): + CppPtrType + else: + nimTypeToCpp(ep.typeName) + fields.add((ep.name, cppType)) + emitStructCborCodec(lines, reqName, fields) + lines.add("") + + lines.add("// ============================================================") + lines.add("// C FFI declarations") + lines.add("// ============================================================") + lines.add("") + lines.add("extern \"C\" {") + lines.add( + "typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data);" + ) + lines.add("") + for p in procs: + lines.add(renderBlockDocComment(p.doc)) + case p.kind + of FFIKind.FFI: + lines.add( + "int $1(void* ctx, FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);" % + [p.procName] + ) + of FFIKind.STATIC: + lines.add( + "int $1(FFICallback callback, void* user_data, const uint8_t* req_cbor, size_t req_cbor_len);" % + [p.procName] + ) + of FFIKind.CTOR: + lines.add( + "void* $1(const uint8_t* req_cbor, size_t req_cbor_len, FFICallback callback, void* user_data);" % + [p.procName] + ) + of FFIKind.DTOR: + lines.add("int $1(void* ctx);" % [p.procName]) + # Listener-registration ABI is always exported. + lines.add( + "uint64_t $1_add_event_listener(void* ctx, const char* event_name, FFICallback callback, void* user_data);" % + [libName] + ) + lines.add( + "int $1_remove_event_listener(void* ctx, uint64_t listener_id);" % [libName] + ) + lines.add("} // extern \"C\"") + lines.add("") + + lines.add(SyncCallHelperTpl) + + let classified = classifyProcs(procs) + let ctors = classified.ctors + let ctxTypeName = libTypeName(ctors, libName) & "Ctx" + + lines.add("// ============================================================") + lines.add("// High-level C++ context class") + lines.add("// ============================================================") + lines.add("") + lines.add("class $1 {" % [ctxTypeName]) + lines.add("public:") + + for ctor in ctors: + let reqName = reqStructName(ctor) + var ctorParams: seq[string] = @[] + var epNames: seq[string] = @[] + for ep in ctor.extraParams: + let cppType = + if ep.ridesAsPtr(): + CppPtrType + else: + nimTypeToCpp(ep.typeName) + ctorParams.add("const $1& $2" % [cppType, ep.name]) + epNames.add(ep.name) + let ctorParamsWithTimeout = + if ctorParams.len > 0: + ctorParams.join(", ") & ", " & CppTimeoutParam + else: + CppTimeoutParam + + let reqInit = cppBracedInit(reqName, epNames) + + # `create` yields the ctx via the callback's CBOR address (sync void* return discarded), owned as a unique_ptr since the class forbids copy/move. + let createRet = "Result>" % [ctxTypeName] + lines.add(renderMemberDocComment(ctor.doc)) + lines.add(" static $1 create($2) {" % [createRet, ctorParamsWithTimeout]) + lines.add(" const auto ffi_req_ = $1;" % [reqInit]) + lines.add(" auto ffi_enc_ = encodeCborFFI(ffi_req_);") + lines.add( + " if (ffi_enc_.isErr()) return $1::err(ffi_enc_.error());" % [createRet] + ) + lines.add(" const auto& ffi_req_bytes_ = ffi_enc_.value();") + lines.add(" auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {") + lines.add( + " (void)$1(ffi_req_bytes_.data(), ffi_req_bytes_.size(), cb, ud);" % + [ctor.procName] + ) + lines.add(" return 0;") + lines.add(" }, timeout);") + lines.add( + " if (ffi_raw_.isErr()) return $1::err(ffi_raw_.error());" % [createRet] + ) + lines.add(" auto ffi_addr_ = decodeCborFFI(ffi_raw_.value());") + lines.add( + " if (ffi_addr_.isErr()) return $1::err(ffi_addr_.error());" % [createRet] + ) + lines.add(" const auto& addr_str = ffi_addr_.value();") + # from_chars (not stoull) so a bad payload is an err() Result, not a throw. + lines.add(" std::uint64_t addr = 0;") + lines.add(" const char* addr_begin = addr_str.data();") + lines.add(" const char* addr_end = addr_begin + addr_str.size();") + lines.add(" const auto fc_ = std::from_chars(addr_begin, addr_end, addr);") + lines.add(" if (fc_.ec != std::errc() || fc_.ptr != addr_end) {") + lines.add( + " return $1::err(\"FFI create returned non-numeric address: \" + addr_str);" % + [createRet] + ) + lines.add(" }") + # `new` (not make_unique) so the ctor can stay private. + lines.add( + " return $1::ok(std::unique_ptr<$2>(new $2(reinterpret_cast(static_cast(addr)), timeout)));" % + [createRet, ctxTypeName] + ) + lines.add(" }") + lines.add("") + + let captureList = + if epNames.len > 0: + epNames.join(", ") & ", timeout" + else: + "timeout" + let callList = + if epNames.len > 0: + epNames.join(", ") & ", timeout" + else: + "timeout" + lines.add(renderMemberDocComment(ctor.doc)) + lines.add( + " static std::future>> createAsync($2) {" % + [ctxTypeName, ctorParamsWithTimeout] + ) + lines.add( + " return std::async(std::launch::async, [$1]() { return create($2); });" % + [captureList, callList] + ) + lines.add(" }") + lines.add("") + + lines.add( + ContextRuleOf5Tpl.multiReplace(("{{CTX}}", ctxTypeName), ("{{LIB}}", libName)) + ) + + emitEventDispatcher(lines, ctxTypeName, libName, events) + + # A static has no ctx to inherit `timeout_` from, so it takes its own `timeout`. + for m in classified.replyProcs(): + let isStatic = m.isStatic() + let methodName = stripLibPrefix(m.procName, libName) + let retCppType = + if m.returnRidesAsPtr(): + CppPtrType + else: + nimTypeToCpp(m.returnTypeName) + let reqName = reqStructName(m) + + var methParams: seq[string] = @[] + var methParamNames: seq[string] = @[] + for ep in m.extraParams: + let cppType = + if ep.ridesAsPtr(): + CppPtrType + else: + nimTypeToCpp(ep.typeName) + methParams.add("const $1& $2" % [cppType, ep.name]) + methParamNames.add(ep.name) + let methParamNamesStr = methParamNames.join(", ") + let methParamsStr = + if not isStatic: + methParams.join(", ") + elif methParams.len > 0: + methParams.join(", ") & ", " & CppTimeoutParam + else: + CppTimeoutParam + + let reqInit = cppBracedInit(reqName, methParamNames) + + let methRet = "Result<$1>" % [retCppType] + lines.add(renderMemberDocComment(m.doc)) + let decl = if isStatic: " static $1 $2($3) {" else: " $1 $2($3) const {" + lines.add(decl % [methRet, methodName, methParamsStr]) + lines.add(" const auto ffi_req_ = $1;" % [reqInit]) + lines.add(" auto ffi_enc_ = encodeCborFFI(ffi_req_);") + lines.add( + " if (ffi_enc_.isErr()) return $1::err(ffi_enc_.error());" % [methRet] + ) + lines.add(" const auto& ffi_req_bytes_ = ffi_enc_.value();") + lines.add(" auto ffi_raw_ = ffi_call_([&](FFICallback cb, void* ud) {") + let ctxArg = if isStatic: "" else: "ptr_, " + lines.add( + " return $1($2cb, ud, ffi_req_bytes_.data(), ffi_req_bytes_.size());" % + [m.procName, ctxArg] + ) + lines.add(" }, $1);" % [if isStatic: "timeout" else: "timeout_"]) + lines.add( + " if (ffi_raw_.isErr()) return $1::err(ffi_raw_.error());" % [methRet] + ) + lines.add(" return decodeCborFFI<$1>(ffi_raw_.value());" % [retCppType]) + lines.add(" }") + lines.add("") + + # A method calls `this->methodName(...)` so a same-named param can't shadow + # the call target; a static has no `this` and forwards its own `timeout`. + let staticArgs = + if methParamNames.len > 0: + methParamNamesStr & ", timeout" + else: + "timeout" + let asyncArgs = if isStatic: staticArgs else: methParamNamesStr + let asyncCapture = + if isStatic: + staticArgs + elif methParamNamesStr.len > 0: + "this, " & methParamNamesStr + else: + "this" + let asyncDecl = + if isStatic: + " static std::future<$1> $2Async($3) {" + else: + " std::future<$1> $2Async($3) const {" + lines.add(renderMemberDocComment(m.doc)) + lines.add(asyncDecl % [methRet, methodName, methParamsStr]) + lines.add( + " return std::async(std::launch::async, [$1]() { return $2$3($4); });" % + [asyncCapture, (if isStatic: "" else: "this->"), methodName, asyncArgs] + ) + lines.add(" }") + lines.add("") + + lines.add("private:") + # Listener machinery must precede the `listeners_` member (its value type must be complete at declaration). + emitEventTrampoline(lines, events) + lines.add(" void* ptr_;") + lines.add(" std::chrono::milliseconds timeout_;") + if events.len > 0: + lines.add( + " std::unordered_map> listeners_;" + ) + lines.add( + " explicit $1(void* p, std::chrono::milliseconds t) : ptr_(p), timeout_(t) {}" % + [ctxTypeName] + ) + lines.add("};") + lines.add("") + + return lines.join("\n") + +proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string = + let src = nimSrcRelPath.replace("\\", "/") + return CMakeListsTpl.multiReplace(("{{LIB}}", libName), ("{{SRC}}", src)) + +proc generateCppBindings*( + procs: seq[FFIProcMeta], + types: seq[FFITypeMeta], + libName: string, + outputDir: string, + nimSrcRelPath: string, + events: seq[FFIEventMeta] = @[], + consts: seq[FFIConstMeta] = @[], +) = + createDir(outputDir) + writeFile( + outputDir / (libName & ".hpp"), + generateCppHeader(procs, types, libName, events, consts), + ) + writeFile(outputDir / "CMakeLists.txt", generateCppCMakeLists(libName, nimSrcRelPath)) diff --git a/wasm-deps/ffi/ffi/codegen/meta.nim b/wasm-deps/ffi/ffi/codegen/meta.nim new file mode 100644 index 000000000..a42154704 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/meta.nim @@ -0,0 +1,204 @@ +## Compile-time metadata types for FFI binding generation, populated by the +## {.ffiCtor.}/{.ffi.} macros and consumed by codegen. + +import std/[strutils, options] + +type + ABIFormat* {.pure.} = enum + ## FFI payload wire format. `Cbor` is wired end-to-end; `C` has a type codec + ## but no proc-dispatch path yet. + Cbor = "cbor" + C = "c" + + FFIParamMeta* = object + name*: string + typeName*: string + isPtr*: bool + isHandle*: bool # {.ffiHandle.} type, wire form uint64 + + FFIKind* {.pure.} = enum + FFI + CTOR + DTOR + STATIC ## `{.ffiStatic.}`: context-independent, its wrapper takes no `ctx` + + FFIProcMeta* = object + procName*: string + libName*: string + kind*: FFIKind + libTypeName*: string + doc*: string + extraParams*: seq[FFIParamMeta] # all params except the lib param + returnTypeName*: string + returnIsPtr*: bool + returnIsHandle*: bool + abiFormat*: ABIFormat + scalarFastPath*: bool + ## `abi = c` proc with an all-scalar signature: uses the CBOR-free fast + ## path, and binds only in the `abi = c` C header (see `bindableProcs`). + + FFIFieldMeta* = object + name*: string + typeName*: string + + FFIEnumValueMeta* = object + ## One `{.ffi.}` enum value. `wire` is what `$value` yields — the symbol name, + ## or the associated string if the enum declares one — which is exactly what + ## cbor_serialization puts on the wire. + name*: string + wire*: string + ord*: int + + FFITypeMeta* = object + name*: string + fields*: seq[FFIFieldMeta] + abiFormat*: ABIFormat + enumValues*: seq[FFIEnumValueMeta] ## non-empty iff the type is an enum + + FFIConstMeta* = object + ## A `{.ffiConst.}` value. `value` is the compile-time-evaluated result of + ## `$theConst`, re-rendered as a literal by each backend. + name*: string + typeName*: string + value*: string + + FFIEventMeta* = object + ## Library-initiated event from `{.ffiEvent: "wire_name".}`; `wireName` is + ## the verbatim CBOR `eventType` the foreign side dispatches on. + wireName*: string + nimProcName*: string + libName*: string + payloadTypeName*: string + abiFormat*: ABIFormat + doc*: string + +var ffiProcRegistry* {.compileTime.}: seq[FFIProcMeta] +var ffiTypeRegistry* {.compileTime.}: seq[FFITypeMeta] +var ffiEventRegistry* {.compileTime.}: seq[FFIEventMeta] +var ffiConstRegistry* {.compileTime.}: seq[FFIConstMeta] +var currentLibName* {.compileTime.}: string + +# Set by `declareLibrary`; the FFI annotations require it. +var libraryDeclared* {.compileTime.}: bool = false + +# Set by `genBindings()`. Annotations expanded after it register too late to be emitted, so the macros check this and fail loudly instead of dropping silently. +var genBindingsEmitted* {.compileTime.}: bool = false + +# Library-wide default ABI, inherited by each annotation unless it overrides. +var currentDefaultABIFormat* {.compileTime.}: ABIFormat = ABIFormat.Cbor + +proc abiCodegenImplemented*(fmt: ABIFormat): bool = + ## Whether `fmt` has a working proc-dispatch path (both Cbor and C do). + fmt in {ABIFormat.Cbor, ABIFormat.C} + +proc overrideKey*(override: string): string = + ## Lowercased key of a `key = value` pragma override, e.g. `"abi = c"` → `"abi"`. + override.split('=')[0].strip().toLowerAscii() + +proc parseABIFormatName*(name: string): tuple[ok: bool, fmt: ABIFormat] = + ## Bare format name ("c"/"cbor", case-insensitive) → ABIFormat; else ok=false. + case name.strip().toLowerAscii() + of "cbor": + (true, ABIFormat.Cbor) + of "c": + (true, ABIFormat.C) + else: + (false, ABIFormat.Cbor) + +proc parseAbiSpec*(override: string): tuple[ok: bool, fmt: ABIFormat, err: string] = + ## Parse an `"abi = "` override; on bad grammar returns ok=false + err. + let parts = override.split('=') + if parts.len != 2: + return ( + false, + ABIFormat.Cbor, + "invalid ABI override: '" & override & "'; expected `abi = c` or `abi = cbor`", + ) + if parts[0].strip().toLowerAscii() != "abi": + return ( + false, + ABIFormat.Cbor, + "invalid ABI override: '" & override & "'; expected `abi = c` or `abi = cbor`", + ) + let (ok, fmt) = parseABIFormatName(parts[1]) + if not ok: + return ( + false, + ABIFormat.Cbor, + "unknown ABI format: '" & parts[1].strip() & "'; valid values are `c` and `cbor`", + ) + (true, fmt, "") + +# Lib type name (set by declareLibrary) so handle-receiver procs resolve the pool. +var currentLibType* {.compileTime.}: string + +# Names of types marked `{.ffiHandle.}` (wire form uint64). +var ffiHandleTypeNames* {.compileTime.}: seq[string] + +proc isFFIHandleTypeName*(name: string): bool {.compileTime.} = + name in ffiHandleTypeNames + +func isEnum*(t: FFITypeMeta): bool = + return t.enumValues.len > 0 + +# Names of `{.ffi.}` enum types; the `abi = c` wire path has to reject them. +var ffiEnumTypeNames* {.compileTime.}: seq[string] + +proc isFFIEnumTypeName*(name: string): bool {.compileTime.} = + name in ffiEnumTypeNames + +func isStatic*(p: FFIProcMeta): bool = + p.kind == FFIKind.STATIC + +type ClassifiedProcs* = object + ctors*: seq[FFIProcMeta] + methods*: seq[FFIProcMeta] + statics*: seq[FFIProcMeta] + dtor*: Option[FFIProcMeta] + +func classifyProcs*(procs: seq[FFIProcMeta]): ClassifiedProcs = + ## Splits the registry into constructors, methods, statics and the first destructor. + var c: ClassifiedProcs + for p in procs: + case p.kind + of FFIKind.CTOR: + c.ctors.add(p) + of FFIKind.FFI: + c.methods.add(p) + of FFIKind.STATIC: + c.statics.add(p) + of FFIKind.DTOR: + if c.dtor.isNone(): + c.dtor = some(p) + c + +func dtorProcName*(c: ClassifiedProcs): string = + ## The destructor's proc name, or "" when the library has no destructor. + if c.dtor.isSome(): + c.dtor.get().procName + else: + "" + +func replyProcs*(c: ClassifiedProcs): seq[FFIProcMeta] = + ## Procs that reply with a decoded value: methods and statics. + c.methods & c.statics + +proc ridesAsPtr*(ep: FFIParamMeta): bool = + ## True if the param crosses the wire as an opaque uint64 (raw ptr or handle). + ep.isPtr or ep.isHandle + +proc returnRidesAsPtr*(p: FFIProcMeta): bool = + ## True if the return crosses the wire as an opaque uint64 (raw ptr or handle). + p.returnIsPtr or p.returnIsHandle + +# Target language(s), override with -d:targetLang=cpp; comma-separated list allowed. +const targetLang* {.strdefine.} = "rust" + +# Output dir override (-d:ffiOutputDir); empty derives `_bindings/` by src. +const ffiOutputDir* {.strdefine.} = "" + +# Nim src path override relative to outputDir (-d:ffiSrcPath); empty derives it. +const ffiSrcPath* {.strdefine.} = "" + +# When true, targets without scalar codegen silently omit scalar-only `abi = c` procs rather than failing the build. Off by default so the drop is loud; see genBindings(). +const ffiAllowScalarSkip* {.booldefine.} = false diff --git a/wasm-deps/ffi/ffi/codegen/rust.nim b/wasm-deps/ffi/ffi/codegen/rust.nim new file mode 100644 index 000000000..613e19f53 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/rust.nim @@ -0,0 +1,825 @@ +## Rust binding generator: emits a complete Rust crate using CBOR (ciborium). + +import std/[os, strutils] +import ./meta, ./string_helpers, ./types_ir, ./consts + +## Wire-format Rust type for any Nim `ptr T`/`pointer`; fixed 64-bit for a +## host-independent CBOR payload size (mirrors CppPtrType). +const RustPtrType* = "u64" + +func rustScalar(s: ScalarKind): string = + case s + of skBool: "bool" + of skI8: "i8" + of skI16: "i16" + of skI32: "i32" + of skI64: "i64" + of skU8: "u8" + of skU16: "u16" + of skU32: "u32" + of skU64: "u64" + of skF32: "f32" + of skF64: "f64" + +func rustSeq(elem: string): string = + "Vec<" & elem & ">" + +func rustOpt(elem: string): string = + "Option<" & elem & ">" + +const rustMap = NativeTypeMap( + scalar: rustScalar, + str: "String", + # serde encodes a plain Vec as a CBOR integer array, and Nim rejects that + # array. ByteBuf gives the CBOR byte string that Nim decodes. + bytes: "serde_bytes::ByteBuf", + ptrType: RustPtrType, + seqOf: rustSeq, + optOf: rustOpt, + structName: capitalizeFirstLetter, +) + +proc nimTypeToRust*(typeName: string): string = + ## Maps Nim type names to Rust type names, including generics. + renderNative(rustMap, parseFFIType(typeName)) + +proc deriveLibName*(procs: seq[FFIProcMeta]): string = + ## Common prefix before the first `_` in proc names, e.g. "timer_create" → "timer". + if currentLibName.len > 0: + return currentLibName + if procs.len == 0: + return "unknown" + let first = procs[0].procName + let parts = first.split('_') + if parts.len > 0: + return parts[0] + return "unknown" + +proc stripLibPrefix*(procName: string, libName: string): string = + ## Strips the library prefix, e.g. ("timer_echo", "timer") → "echo". + let prefix = libName & "_" + if procName.startsWith(prefix): + return procName[prefix.len .. ^1] + return procName + +proc reqStructName(p: FFIProcMeta): string = + ## Mirrors the Nim macro: Req or CtorReq for ctors. + let camel = snakeToPascalCase(p.procName) + if p.kind == FFIKind.CTOR: + camel & "CtorReq" + else: + camel & "Req" + +func typeUsesBytes(typeName: string): bool = + ## True if `typeName` is a `seq[byte]` at any depth of Seq or Option. + var t = parseFFIType(typeName) + while t.kind in {ftSeq, ftOpt}: + t = t.elem + t.kind == ftBytes + +func needsSerdeBytes*(types: seq[FFITypeMeta], procs: seq[FFIProcMeta]): bool = + ## True if a field, a parameter or a return type maps to `serde_bytes::ByteBuf`. + ## `types` holds every struct. Thus a scan of the fields also finds the bytes + ## in a nested struct. + for t in types: + for f in t.fields: + if typeUsesBytes(f.typeName): + return true + for p in procs: + for ep in p.extraParams: + if typeUsesBytes(ep.typeName): + return true + if p.returnTypeName.len > 0 and typeUsesBytes(p.returnTypeName): + return true + false + +proc generateCargoToml*(libName: string, needsBytes = false): string = + # flume: callback channel (recv_timeout + recv_async), default-features off. tokio: only the async timeout. + # Add serde_bytes only when a `seq[byte]` goes on the wire as a CBOR byte string. + let serdeBytesDep = if needsBytes: "\nserde_bytes = \"0.11\"" else: "" + return + """[package] +name = "$1" +version = "0.1.0" +edition = "2021" + +[dependencies] +serde = { version = "1", features = ["derive"] }$2 +ciborium = "0.2" +flume = { version = "0.11", default-features = false, features = ["async"] } +tokio = { version = "1", features = ["sync", "time"] } + +[dev-dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time"] } +""" % + [libName, serdeBytesDep] + +proc generateBuildRs*(libName: string, nimSrcRelPath: string): string = + ## Generates build.rs that compiles the Nim library; nimSrcRelPath is relative + ## to the crate directory. + let escapedSrc = nimSrcRelPath.replace("\\", "\\\\") + return + """use std::path::PathBuf; +use std::process::Command; + +fn main() { + let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + let nim_src = manifest.join("$1"); + let nim_src = nim_src.canonicalize().unwrap_or(manifest.join("$1")); + + // Walk up to find the nim-ffi repo root (directory containing nim_src's library) + // The repo root is where nim c should be run from (contains config.nims). + // We assume nim_src lives somewhere under repo_root. + // Derive repo_root as the ancestor that contains the .nimble file or config.nims. + let mut repo_root = nim_src.clone(); + loop { + repo_root = match repo_root.parent() { + Some(p) => p.to_path_buf(), + None => break, + }; + if repo_root.join("config.nims").exists() || repo_root.join("ffi.nimble").exists() { + break; + } + } + + #[cfg(target_os = "macos")] + let lib_ext = "dylib"; + #[cfg(target_os = "linux")] + let lib_ext = "so"; + + let out_lib = repo_root.join(format!("lib$2.{lib_ext}")); + + let mut cmd = Command::new("nim"); + cmd.arg("c") + .arg("--mm:orc") + .arg("-d:chronicles_log_level=WARN") + .arg("--app:lib") + .arg("--noMain") + .arg(format!("--nimMainPrefix:lib$2")) + .arg(format!("-o:{}", out_lib.display())); + cmd.arg(&nim_src).current_dir(&repo_root); + + let status = cmd.status().expect("failed to run nim compiler"); + assert!(status.success(), "Nim compilation failed"); + + println!("cargo:rustc-link-search={}", repo_root.display()); + println!("cargo:rustc-link-lib=$2"); + println!("cargo:rerun-if-changed={}", nim_src.display()); +} +""" % + [escapedSrc, libName] + +proc generateLibRs*(): string = + return """mod ffi; +mod types; +mod api; +pub use types::*; +pub use api::*; +""" + +proc generateFFIRs*(procs: seq[FFIProcMeta]): string = + ## Generates ffi.rs with extern "C" declarations; each proc takes one CBOR + ## buffer (ptr+len) as its request payload. + var lines: seq[string] = @[] + lines.add("use std::os::raw::{c_char, c_int, c_void};") + lines.add("") + lines.add("pub type FFICallback = unsafe extern \"C\" fn(") + lines.add(" ret: c_int,") + lines.add(" msg: *const c_char,") + lines.add(" len: usize,") + lines.add(" user_data: *mut c_void,") + lines.add(");") + lines.add("") + + var libNames: seq[string] = @[] + for p in procs: + if p.libName notin libNames: + libNames.add(p.libName) + + var linkLibName = "" + if libNames.len > 0 and libNames[0].len > 0: + linkLibName = libNames[0] + else: + if procs.len > 0: + let parts = procs[0].procName.split('_') + if parts.len > 0: + linkLibName = parts[0] + + lines.add("#[link(name = \"$1\")]" % [linkLibName]) + lines.add("extern \"C\" {") + + for p in procs: + var params: seq[string] = @[] + lines.add(renderMemberDocComment(p.doc)) + case p.kind + of FFIKind.FFI, FFIKind.STATIC: + if not p.isStatic(): + params.add("ctx: *mut c_void") + params.add("callback: FFICallback") + params.add("user_data: *mut c_void") + params.add("req_cbor: *const u8") + params.add("req_cbor_len: usize") + lines.add(" pub fn $1($2) -> c_int;" % [p.procName, params.join(", ")]) + of FFIKind.CTOR: + # Ctor: no ctx; returns the freshly-allocated handle. + params.add("req_cbor: *const u8") + params.add("req_cbor_len: usize") + params.add("callback: FFICallback") + params.add("user_data: *mut c_void") + lines.add(" pub fn $1($2) -> *mut c_void;" % [p.procName, params.join(", ")]) + of FFIKind.DTOR: + params.add("ctx: *mut c_void") + lines.add(" pub fn $1($2) -> c_int;" % [p.procName, params.join(", ")]) + + # Listener-registration ABI, always present in the dylib. + lines.add( + " pub fn $1_add_event_listener(ctx: *mut c_void, event_name: *const c_char, callback: FFICallback, user_data: *mut c_void) -> u64;" % + [linkLibName] + ) + lines.add( + " pub fn $1_remove_event_listener(ctx: *mut c_void, listener_id: u64) -> c_int;" % + [linkLibName] + ) + + lines.add("}") + return lines.join("\n") & "\n" + +func rustConstType(typeName: string): string = + ## `&str` rather than `String`: a `pub const` can't own a heap value. The + ## 'static lifetime is implied, and spelling it out trips clippy. + let t = parseFFIType(typeName) + if t.kind == ftStr: + return "&str" + return renderNative(rustMap, t) + +proc generateTypesRs*( + types: seq[FFITypeMeta], procs: seq[FFIProcMeta], consts: seq[FFIConstMeta] = @[] +): string = + ## Generates types.rs: Rust structs for user FFI types and each per-proc Req. + var lines: seq[string] = @[] + lines.add("use serde::{Deserialize, Serialize};") + lines.add("") + + for c in consts: + let t = parseFFIType(c.typeName) + lines.add( + "pub const $1: $2 = $3;" % [ + identToUpperSnake(c.name), rustConstType(c.typeName), rustConstValue(t, c.value) + ] + ) + if consts.len > 0: + lines.add("") + + for t in types: + if not t.isEnum(): + continue + lines.add("#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]") + lines.add("pub enum $1 {" % [t.name]) + for v in t.enumValues: + let variant = capitalizeFirstLetter(v.name) + # serde carries the same text form Nim's cbor_serialization writes. + if variant != v.wire: + lines.add(" #[serde(rename = \"$1\")]" % [v.wire]) + lines.add(" $1," % [variant]) + lines.add("}") + lines.add("") + + for t in types: + if t.isEnum(): + continue + lines.add("#[derive(Debug, Clone, Serialize, Deserialize)]") + lines.add("pub struct $1 {" % [t.name]) + for f in t.fields: + let snakeName = camelToSnakeCase(f.name) + let rustType = nimTypeToRust(f.typeName) + # serde rename when camelCase differs from snake_case. + if snakeName != f.name: + lines.add(" #[serde(rename = \"$1\")]" % [f.name]) + lines.add(" pub $1: $2," % [snakeName, rustType]) + lines.add("}") + lines.add("") + + # Per-proc Req structs: the unit of CBOR encoding sent across the boundary. + for p in procs: + if p.kind == FFIKind.DTOR: + continue + let reqName = reqStructName(p) + lines.add("#[derive(Debug, Clone, Serialize, Deserialize)]") + if p.extraParams.len == 0: + lines.add("pub struct $1 {}" % [reqName]) + else: + lines.add("pub struct $1 {" % [reqName]) + for ep in p.extraParams: + let snake = camelToSnakeCase(ep.name) + let rustType = + if ep.ridesAsPtr(): + RustPtrType + else: + nimTypeToRust(ep.typeName) + if snake != ep.name: + lines.add(" #[serde(rename = \"$1\")]" % [ep.name]) + lines.add(" pub $1: $2," % [snake, rustType]) + lines.add("}") + lines.add("") + + return lines.join("\n") + +proc generateApiRs*( + procs: seq[FFIProcMeta], libName: string, events: seq[FFIEventMeta] = @[] +): string = + ## Generates api.rs with a blocking and a tokio-async high-level API. + ## Requests/responses are CBOR (ciborium); errors are raw UTF-8 strings. + var lines: seq[string] = @[] + + let classified = classifyProcs(procs) + let ctors = classified.ctors + let dtorProcName = classified.dtorProcName + + var libTypeName = "" + if ctors.len > 0: + libTypeName = ctors[0].libTypeName + else: + libTypeName = capitalizeFirstLetter(libName) + + let ctxTypeName = libTypeName & "Ctx" + + lines.add("use std::os::raw::{c_char, c_int, c_void};") + lines.add("use std::slice;") + lines.add("use std::time::Duration;") + lines.add("use serde::de::DeserializeOwned;") + lines.add("use serde::Serialize;") + lines.add("use super::ffi;") + lines.add("use super::types::*;") + lines.add("") + + lines.add("fn encode_cbor(value: &T) -> Result, String> {") + lines.add(" let mut buf = Vec::new();") + lines.add( + " ciborium::ser::into_writer(value, &mut buf).map_err(|e| e.to_string())?;" + ) + lines.add(" Ok(buf)") + lines.add("}") + lines.add("") + lines.add("fn decode_cbor(bytes: &[u8]) -> Result {") + lines.add(" ciborium::de::from_reader(bytes).map_err(|e| e.to_string())") + lines.add("}") + lines.add("") + + # FFI trampoline: user_data owns a Box; a late callback sends into a closed receiver, which is harmless. + lines.add("type FFIResult = Result, String>;") + lines.add("type FFISender = flume::Sender;") + lines.add("") + lines.add("// Reconstruct the (ret, msg, len) tuple delivered by the C callback") + lines.add( + "// into a Result, String>: payload on success, UTF-8 message on error." + ) + lines.add( + "// `from_utf8_lossy` accepts non-UTF-8 error bytes by inserting U+FFFD; the" + ) + lines.add( + "// alternative would be to dispatch a separate Err for invalid UTF-8, but the" + ) + lines.add("// codegen contract is that Nim handlers emit `string` error payloads, so") + lines.add("// invalid UTF-8 here would be a Nim-side bug.") + lines.add( + "unsafe fn ffi_payload(ret: c_int, msg: *const c_char, len: usize) -> FFIResult {" + ) + lines.add(" let bytes = if msg.is_null() || len == 0 {") + lines.add(" Vec::new()") + lines.add(" } else {") + lines.add(" slice::from_raw_parts(msg as *const u8, len).to_vec()") + lines.add(" };") + lines.add(" if ret == NIMFFI_RET_OK { Ok(bytes) }") + lines.add(" else { Err(String::from_utf8_lossy(&bytes).into_owned()) }") + lines.add("}") + lines.add("") + lines.add("// nim-ffi result-callback status codes (mirror ffi/ffi_types.nim).") + lines.add("const NIMFFI_RET_OK: c_int = 0;") + lines.add("const NIMFFI_RET_MISSING_CALLBACK: c_int = 2;") + lines.add("const NIMFFI_RET_STALE_WARN: c_int = 3;") + lines.add("") + lines.add("unsafe extern \"C\" fn on_result(") + lines.add(" ret: c_int,") + lines.add(" msg: *const c_char,") + lines.add(" len: usize,") + lines.add(" user_data: *mut c_void,") + lines.add(") {") + lines.add( + " // NIMFFI_RET_STALE_WARN (3) is a non-terminal progress ping: the request" + ) + lines.add( + " // is still running. This wrapper only delivers the final result, so ignore" + ) + lines.add( + " // it WITHOUT reclaiming the box — a terminal callback still owns the Sender." + ) + lines.add(" if ret == NIMFFI_RET_STALE_WARN { return; }") + lines.add("") + lines.add(" // Take ownership of the boxed Sender — dropping it at end of scope") + lines.add(" // releases the only outstanding handle.") + lines.add(" let tx = Box::from_raw(user_data as *mut FFISender);") + lines.add("") + lines.add( + " // `tx.send` returns Err only if the awaiting future was dropped (and with it" + ) + lines.add( + " // the Receiver): e.g. tokio::time::timeout elapsed, a tokio::select! branch" + ) + lines.add( + " // lost the race, or the future was dropped before being awaited. This cannot" + ) + lines.add( + " // happen with the current rust_client demo but may occur in arbitrary" + ) + lines.add(" // downstream consumers, so we discard the Err safely.") + lines.add( + " // Given that this is invoked from a Nim thread, we can't propagate the error by panicking or" + ) + lines.add( + " // returning a Result. Furthermore, an API dev may intentionally set a timeout in the await," + ) + lines.add( + " // in which case is also fine to discard the send error in this case because the API user will" + ) + lines.add(" // handle the timeout expiry in their own code.") + lines.add( + " // The important part is to ensure that the callback doesn't panic or block indefinitely if the" + ) + lines.add(" // receiver is gone.") + lines.add(" let _ = tx.send(ffi_payload(ret, msg, len));") + lines.add("}") + lines.add("") + lines.add("fn ffi_call_sync(timeout: Duration, f: F) -> FFIResult") + lines.add("where") + lines.add(" F: FnOnce(ffi::FFICallback, *mut c_void) -> c_int,") + lines.add("{") + lines.add(" let (tx, rx) = flume::bounded::(1);") + lines.add(" let raw = Box::into_raw(Box::new(tx)) as *mut c_void;") + lines.add(" let ret = f(on_result, raw);") + lines.add(" if ret == NIMFFI_RET_MISSING_CALLBACK {") + lines.add(" // Callback will never fire; reclaim the box to avoid a leak.") + lines.add(" drop(unsafe { Box::from_raw(raw as *mut FFISender) });") + lines.add(" return Err(\"RET_MISSING_CALLBACK (internal error)\".into());") + lines.add(" }") + lines.add(" match rx.recv_timeout(timeout) {") + lines.add(" Ok(payload) => payload,") + lines.add(" Err(flume::RecvTimeoutError::Timeout) =>") + lines.add(" Err(format!(\"timed out after {:?}\", timeout)),") + lines.add(" Err(flume::RecvTimeoutError::Disconnected) =>") + lines.add( + " Err(\"callback channel disconnected before delivery\".into())," + ) + lines.add(" }") + lines.add("}") + lines.add("") + lines.add("async fn ffi_call_async(timeout: Duration, f: F) -> FFIResult") + lines.add("where") + lines.add(" F: FnOnce(ffi::FFICallback, *mut c_void) -> c_int,") + lines.add("{") + lines.add(" let (tx, rx) = flume::bounded::(1);") + lines.add(" let raw = Box::into_raw(Box::new(tx)) as *mut c_void;") + lines.add(" let ret = f(on_result, raw);") + lines.add(" if ret == NIMFFI_RET_MISSING_CALLBACK {") + lines.add(" drop(unsafe { Box::from_raw(raw as *mut FFISender) });") + lines.add(" return Err(\"RET_MISSING_CALLBACK (internal error)\".into());") + lines.add(" }") + lines.add(" match tokio::time::timeout(timeout, rx.recv_async()).await {") + lines.add(" Ok(Ok(payload)) => payload,") + lines.add( + " Ok(Err(_)) => Err(\"callback channel disconnected before delivery\".into())," + ) + lines.add(" Err(_) => Err(format!(\"timed out after {:?}\", timeout)),") + lines.add(" }") + lines.add("}") + lines.add("") + + # Per-listener handler boxes + extern "C" trampolines: the Box is kept alive in `listeners`, its raw pointer is the per-event `user_data`. + if events.len > 0: + for ev in events: + let handlerStruct = capitalizeFirstLetter(ev.nimProcName) & "Handler" + let trampolineName = camelToSnakeCase(ev.nimProcName) & "_trampoline" + lines.add("struct $1 {" % [handlerStruct]) + lines.add(" f: Box," % [ev.payloadTypeName]) + lines.add("}") + lines.add("") + lines.add("unsafe extern \"C\" fn $1(" % [trampolineName]) + lines.add(" ret: c_int, msg: *const c_char, len: usize, ud: *mut c_void,") + lines.add(") {") + lines.add(" if ud.is_null() || ret != 0 || msg.is_null() || len == 0 {") + lines.add(" return;") + lines.add(" }") + lines.add(" let h = &*(ud as *const $1);" % [handlerStruct]) + lines.add(" let bytes = slice::from_raw_parts(msg as *const u8, len);") + lines.add(" #[derive(serde::Deserialize)]") + lines.add(" struct Envelope { payload: $1 }" % [ev.payloadTypeName]) + lines.add( + " if let Ok(env) = ciborium::de::from_reader::(bytes) {" + ) + lines.add(" (h.f)(&env.payload);") + lines.add(" }") + lines.add("}") + lines.add("") + + # Public handle returned by every add_…_listener call. + lines.add("#[derive(Debug, Clone, Copy)]") + lines.add("pub struct ListenerHandle { pub id: u64 }") + lines.add("") + + lines.add("/// High-level context for `$1`." % [libTypeName]) + lines.add("pub struct $1 {" % [ctxTypeName]) + lines.add(" ptr: *mut c_void,") + lines.add(" timeout: Duration,") + if events.len > 0: + # Keeps each handler box alive while its listener id is live on the Nim side. + lines.add( + " listeners: std::sync::Mutex>>," + ) + lines.add("}") + lines.add("") + # SAFETY block applies to both impls below. + lines.add( + "// SAFETY: The `ptr` field points to an FFIContext owned by the Nim runtime." + ) + lines.add("// Every call through the generated FFI proc goes through") + lines.add( + "// `sendRequestToFFIThread` on the Nim side, which only enqueues the request" + ) + lines.add("// onto a mutex-guarded MPSC queue (sound from any number of threads) and") + lines.add( + "// wakes the single FFI thread that dispatches every handler. The context is" + ) + lines.add( + "// thus never mutated non-atomically from the caller's thread. The Nim-side" + ) + lines.add("// reentrancy guard (`onFFIThread` threadvar) prevents handlers from") + lines.add("// re-entering the dispatcher. These invariants make it sound to mark the") + lines.add("// wrapper as Send + Sync.") + lines.add("unsafe impl Send for $1 {}" % [ctxTypeName]) + lines.add("unsafe impl Sync for $1 {}" % [ctxTypeName]) + lines.add("") + + # Drop tears down the Nim runtime when the ctx goes out of scope; without it, forgetting the ctx leaks the entire runtime (FFI thread, watchdog, chronos). + if dtorProcName.len > 0: + lines.add("impl Drop for $1 {" % [ctxTypeName]) + lines.add(" fn drop(&mut self) {") + lines.add(" if !self.ptr.is_null() {") + lines.add(" unsafe { ffi::$1(self.ptr); }" % [dtorProcName]) + lines.add(" self.ptr = std::ptr::null_mut();") + lines.add(" }") + # `listeners` drops after this body; the dylib has joined its threads by then, so no callback is mid-flight against the raw pointers we handed it. + lines.add(" }") + lines.add("}") + lines.add("") + + lines.add("impl $1 {" % [ctxTypeName]) + + for ctor in ctors: + let reqName = reqStructName(ctor) + var paramsList: seq[string] = @[] + var fieldInits: seq[string] = @[] + for ep in ctor.extraParams: + let snake = camelToSnakeCase(ep.name) + let rustType = + if ep.ridesAsPtr(): + RustPtrType + else: + nimTypeToRust(ep.typeName) + paramsList.add("$1: $2" % [snake, rustType]) + fieldInits.add(snake) + # `create` and `new_async` take an explicit `timeout: Duration` that flows into `self.timeout` so subsequent method calls inherit it. + let ctorParamsStr = + if paramsList.len > 0: + paramsList.join(", ") & ", timeout: Duration" + else: + "timeout: Duration" + + let reqLit = + if fieldInits.len > 0: + reqName & " { " & fieldInits.join(", ") & " }" + else: + reqName & " {}" + + lines.add(renderMemberDocComment(ctor.doc)) + lines.add(" pub fn create($1) -> Result {" % [ctorParamsStr]) + lines.add(" let req = $1;" % [reqLit]) + lines.add(" let req_bytes = encode_cbor(&req)?;") + # Ctor also fires the callback carrying the payload, so discard the synchronous *mut c_void and yield RET_OK to wait on the callback. + lines.add(" let raw_bytes = ffi_call_sync(timeout, |cb, ud| unsafe {") + lines.add( + " let _ = ffi::$1(req_bytes.as_ptr(), req_bytes.len(), cb, ud);" % + [ctor.procName] + ) + lines.add(" 0") + lines.add(" })?;") + # Ctor success payload is a CBOR text string holding the ctx address. + lines.add(" let addr_str: String = decode_cbor(&raw_bytes)?;") + lines.add( + " let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;" + ) + if events.len > 0: + lines.add( + " Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })" + ) + else: + lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout })") + lines.add(" }") + lines.add("") + + lines.add(renderMemberDocComment(ctor.doc)) + lines.add( + " pub async fn new_async($1) -> Result {" % [ctorParamsStr] + ) + lines.add(" let req = $1;" % [reqLit]) + lines.add(" let req_bytes = encode_cbor(&req)?;") + # See `create`: discard the ctor's synchronous return; the callback delivers the ctx address. + lines.add(" let raw_bytes = ffi_call_async(timeout, move |cb, ud| unsafe {") + lines.add( + " let _ = ffi::$1(req_bytes.as_ptr(), req_bytes.len(), cb, ud);" % + [ctor.procName] + ) + lines.add(" 0") + lines.add(" }).await?;") + lines.add(" let addr_str: String = decode_cbor(&raw_bytes)?;") + lines.add( + " let addr: usize = addr_str.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;" + ) + if events.len > 0: + lines.add( + " Ok(Self { ptr: addr as *mut c_void, timeout, listeners: std::sync::Mutex::new(std::collections::HashMap::new()) })" + ) + else: + lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout })") + lines.add(" }") + lines.add("") + + if events.len > 0: + # Shared by every public `add_*_listener`: caller owns the concrete-typed box, erased to `dyn Any + Send` only on hand-off. + lines.add(" fn add_listener_inner(") + lines.add(" &self,") + lines.add(" event_name: *const c_char,") + lines.add(" callback: ffi::FFICallback,") + lines.add(" raw: *mut c_void,") + lines.add(" owned: Box,") + lines.add(" ) -> ListenerHandle {") + lines.add(" let id = unsafe {") + lines.add( + " ffi::$1_add_event_listener(self.ptr, event_name, callback, raw)" % + [libName] + ) + lines.add(" };") + lines.add(" if id != 0 {") + lines.add(" self.listeners.lock().unwrap().insert(id, owned);") + lines.add(" }") + lines.add(" ListenerHandle { id }") + lines.add(" }") + lines.add("") + + for ev in events: + let methodName = "add_" & camelToSnakeCase(ev.nimProcName) & "_listener" + let handlerStruct = capitalizeFirstLetter(ev.nimProcName) & "Handler" + let trampolineName = camelToSnakeCase(ev.nimProcName) & "_trampoline" + lines.add(renderMemberDocComment(ev.doc)) + lines.add( + " /// Register a typed listener for `$1`. The returned handle can be" % + [ev.wireName] + ) + lines.add(" /// passed to `remove_event_listener` to unregister.") + lines.add(" pub fn $1(&self, handler: F) -> ListenerHandle" % [methodName]) + lines.add(" where F: Fn(&$1) + Send + Sync + 'static," % [ev.payloadTypeName]) + lines.add(" {") + lines.add( + " let owned: Box<$1> = Box::new($1 { f: Box::new(handler) });" % + [handlerStruct] + ) + lines.add( + " let raw = &*owned as *const $1 as *mut c_void;" % [handlerStruct] + ) + lines.add( + " self.add_listener_inner(b\"$1\\0\".as_ptr() as *const c_char, $2, raw, owned)" % + [ev.wireName, trampolineName] + ) + lines.add(" }") + lines.add("") + + # Remove by handle; drops the Box after the C ABI confirms unregistration. + lines.add(" /// Remove a previously-registered listener by handle. Returns true") + lines.add(" /// if the listener existed and was removed; false otherwise.") + lines.add( + " pub fn remove_event_listener(&self, handle: ListenerHandle) -> bool {" + ) + lines.add(" if handle.id == 0 { return false; }") + lines.add(" let rc = unsafe {") + lines.add( + " ffi::$1_remove_event_listener(self.ptr, handle.id)" % [libName] + ) + lines.add(" };") + lines.add(" self.listeners.lock().unwrap().remove(&handle.id);") + lines.add(" rc == 0") + lines.add(" }") + lines.add("") + + # A static is an associated fn: no `&self` to read `timeout` from, so it takes one. + for m in classified.replyProcs(): + let isStatic = m.isStatic() + let methodName = stripLibPrefix(m.procName, libName) + let retRustType = nimTypeToRust(m.returnTypeName) + let reqName = reqStructName(m) + + var paramsList: seq[string] = @[] + var fieldInits: seq[string] = @[] + for ep in m.extraParams: + let snake = camelToSnakeCase(ep.name) + let rustType = + if ep.ridesAsPtr(): + RustPtrType + else: + nimTypeToRust(ep.typeName) + paramsList.add("$1: $2" % [snake, rustType]) + fieldInits.add(snake) + if isStatic: + paramsList.add("timeout: Duration") + let paramsStr = + if isStatic: + paramsList.join(", ") + elif paramsList.len > 0: + "&self, " & paramsList.join(", ") + else: + "&self" + + let reqLit = + if fieldInits.len > 0: + reqName & " { " & fieldInits.join(", ") & " }" + else: + reqName & " {}" + + let retTypeForApi = if m.returnRidesAsPtr(): RustPtrType else: retRustType + let timeoutExpr = if isStatic: "timeout" else: "self.timeout" + let ctxArg = if isStatic: "" else: "self.ptr, " + + lines.add(renderMemberDocComment(m.doc)) + lines.add( + " pub fn $1($2) -> Result<$3, String> {" % + [methodName, paramsStr, retTypeForApi] + ) + lines.add(" let req = $1;" % [reqLit]) + lines.add(" let req_bytes = encode_cbor(&req)?;") + lines.add( + " let raw_bytes = ffi_call_sync($1, |cb, ud| unsafe {" % [timeoutExpr] + ) + lines.add( + " ffi::$1($2cb, ud, req_bytes.as_ptr(), req_bytes.len())" % + [m.procName, ctxArg] + ) + lines.add(" })?;") + lines.add(" decode_cbor::<$1>(&raw_bytes)" % [retTypeForApi]) + lines.add(" }") + lines.add("") + + # async method: ptr cast to usize (Copy + Send) keeps the move closure and returned future Send for multi-threaded tokio runtimes. + lines.add(renderMemberDocComment(m.doc)) + lines.add( + " pub async fn $1_async($2) -> Result<$3, String> {" % + [methodName, paramsStr, retTypeForApi] + ) + lines.add(" let req = $1;" % [reqLit]) + lines.add(" let req_bytes = encode_cbor(&req)?;") + if not isStatic: + lines.add(" let ptr = self.ptr as usize;") + lines.add( + " let raw_bytes = ffi_call_async($1, move |cb, ud| unsafe {" % [ + timeoutExpr + ] + ) + lines.add( + " ffi::$1($2cb, ud, req_bytes.as_ptr(), req_bytes.len())" % + [m.procName, if isStatic: "" else: "ptr as *mut c_void, "] + ) + lines.add(" }).await?;") + lines.add(" decode_cbor::<$1>(&raw_bytes)" % [retTypeForApi]) + lines.add(" }") + lines.add("") + + lines.add("}") + return lines.join("\n") & "\n" + +proc generateRustCrate*( + procs: seq[FFIProcMeta], + types: seq[FFITypeMeta], + libName: string, + outputDir: string, + nimSrcRelPath: string, + events: seq[FFIEventMeta] = @[], + consts: seq[FFIConstMeta] = @[], +) = + ## Generates a complete Rust crate in outputDir. + createDir(outputDir) + createDir(outputDir / "src") + + writeFile( + outputDir / "Cargo.toml", generateCargoToml(libName, needsSerdeBytes(types, procs)) + ) + writeFile(outputDir / "build.rs", generateBuildRs(libName, nimSrcRelPath)) + writeFile(outputDir / "src" / "lib.rs", generateLibRs()) + writeFile(outputDir / "src" / "ffi.rs", generateFFIRs(procs)) + writeFile(outputDir / "src" / "types.rs", generateTypesRs(types, procs, consts)) + writeFile(outputDir / "src" / "api.rs", generateApiRs(procs, libName, events)) diff --git a/wasm-deps/ffi/ffi/codegen/string_helpers.nim b/wasm-deps/ffi/ffi/codegen/string_helpers.nim new file mode 100644 index 000000000..663c5f70f --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/string_helpers.nim @@ -0,0 +1,96 @@ +## Unicode-aware identifier casing and doc-comment rendering, shared by codegen +## and the FFI macro. + +import std/[strutils, unicode] + +func docLines(doc: string): seq[string] = + ## `doc` split into lines, trailing blank ones dropped. + if doc.strip().len == 0: + return @[] + var lines = doc.splitLines() + while lines.len > 0 and lines[^1].strip().len == 0: + lines.setLen(lines.len - 1) + return lines + +func renderDocComment*(doc, indent, prefix: string): seq[string] = + ## `doc` as one `prefix`-led line comment per source line, at `indent`. + var rendered: seq[string] = @[] + for line in docLines(doc): + # A trailing `\` would splice the next generated line into a `//` comment. + rendered.add( + indent & (prefix & line).strip(leading = false, chars = Whitespace + {'\\'}) + ) + return rendered + +func renderMemberDocComment*(doc: string): seq[string] = + ## `///` at the indent C++ class members and Rust `impl` items sit at. + return doc.renderDocComment(" ", "/// ") + +func escapeBlockComment(line: string): string = + ## `*/` would close the comment early and splice the rest in as code. + return line.replace("*/", "* /") + +func renderBlockDocComment*(doc: string, indent = ""): seq[string] = + ## `doc` as a `/** ... */` block at `indent`; one-liners stay on one line. + let lines = docLines(doc) + if lines.len == 0: + return @[] + if lines.len == 1: + return @[indent & "/** " & escapeBlockComment(lines[0].strip()) & " */"] + var rendered = @[indent & "/**"] + for line in lines: + rendered.add((indent & " * " & escapeBlockComment(line)).strip(leading = false)) + rendered.add(indent & " */") + return rendered + +proc toLower*(s: string): string = + ## Unicode-aware lowercase for an entire string. + var buf = "" + for r in runes(s): + buf.add($r.toLower()) + return buf + +proc camelToSnakeCase*(s: string): string = + ## camelCase → snake_case, e.g. "delayMs" → "delay_ms". + var snake = "" + var first = true + for r in runes(s): + if r.isUpper() and not first: + snake.add('_') + snake.add($r.toLower()) + first = false + return snake + +func capitalizeFirstLetter*(s: string): string = + ## Returns `s` with its first rune uppercased, rest unchanged. + if s.len == 0: + return s + var runesSeq = toRunes(s) + runesSeq[0] = runesSeq[0].toUpper() + return $runesSeq + +func identToUpperSnake*(s: string): string = + ## Nim identifier → UPPER_SNAKE, keeping acronym runs intact: "maxPeers" and + ## "MAX_PEERS" both give "MAX_PEERS", "httpTTL" gives "HTTP_TTL". + var upper = "" + let rs = toRunes(s) + for i, r in rs: + if r == Rune('_'): + if upper.len > 0 and upper[^1] != '_': + upper.add('_') + continue + let startsWord = + i > 0 and r.isUpper() and + (not rs[i - 1].isUpper() or (i + 1 < rs.len and rs[i + 1].isLower())) + if startsWord and upper.len > 0 and upper[^1] != '_': + upper.add('_') + upper.add($r.toUpper()) + return upper + +proc snakeToPascalCase*(s: string): string = + ## snake_case → PascalCase, e.g. "hello_world" → "HelloWorld". + let parts = s.split('_') + var pascal = "" + for p in parts: + pascal.add capitalizeFirstLetter(p) + return pascal diff --git a/wasm-deps/ffi/ffi/codegen/templates/c/CMakeLists.txt.tpl b/wasm-deps/ffi/ffi/codegen/templates/c/CMakeLists.txt.tpl new file mode 100644 index 000000000..091a57521 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/c/CMakeLists.txt.tpl @@ -0,0 +1,47 @@ +cmake_minimum_required(VERSION 3.14) +project({{LIB}}_c_bindings C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# ── Locate the repository root (contains ffi.nimble) ───────────────────────── +set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}") +set(REPO_ROOT "") +foreach(_i RANGE 10) + if(EXISTS "${_search_dir}/ffi.nimble") + set(REPO_ROOT "${_search_dir}") + break() + endif() + get_filename_component(_search_dir "${_search_dir}" DIRECTORY) +endforeach() +if("${REPO_ROOT}" STREQUAL "") + message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)") +endif() + +# Build the Nim dylib + vendored TinyCBOR (shared with the C++ backend). +set(NIM_FFI_LIB {{LIB}}) +set(NIM_FFI_SRC {{SRC}}) +include("${REPO_ROOT}/ffi/codegen/templates/nim_ffi_lib.cmake") + +find_package(Threads REQUIRED) + +add_library({{LIB}}_headers INTERFACE) +target_include_directories({{LIB}}_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries({{LIB}}_headers INTERFACE {{LIB}} tinycbor Threads::Threads) +# The generated header is async (no blocking helper), but consumer code that +# waits on a result callback typically uses nanosleep / pthreads, which need a +# POSIX feature level that strict `-std=c11` hides. Define it for consumers. +target_compile_definitions({{LIB}}_headers INTERFACE _POSIX_C_SOURCE=200809L) + +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c") + add_executable({{LIB}}_example main.c) + target_link_libraries({{LIB}}_example PRIVATE {{LIB}}_headers) + add_dependencies({{LIB}}_example {{LIB}}_nim_lib) + if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_custom_command(TARGET {{LIB}}_example POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${{{LIB}}_RUNTIME_LIB}" + "$" + COMMENT "Staging {{LIB}}.dll next to {{LIB}}_example.exe") + endif() +endif() diff --git a/wasm-deps/ffi/ffi/codegen/templates/c/CMakeLists_abi.txt.tpl b/wasm-deps/ffi/ffi/codegen/templates/c/CMakeLists_abi.txt.tpl new file mode 100644 index 000000000..2fa6a90d2 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/c/CMakeLists_abi.txt.tpl @@ -0,0 +1,72 @@ +cmake_minimum_required(VERSION 3.14) +project({{LIB}}_c_abi_bindings C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +# The CBOR-free `abi = c` binding links no TinyCBOR — the generated header +# structs are the ABI. Only the Nim dylib is built. + +set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}") +set(REPO_ROOT "") +foreach(_i RANGE 10) + if(EXISTS "${_search_dir}/ffi.nimble") + set(REPO_ROOT "${_search_dir}") + break() + endif() + get_filename_component(_search_dir "${_search_dir}" DIRECTORY) +endforeach() +if("${REPO_ROOT}" STREQUAL "") + message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)") +endif() + +# Extra `nim c` arguments (e.g. a `-d:` that flips a shared example source to +# `abi = c`). A library that declares `defaultABIFormat = "c"` needs none. +set(NIM_FFI_EXTRA_ARGS "" CACHE STRING "Extra nim c args when building the dylib") + +find_program(NIM_EXECUTABLE nim REQUIRED) + +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(NIM_LIB_FILE "${REPO_ROOT}/lib{{LIB}}.dylib") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(NIM_LIB_FILE "${REPO_ROOT}/{{LIB}}.dll") +else() + set(NIM_LIB_FILE "${REPO_ROOT}/lib{{LIB}}.so") +endif() + +get_filename_component(NIM_SRC "${CMAKE_CURRENT_SOURCE_DIR}/{{SRC}}" ABSOLUTE) + +add_custom_command( + OUTPUT "${NIM_LIB_FILE}" + COMMAND "${NIM_EXECUTABLE}" c + --mm:orc + -d:chronicles_log_level=WARN + --app:lib + --noMain + "--nimMainPrefix:lib{{LIB}}" + ${NIM_FFI_EXTRA_ARGS} + "-o:${NIM_LIB_FILE}" + "${NIM_SRC}" + WORKING_DIRECTORY "${REPO_ROOT}" + DEPENDS "${NIM_SRC}" + COMMENT "Compiling Nim library lib{{LIB}} (abi = c)" + VERBATIM +) +add_custom_target({{LIB}}_nim_lib ALL DEPENDS "${NIM_LIB_FILE}") + +add_library({{LIB}} SHARED IMPORTED GLOBAL) +set_target_properties({{LIB}} PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}") +add_dependencies({{LIB}} {{LIB}}_nim_lib) + +find_package(Threads REQUIRED) + +add_library({{LIB}}_headers INTERFACE) +target_include_directories({{LIB}}_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries({{LIB}}_headers INTERFACE {{LIB}} Threads::Threads) +target_compile_definitions({{LIB}}_headers INTERFACE _POSIX_C_SOURCE=200809L) + +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.c") + add_executable({{LIB}}_example main.c) + target_link_libraries({{LIB}}_example PRIVATE {{LIB}}_headers) + add_dependencies({{LIB}}_example {{LIB}}_nim_lib) +endif() diff --git a/wasm-deps/ffi/ffi/codegen/templates/c/cbor_helpers.h.tpl b/wasm-deps/ffi/ffi/codegen/templates/c/cbor_helpers.h.tpl new file mode 100644 index 000000000..3056aab19 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/c/cbor_helpers.h.tpl @@ -0,0 +1,352 @@ +#ifndef NIM_FFI_CBOR_HELPERS_H_INCLUDED +#define NIM_FFI_CBOR_HELPERS_H_INCLUDED +/* Leaf CBOR codecs (scalars, text strings, byte strings) plus the buffer + * drivers. The per-struct / per-container codecs in the library header call + * into these by name (C has no overloading, so each leaf gets a distinct + * nimffi_enc_* / nimffi_dec_* symbol). Guarded so two nim-ffi headers can + * share a translation unit. */ +#include "nim_ffi_prelude.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Result delivery callback exported by the Nim dylib: `ret` is 0 on success + * (then `msg`/`len` carry the CBOR response) or non-zero on failure (then + * `msg`/`len` carry the error text, which is NOT NUL-terminated). */ +typedef void (*FFICallback)(int ret, const char* msg, size_t len, void* user_data); + +/* Return / callback status codes. NIMFFI_RET_OK (0) is success; any non-zero + * value handed to a result callback's `err_code` (or returned by a submit call) + * is a failure. NIMFFI_RET_MISSING_CALLBACK is a special case from the Nim + * dispatcher: the callback will never fire, so the request path must report the + * failure itself. + * + * NIMFFI_RET_STALE_WARN is the one NON-terminal code: nim-ffi delivers it every + * ~5s while a handler is still running (with `msg`/`len` carrying the elapsed + * milliseconds as decimal text), then still ends with a terminal RET_OK/RET_ERR. + * A caller that only wants the final answer must ignore it, not treat it as an + * error. */ +#define NIMFFI_RET_OK 0 +#define NIMFFI_RET_ERROR 1 +#define NIMFFI_RET_MISSING_CALLBACK 2 +#define NIMFFI_RET_STALE_WARN 3 + +/* ── leaf encoders ─────────────────────────────────────────────────────── */ +static inline CborError nimffi_enc_bool(CborEncoder* e, const bool* v) { + return cbor_encode_boolean(e, *v); +} +static inline CborError nimffi_enc_i64(CborEncoder* e, const int64_t* v) { + return cbor_encode_int(e, *v); +} +static inline CborError nimffi_enc_i32(CborEncoder* e, const int32_t* v) { + return cbor_encode_int(e, (int64_t)*v); +} +static inline CborError nimffi_enc_i16(CborEncoder* e, const int16_t* v) { + return cbor_encode_int(e, (int64_t)*v); +} +static inline CborError nimffi_enc_i8(CborEncoder* e, const int8_t* v) { + return cbor_encode_int(e, (int64_t)*v); +} +static inline CborError nimffi_enc_u64(CborEncoder* e, const uint64_t* v) { + return cbor_encode_uint(e, *v); +} +static inline CborError nimffi_enc_u32(CborEncoder* e, const uint32_t* v) { + return cbor_encode_uint(e, (uint64_t)*v); +} +static inline CborError nimffi_enc_u16(CborEncoder* e, const uint16_t* v) { + return cbor_encode_uint(e, (uint64_t)*v); +} +static inline CborError nimffi_enc_u8(CborEncoder* e, const uint8_t* v) { + return cbor_encode_uint(e, (uint64_t)*v); +} +static inline CborError nimffi_enc_f64(CborEncoder* e, const double* v) { + return cbor_encode_double(e, *v); +} +static inline CborError nimffi_enc_f32(CborEncoder* e, const float* v) { + return cbor_encode_float(e, *v); +} +static inline CborError nimffi_enc_str(CborEncoder* e, const NimFfiStr* v) { + return cbor_encode_text_string(e, v->data ? v->data : "", v->len); +} +static inline CborError nimffi_enc_bytes(CborEncoder* e, const NimFfiBytes* v) { + return cbor_encode_byte_string(e, v->data, v->len); +} + +/* ── leaf decoders ─────────────────────────────────────────────────────── */ +/* After reading a leaf, the parser must advance past it; both steps + * short-circuit on the same CborError, so they travel together. */ +static inline CborError nimffi_advance_if_ok(CborValue* it, CborError err) { + if (err) { + return err; + } + return cbor_value_advance(it); +} + +static inline CborError nimffi_dec_bool(CborValue* it, bool* out) { + if (!cbor_value_is_boolean(it)) { + return CborErrorImproperValue; + } + return nimffi_advance_if_ok(it, cbor_value_get_boolean(it, out)); +} +static inline CborError nimffi_dec_i64(CborValue* it, int64_t* out) { + if (!cbor_value_is_integer(it)) { + return CborErrorImproperValue; + } + return nimffi_advance_if_ok(it, cbor_value_get_int64_checked(it, out)); +} +static inline CborError nimffi_dec_i32(CborValue* it, int32_t* out) { + int64_t tmp = 0; + CborError err = nimffi_dec_i64(it, &tmp); + if (err) { + return err; + } + if (tmp < INT32_MIN || tmp > INT32_MAX) { + return CborErrorDataTooLarge; + } + *out = (int32_t)tmp; + return CborNoError; +} +static inline CborError nimffi_dec_i16(CborValue* it, int16_t* out) { + int64_t tmp = 0; + CborError err = nimffi_dec_i64(it, &tmp); + if (err) { + return err; + } + if (tmp < INT16_MIN || tmp > INT16_MAX) { + return CborErrorDataTooLarge; + } + *out = (int16_t)tmp; + return CborNoError; +} +static inline CborError nimffi_dec_i8(CborValue* it, int8_t* out) { + int64_t tmp = 0; + CborError err = nimffi_dec_i64(it, &tmp); + if (err) { + return err; + } + if (tmp < INT8_MIN || tmp > INT8_MAX) { + return CborErrorDataTooLarge; + } + *out = (int8_t)tmp; + return CborNoError; +} +static inline CborError nimffi_dec_u64(CborValue* it, uint64_t* out) { + if (!cbor_value_is_unsigned_integer(it)) { + return CborErrorImproperValue; + } + return nimffi_advance_if_ok(it, cbor_value_get_uint64(it, out)); +} +static inline CborError nimffi_dec_u32(CborValue* it, uint32_t* out) { + uint64_t tmp = 0; + CborError err = nimffi_dec_u64(it, &tmp); + if (err) { + return err; + } + if (tmp > UINT32_MAX) { + return CborErrorDataTooLarge; + } + *out = (uint32_t)tmp; + return CborNoError; +} +static inline CborError nimffi_dec_u16(CborValue* it, uint16_t* out) { + uint64_t tmp = 0; + CborError err = nimffi_dec_u64(it, &tmp); + if (err) { + return err; + } + if (tmp > UINT16_MAX) { + return CborErrorDataTooLarge; + } + *out = (uint16_t)tmp; + return CborNoError; +} +static inline CborError nimffi_dec_u8(CborValue* it, uint8_t* out) { + uint64_t tmp = 0; + CborError err = nimffi_dec_u64(it, &tmp); + if (err) { + return err; + } + if (tmp > UINT8_MAX) { + return CborErrorDataTooLarge; + } + *out = (uint8_t)tmp; + return CborNoError; +} +static inline CborError nimffi_dec_f64(CborValue* it, double* out) { + if (cbor_value_is_double(it)) { + return nimffi_advance_if_ok(it, cbor_value_get_double(it, out)); + } + if (cbor_value_is_float(it)) { + float f = 0.0f; + CborError err = cbor_value_get_float(it, &f); + if (err) { + return err; + } + *out = (double)f; + return cbor_value_advance(it); + } + return CborErrorImproperValue; +} +static inline CborError nimffi_dec_f32(CborValue* it, float* out) { + if (cbor_value_is_float(it)) { + return nimffi_advance_if_ok(it, cbor_value_get_float(it, out)); + } + if (cbor_value_is_double(it)) { + double d = 0.0; + CborError err = cbor_value_get_double(it, &d); + if (err) { + return err; + } + *out = (float)d; + return cbor_value_advance(it); + } + return CborErrorImproperValue; +} +static inline CborError nimffi_dec_str(CborValue* it, NimFfiStr* out) { + if (!cbor_value_is_text_string(it)) { + return CborErrorImproperValue; + } + size_t len = 0; + CborError err = cbor_value_get_string_length(it, &len); + if (err) { + return err; + } + if (len == SIZE_MAX) { /* len + 1 would wrap to a 0-byte allocation */ + return CborErrorDataTooLarge; + } + /* one extra byte so a NUL-free payload is a valid C string */ + out->data = (char*)malloc(len + 1); + if (!out->data) { + return CborErrorOutOfMemory; + } + out->len = len; + size_t copied = len; + err = cbor_value_copy_text_string(it, out->data, &copied, NULL); + if (err) { + free(out->data); + out->data = NULL; + out->len = 0; + return err; + } + out->data[len] = '\0'; + return cbor_value_advance(it); +} +static inline CborError nimffi_dec_bytes(CborValue* it, NimFfiBytes* out) { + if (!cbor_value_is_byte_string(it)) { + return CborErrorImproperValue; + } + size_t len = 0; + CborError err = cbor_value_get_string_length(it, &len); + if (err) { + return err; + } + out->data = (uint8_t*)malloc(len ? len : 1); + if (!out->data) { + return CborErrorOutOfMemory; + } + out->len = len; + size_t copied = len; + err = cbor_value_copy_byte_string(it, out->data, &copied, NULL); + if (err) { + free(out->data); + out->data = NULL; + out->len = 0; + return err; + } + return cbor_value_advance(it); +} + +/* ── buffer drivers ────────────────────────────────────────────────────── */ +typedef CborError (*nimffi_enc_fn)(CborEncoder*, const void*); +typedef CborError (*nimffi_dec_fn)(CborValue*, void*); + +static inline char* nimffi_dup_cstr(const char* s) { + size_t n = strlen(s) + 1; + char* p = (char*)malloc(n); + if (p) { + memcpy(p, s, n); + } + return p; +} + +/* NUL-terminated copy of a length-delimited (not NUL-terminated) byte run, + * for turning the FFICallback's raw error `msg`/`len` into a C string; NULL if + * it can't. */ +static inline char* nimffi_dup_cstr_n(const char* s, size_t n) { + if (n == SIZE_MAX) { + return NULL; + } + char* p = (char*)malloc(n + 1); + if (p) { + if (n > 0) { + memcpy(p, s, n); + } + p[n] = '\0'; + } + return p; +} + +/* Encode `val` with `fn` into a freshly malloc'd buffer, doubling on overflow. + * Returns 0 and sets out/outlen on success; -1 and *err (heap) on failure. */ +static inline int nimffi_encode_to_buf( + nimffi_enc_fn fn, const void* val, + uint8_t** out, size_t* outlen, char** err) { + size_t cap = 4096; + uint8_t* buf = (uint8_t*)malloc(cap); + if (!buf) { + if (err) *err = nimffi_dup_cstr("out of memory"); + return -1; + } + for (;;) { + CborEncoder enc; + cbor_encoder_init(&enc, buf, cap, 0); + CborError e = fn(&enc, val); + if (e == CborNoError) { + *outlen = cbor_encoder_get_buffer_size(&enc, buf); + *out = buf; + return 0; + } + if (e == CborErrorOutOfMemory) { + size_t extra = cbor_encoder_get_extra_bytes_needed(&enc); + cap += extra > 0 ? extra : cap; + uint8_t* grown = (uint8_t*)realloc(buf, cap); + if (!grown) { + free(buf); + if (err) *err = nimffi_dup_cstr("out of memory"); + return -1; + } + buf = grown; + continue; + } + free(buf); + if (err) *err = nimffi_dup_cstr(cbor_error_string(e)); + return -1; + } +} + +/* Decode a CBOR buffer into `out` with `fn`. Returns 0 on success; -1 and + * *err (heap) on failure. */ +static inline int nimffi_decode_from_buf( + nimffi_dec_fn fn, const uint8_t* buf, size_t len, + void* out, char** err) { + CborParser parser; + CborValue it; + CborError e = cbor_parser_init(buf, len, 0, &parser, &it); + if (e != CborNoError) { + if (err) *err = nimffi_dup_cstr(cbor_error_string(e)); + return -1; + } + e = fn(&it, out); + if (e != CborNoError) { + if (err) *err = nimffi_dup_cstr(cbor_error_string(e)); + return -1; + } + return 0; +} + +#ifdef __cplusplus +} +#endif + +#endif /* NIM_FFI_CBOR_HELPERS_H_INCLUDED */ diff --git a/wasm-deps/ffi/ffi/codegen/templates/c/header_prelude.h.tpl b/wasm-deps/ffi/ffi/codegen/templates/c/header_prelude.h.tpl new file mode 100644 index 000000000..cc04549c1 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/c/header_prelude.h.tpl @@ -0,0 +1,88 @@ +#ifndef NIM_FFI_PRELUDE_H_INCLUDED +#define NIM_FFI_PRELUDE_H_INCLUDED +/* Generated C binding for a nim-ffi library. Requests/responses travel as + * CBOR (encoded with vendored TinyCBOR on this side, matching the Nim-side + * cbor_serial codec on the wire — both ends speak RFC 8949). + * + * The API is asynchronous: every method/constructor takes a result callback + * and returns immediately. The callback fires exactly once — synchronously on + * a submit-time failure, otherwise from the Nim dispatch thread when the reply + * arrives. + * + * Memory ownership contract: + * - Request-side strings/sequences are *borrowed*: the binding only reads + * them while encoding, so a string literal wrapped with nimffi_str() is + * fine and is never freed by the binding. + * - Response values and error strings passed into a result callback are + * *owned by the binding* and valid only for the duration of that callback; + * the binding reclaims them once the callback returns. The caller never + * frees them. (The generated _free_() helpers are internal — the + * trampolines use them to reclaim decoded payloads.) + * - A context handle delivered to a constructor callback is the exception: + * ownership transfers to the caller, who releases it with + * _ctx_destroy(). It is a lifecycle handle, not returned data. + * + * Trust boundary: the decoders assume the CBOR they parse was produced by the + * paired Nim library. They reject malformed input rather than trusting it, but + * they are not hardened against a hostile peer feeding crafted payloads through + * the raw nimffi_decode_from_buf entry point. + */ +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Owned, length-delimited UTF-8 text (Nim `string`/`cstring`). On the request + * side `data` may point at borrowed storage (see nimffi_str); on the response + * side it is heap-allocated and freed by nimffi_free_str. Always NUL-padded by + * one byte after decode so `data` is usable as a C string when it has no + * embedded NULs. */ +typedef struct { + char* data; + size_t len; +} NimFfiStr; + +/* Owned, length-delimited byte buffer (Nim `seq[byte]`). */ +typedef struct { + uint8_t* data; + size_t len; +} NimFfiBytes; + +/* Wrap a borrowed C string for use as a request field. The returned view is + * not owned by the binding and must outlive the call that encodes it. */ +static inline NimFfiStr nimffi_str(const char* s) { + NimFfiStr v; + v.data = (char*)s; + v.len = s ? strlen(s) : 0; + return v; +} + +static inline void nimffi_free_str(NimFfiStr* v) { + if (!v || !v->data) { + return; + } + free(v->data); + v->data = NULL; + v->len = 0; +} + +static inline void nimffi_free_bytes(NimFfiBytes* v) { + if (!v || !v->data) { + return; + } + free(v->data); + v->data = NULL; + v->len = 0; +} + +#ifdef __cplusplus +} +#endif + +#endif /* NIM_FFI_PRELUDE_H_INCLUDED */ diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/CMakeLists.txt.tpl b/wasm-deps/ffi/ffi/codegen/templates/cpp/CMakeLists.txt.tpl new file mode 100644 index 000000000..e8416d660 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/CMakeLists.txt.tpl @@ -0,0 +1,50 @@ +cmake_minimum_required(VERSION 3.14) +project({{LIB}}_cpp_bindings CXX C) + +# The generated bindings target C++20: designated initializers and other +# C++20 constructs are used throughout the emitted code. +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# MSVC defaults __cplusplus to 199711L regardless of the active /std:c++XX +# level — the generated header's C++20 guard would then misfire. /Zc:__cplusplus +# makes MSVC report the actual standard. Harmless on every other compiler. +if(MSVC) + add_compile_options(/Zc:__cplusplus) +endif() + +# ── Locate the repository root (contains ffi.nimble) ───────────────────────── +set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}") +set(REPO_ROOT "") +foreach(_i RANGE 10) + if(EXISTS "${_search_dir}/ffi.nimble") + set(REPO_ROOT "${_search_dir}") + break() + endif() + get_filename_component(_search_dir "${_search_dir}" DIRECTORY) +endforeach() +if("${REPO_ROOT}" STREQUAL "") + message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)") +endif() + +# Build the Nim dylib + vendored TinyCBOR (shared with the C backend). +set(NIM_FFI_LIB {{LIB}}) +set(NIM_FFI_SRC {{SRC}}) +include("${REPO_ROOT}/ffi/codegen/templates/nim_ffi_lib.cmake") + +add_library({{LIB}}_headers INTERFACE) +target_include_directories({{LIB}}_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries({{LIB}}_headers INTERFACE {{LIB}} tinycbor) + +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp") + add_executable({{LIB}}_example main.cpp) + target_link_libraries({{LIB}}_example PRIVATE {{LIB}}_headers) + add_dependencies({{LIB}}_example {{LIB}}_nim_lib) + if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_custom_command(TARGET {{LIB}}_example POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${{{LIB}}_RUNTIME_LIB}" + "$" + COMMENT "Staging {{LIB}}.dll next to {{LIB}}_example.exe") + endif() +endif() diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/cbor_helpers.hpp.tpl b/wasm-deps/ffi/ffi/codegen/templates/cpp/cbor_helpers.hpp.tpl new file mode 100644 index 000000000..ea02efd62 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/cbor_helpers.hpp.tpl @@ -0,0 +1,190 @@ +// ── encode_cbor overloads (primitives + containers) ───────────────────── +// Per-struct encode_cbor / decode_cbor are emitted by cpp.nim next to each +// generated struct; these helpers cover the leaf types they defer into. +// Guarded so two nim-ffi headers can share a translation unit. +#ifndef NIM_FFI_CBOR_HELPERS_HPP_INCLUDED +#define NIM_FFI_CBOR_HELPERS_HPP_INCLUDED + +inline CborError encode_cbor(CborEncoder& e, bool v) { + return cbor_encode_boolean(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, int64_t v) { + return cbor_encode_int(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, int32_t v) { + return cbor_encode_int(&e, static_cast(v)); +} +inline CborError encode_cbor(CborEncoder& e, uint64_t v) { + return cbor_encode_uint(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, double v) { + return cbor_encode_double(&e, v); +} +inline CborError encode_cbor(CborEncoder& e, const std::string& v) { + return cbor_encode_text_string(&e, v.data(), v.size()); +} + +template +inline CborError encode_cbor(CborEncoder& e, const std::vector& v) { + CborEncoder arr; + CborError err = cbor_encoder_create_array(&e, &arr, v.size()); + if (err) return err; + for (const auto& item : v) { + err = encode_cbor(arr, item); + if (err) return err; + } + return cbor_encoder_close_container(&e, &arr); +} + +// `seq[byte]` rides the wire as a CBOR byte string (major type 2), matching +// Nim's cbor_serialization. This non-template overload beats the std::vector +// template in overload resolution, so std::vector fields use it +// automatically. +inline CborError encode_cbor(CborEncoder& e, const std::vector& v) { + return cbor_encode_byte_string(&e, v.data(), v.size()); +} + +template +inline CborError encode_cbor(CborEncoder& e, const std::optional& v) { + if (!v) return cbor_encode_null(&e); + return encode_cbor(e, *v); +} + +// ── decode_cbor overloads ─────────────────────────────────────────────── + +// After reading a leaf value, the parser must advance past it; both steps +// short-circuit on the same CborError, so they always travel together. +inline CborError advance_if_ok(CborValue& it, CborError err) { + if (err) return err; + return cbor_value_advance(&it); +} + +inline CborError decode_cbor(CborValue& it, bool& out) { + if (!cbor_value_is_boolean(&it)) return CborErrorImproperValue; + return advance_if_ok(it, cbor_value_get_boolean(&it, &out)); +} +inline CborError decode_cbor(CborValue& it, int64_t& out) { + if (!cbor_value_is_integer(&it)) return CborErrorImproperValue; + return advance_if_ok(it, cbor_value_get_int64_checked(&it, &out)); +} +inline CborError decode_cbor(CborValue& it, int32_t& out) { + int64_t tmp = 0; + CborError err = decode_cbor(it, tmp); + if (err) return err; + out = static_cast(tmp); + return CborNoError; +} +inline CborError decode_cbor(CborValue& it, uint64_t& out) { + if (!cbor_value_is_unsigned_integer(&it)) return CborErrorImproperValue; + return advance_if_ok(it, cbor_value_get_uint64(&it, &out)); +} +inline CborError decode_cbor(CborValue& it, double& out) { + if (cbor_value_is_double(&it)) { + return advance_if_ok(it, cbor_value_get_double(&it, &out)); + } + if (cbor_value_is_float(&it)) { + float f = 0.0f; + CborError err = cbor_value_get_float(&it, &f); + if (err) return err; + out = static_cast(f); + return cbor_value_advance(&it); + } + return CborErrorImproperValue; +} +inline CborError decode_cbor(CborValue& it, std::string& out) { + if (!cbor_value_is_text_string(&it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_string_length(&it, &len); + if (err) return err; + out.resize(len); + return advance_if_ok( + it, cbor_value_copy_text_string(&it, out.empty() ? nullptr : &out[0], &len, nullptr)); +} + +template +inline CborError decode_cbor(CborValue& it, std::vector& out) { + if (!cbor_value_is_array(&it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_array_length(&it, &len); + if (err) return err; + out.clear(); + out.resize(len); + CborValue inner; + err = cbor_value_enter_container(&it, &inner); + if (err) return err; + for (size_t i = 0; i < len; ++i) { + err = decode_cbor(inner, out[i]); + if (err) return err; + } + return cbor_value_leave_container(&it, &inner); +} + +// Counterpart to the byte-string encoder above: decode a CBOR byte string +// (major type 2) back into std::vector. +inline CborError decode_cbor(CborValue& it, std::vector& out) { + if (!cbor_value_is_byte_string(&it)) return CborErrorImproperValue; + size_t len = 0; + CborError err = cbor_value_get_string_length(&it, &len); + if (err) return err; + out.resize(len); + return advance_if_ok( + it, cbor_value_copy_byte_string(&it, out.empty() ? nullptr : out.data(), &len, nullptr)); +} + +template +inline CborError decode_cbor(CborValue& it, std::optional& out) { + if (cbor_value_is_null(&it)) { + out = std::nullopt; + return cbor_value_advance(&it); + } + T tmp{}; + CborError err = decode_cbor(it, tmp); + if (err) return err; + out = std::move(tmp); + return CborNoError; +} + +// ── Public entry points ───────────────────────────────────────────────── + +template +inline Result> encodeCborFFI(const T& value) { + // Start with a generous 4 KiB buffer; double on overflow until it fits. + std::vector buf(4096); + while (true) { + CborEncoder enc; + cbor_encoder_init(&enc, buf.data(), buf.size(), 0); + CborError err = encode_cbor(enc, value); + if (err == CborNoError) { + const size_t used = cbor_encoder_get_buffer_size(&enc, buf.data()); + buf.resize(used); + return Result>::ok(std::move(buf)); + } + if (err == CborErrorOutOfMemory) { + const size_t extra = cbor_encoder_get_extra_bytes_needed(&enc); + buf.resize(buf.size() + (extra > 0 ? extra : buf.size())); + continue; + } + return Result>::err( + std::string("FFI CBOR encode failed: ") + cbor_error_string(err)); + } +} + +template +inline Result decodeCborFFI(const std::vector& bytes) { + CborParser parser; + CborValue it; + CborError err = cbor_parser_init(bytes.data(), bytes.size(), 0, &parser, &it); + if (err != CborNoError) { + return Result::err(std::string("FFI CBOR parse init failed: ") + + cbor_error_string(err)); + } + T out{}; + err = decode_cbor(it, out); + if (err != CborNoError) { + return Result::err(std::string("FFI CBOR decode failed: ") + + cbor_error_string(err)); + } + return Result::ok(std::move(out)); +} + +#endif // NIM_FFI_CBOR_HELPERS_HPP_INCLUDED diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/context_rule_of_5.hpp.tpl b/wasm-deps/ffi/ffi/codegen/templates/cpp/context_rule_of_5.hpp.tpl new file mode 100644 index 000000000..22ce19d81 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/context_rule_of_5.hpp.tpl @@ -0,0 +1,20 @@ + // Special-member policy: this class owns a {{LIB}} context, which in + // turn owns the library's worker thread(s) and internal state. Moving + // such an object out from under a caller silently tears that state + // down and is easy to misuse (e.g. storing in a container that + // relocates its elements). It also has no clean analogue in the other + // binding languages we generate. So copies and moves are both + // deleted; ownership is transferred via {{CTX}}::create returning a + // std::unique_ptr<{{CTX}}>. The destructor still releases the + // context. + ~{{CTX}}() { + if (ptr_) { + {{LIB}}_destroy(ptr_); + ptr_ = nullptr; + } + } + + {{CTX}}(const {{CTX}}&) = delete; + {{CTX}}& operator=(const {{CTX}}&) = delete; + {{CTX}}({{CTX}}&&) = delete; + {{CTX}}& operator=({{CTX}}&&) = delete; diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/header_prelude.hpp.tpl b/wasm-deps/ffi/ffi/codegen/templates/cpp/header_prelude.hpp.tpl new file mode 100644 index 000000000..91713efa2 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/header_prelude.hpp.tpl @@ -0,0 +1,40 @@ +#pragma once +// Generated bindings require C++20 (designated initializers and other +// C++20 constructs are used throughout the emitted code). +// MSVC keeps __cplusplus at 199711L unless /Zc:__cplusplus is passed, +// so consult _MSVC_LANG when present (it always reflects the active +// /std:c++XX level). +#if defined(_MSVC_LANG) +# if _MSVC_LANG < 202002L +# error "nim-ffi generated headers require C++20 or later (use /std:c++20)" +# endif +#elif !defined(__cplusplus) || __cplusplus < 202002L +# error "nim-ffi generated headers require C++20 or later" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +extern "C" { +#include +} + +// nim-ffi result-callback status codes (mirror ffi/ffi_types.nim and the C +// header). Guarded so a translation unit that also pulls in the C header keeps +// a single definition. +#ifndef NIMFFI_RET_OK +#define NIMFFI_RET_OK 0 +#define NIMFFI_RET_ERR 1 +#define NIMFFI_RET_MISSING_CALLBACK 2 +#define NIMFFI_RET_STALE_WARN 3 +#endif diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/result.hpp.tpl b/wasm-deps/ffi/ffi/codegen/templates/cpp/result.hpp.tpl new file mode 100644 index 000000000..d58884cf5 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/result.hpp.tpl @@ -0,0 +1,61 @@ +// ============================================================ +// Result — exception-free error channel +// ============================================================ +// The generated bindings never throw: every fallible entry point (create, +// instance methods, and their *Async futures) returns a Result. Callers +// branch on isOk()/isErr() (or the explicit bool conversion) and read +// value()/error(). This mirrors the Nim side's Result[T, string] and keeps +// us off C++23's std::expected. +#ifndef NIM_FFI_RESULT_HPP_INCLUDED +#define NIM_FFI_RESULT_HPP_INCLUDED + +template +class Result { + std::optional value_; + std::string error_; +public: + static Result ok(T value) { + Result r; + r.value_ = std::move(value); + return r; + } + static Result err(std::string message) { + Result r; + r.error_ = std::move(message); + return r; + } + bool isOk() const { return value_.has_value(); } + bool isErr() const { return !value_.has_value(); } + explicit operator bool() const { return isOk(); } + const T& value() const { assert(value_.has_value() && "Result::value() called on err Result — check isOk() first"); return *value_; } + T& value() { assert(value_.has_value() && "Result::value() called on err Result — check isOk() first"); return *value_; } + const T& operator*() const { assert(value_.has_value() && "Result::operator*() called on err Result — check isOk() first"); return *value_; } + const T* operator->() const { assert(value_.has_value() && "Result::operator->() called on err Result — check isOk() first"); return &*value_; } + T&& take() { assert(value_.has_value() && "Result::take() called on err Result — check isOk() first"); return std::move(*value_); } + const std::string& error() const { assert(!value_.has_value() && "Result::error() called on ok Result — check isErr() first"); return error_; } +}; + +template <> +class Result { + bool ok_ = true; + std::string error_; +public: + static Result ok() { + Result r; + r.ok_ = true; + return r; + } + static Result err(std::string message) { + Result r; + r.ok_ = false; + r.error_ = std::move(message); + return r; + } + Result() = default; + bool isOk() const { return ok_; } + bool isErr() const { return !ok_; } + explicit operator bool() const { return isOk(); } + const std::string& error() const { assert(!ok_ && "Result::error() called on ok Result — check isErr() first"); return error_; } +}; + +#endif // NIM_FFI_RESULT_HPP_INCLUDED diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/sync_call_helper.hpp.tpl b/wasm-deps/ffi/ffi/codegen/templates/cpp/sync_call_helper.hpp.tpl new file mode 100644 index 000000000..8229383ec --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/sync_call_helper.hpp.tpl @@ -0,0 +1,66 @@ +// ============================================================ +// Synchronous call helper +// ============================================================ +// Guarded so two nim-ffi headers can share a translation unit. +#ifndef NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED +#define NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED + +namespace { + +struct FFICallState_ { + std::mutex mtx; + std::condition_variable cv; + bool done{false}; + bool ok{false}; + std::vector bytes; + std::string err; +}; + +inline void ffi_cb_(int ret, const char* msg, size_t len, void* ud) { + // NIMFFI_RET_STALE_WARN (3) is a non-terminal progress ping: the request is + // still running. This blocking wrapper only reports the final result, so + // ignore it WITHOUT touching `ud` — a terminal callback still owns the + // shared handle and will free it. + if (ret == NIMFFI_RET_STALE_WARN) return; + + // ffi_call_ heap-allocated a shared_ptr and passed its address as ud; + // take ownership here so it's freed on every exit path. + std::unique_ptr> handle( + static_cast*>(ud)); + FFICallState_& s = **handle; + + std::lock_guard lock(s.mtx); + s.ok = (ret == NIMFFI_RET_OK); + if (msg && len > 0) { + const auto* p = reinterpret_cast(msg); + if (s.ok) s.bytes.assign(p, p + len); + else s.err.assign(msg, len); + } + s.done = true; + s.cv.notify_one(); +} + +inline Result> ffi_call_( + std::function f, + std::chrono::milliseconds timeout) { + using Bytes = std::vector; + auto state = std::make_shared(); + auto* cb_ref = new std::shared_ptr(state); + const int ret = f(ffi_cb_, cb_ref); + if (ret == NIMFFI_RET_MISSING_CALLBACK) { + delete cb_ref; + return Result::err("RET_MISSING_CALLBACK (internal error)"); + } + std::unique_lock lock(state->mtx); + const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; }); + if (!fired) + return Result::err("FFI call timed out after " + + std::to_string(timeout.count()) + "ms"); + if (!state->ok) + return Result::err(state->err); + return Result::ok(std::move(state->bytes)); +} + +} // anonymous namespace + +#endif // NIM_FFI_SYNC_CALL_HELPER_HPP_INCLUDED diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/LICENSE b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/LICENSE new file mode 100644 index 000000000..4aad977ce --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Intel Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cbor.h b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cbor.h new file mode 100644 index 000000000..be5bbc77a --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cbor.h @@ -0,0 +1,724 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#ifndef CBOR_H +#define CBOR_H + +#ifndef assert +#include +#endif +#include +#include +#include +#include +#include + +#include "tinycbor-version.h" + +#define TINYCBOR_VERSION ((TINYCBOR_VERSION_MAJOR << 16) | (TINYCBOR_VERSION_MINOR << 8) | TINYCBOR_VERSION_PATCH) + +#ifdef __cplusplus +extern "C" { +#else +#include +#endif + +#ifndef SIZE_MAX +/* Some systems fail to define SIZE_MAX in , even though C99 requires it... + * Conversion from signed to unsigned is defined in 6.3.1.3 (Signed and unsigned integers) p2, + * which says: "the value is converted by repeatedly adding or subtracting one more than the + * maximum value that can be represented in the new type until the value is in the range of the + * new type." + * So -1 gets converted to size_t by adding SIZE_MAX + 1, which results in SIZE_MAX. + */ +# define SIZE_MAX ((size_t)-1) +#endif + +#ifndef CBOR_API +# define CBOR_API +#endif +#ifndef CBOR_PRIVATE_API +# define CBOR_PRIVATE_API +#endif +#ifndef CBOR_INLINE_API +# if defined(__cplusplus) +# define CBOR_INLINE inline +# define CBOR_INLINE_API inline +# else +# define CBOR_INLINE_API static CBOR_INLINE +# if defined(_MSC_VER) +# define CBOR_INLINE __inline +# elif defined(__GNUC__) +# define CBOR_INLINE __inline__ +# elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L +# define CBOR_INLINE inline +# else +# define CBOR_INLINE +# endif +# endif +#endif + +typedef enum CborType { + CborIntegerType = 0x00, + CborByteStringType = 0x40, + CborTextStringType = 0x60, + CborArrayType = 0x80, + CborMapType = 0xa0, + CborTagType = 0xc0, + CborSimpleType = 0xe0, + CborBooleanType = 0xf5, + CborNullType = 0xf6, + CborUndefinedType = 0xf7, + CborHalfFloatType = 0xf9, + CborFloatType = 0xfa, + CborDoubleType = 0xfb, + + CborInvalidType = 0xff /* equivalent to the break byte, so it will never be used */ +} CborType; + +typedef uint64_t CborTag; +typedef enum CborKnownTags { + CborDateTimeStringTag = 0, + CborUnixTime_tTag = 1, + CborPositiveBignumTag = 2, + CborNegativeBignumTag = 3, + CborDecimalTag = 4, + CborBigfloatTag = 5, + CborCOSE_Encrypt0Tag = 16, + CborCOSE_Mac0Tag = 17, + CborCOSE_Sign1Tag = 18, + CborExpectedBase64urlTag = 21, + CborExpectedBase64Tag = 22, + CborExpectedBase16Tag = 23, + CborEncodedCborTag = 24, + CborUrlTag = 32, + CborBase64urlTag = 33, + CborBase64Tag = 34, + CborRegularExpressionTag = 35, + CborMimeMessageTag = 36, + CborCOSE_EncryptTag = 96, + CborCOSE_MacTag = 97, + CborCOSE_SignTag = 98, + CborSignatureTag = 55799 +} CborKnownTags; + +/* #define the constants so we can check with #ifdef */ +#define CborDateTimeStringTag CborDateTimeStringTag +#define CborUnixTime_tTag CborUnixTime_tTag +#define CborPositiveBignumTag CborPositiveBignumTag +#define CborNegativeBignumTag CborNegativeBignumTag +#define CborDecimalTag CborDecimalTag +#define CborBigfloatTag CborBigfloatTag +#define CborCOSE_Encrypt0Tag CborCOSE_Encrypt0Tag +#define CborCOSE_Mac0Tag CborCOSE_Mac0Tag +#define CborCOSE_Sign1Tag CborCOSE_Sign1Tag +#define CborExpectedBase64urlTag CborExpectedBase64urlTag +#define CborExpectedBase64Tag CborExpectedBase64Tag +#define CborExpectedBase16Tag CborExpectedBase16Tag +#define CborEncodedCborTag CborEncodedCborTag +#define CborUrlTag CborUrlTag +#define CborBase64urlTag CborBase64urlTag +#define CborBase64Tag CborBase64Tag +#define CborRegularExpressionTag CborRegularExpressionTag +#define CborMimeMessageTag CborMimeMessageTag +#define CborCOSE_EncryptTag CborCOSE_EncryptTag +#define CborCOSE_MacTag CborCOSE_MacTag +#define CborCOSE_SignTag CborCOSE_SignTag +#define CborSignatureTag CborSignatureTag + +/* Error API */ + +typedef enum CborError { + CborNoError = 0, + + /* errors in all modes */ + CborUnknownError, + CborErrorUnknownLength, /* request for length in array, map, or string with indeterminate length */ + CborErrorAdvancePastEOF, + CborErrorIO, + + /* parser errors streaming errors */ + CborErrorGarbageAtEnd = 256, + CborErrorUnexpectedEOF, + CborErrorUnexpectedBreak, + CborErrorUnknownType, /* can only happen in major type 7 */ + CborErrorIllegalType, /* type not allowed here */ + CborErrorIllegalNumber, + CborErrorIllegalSimpleType, /* types of value less than 32 encoded in two bytes */ + CborErrorNoMoreStringChunks, + + /* parser errors in strict mode parsing only */ + CborErrorUnknownSimpleType = 512, + CborErrorUnknownTag, + CborErrorInappropriateTagForType, + CborErrorDuplicateObjectKeys, + CborErrorInvalidUtf8TextString, + CborErrorExcludedType, + CborErrorExcludedValue, + CborErrorImproperValue, + CborErrorOverlongEncoding, + CborErrorMapKeyNotString, + CborErrorMapNotSorted, + CborErrorMapKeysNotUnique, + + /* encoder errors */ + CborErrorTooManyItems = 768, + CborErrorTooFewItems, + + /* internal implementation errors */ + CborErrorDataTooLarge = 1024, + CborErrorNestingTooDeep, + CborErrorUnsupportedType, + CborErrorUnimplementedValidation, + + /* errors in converting to JSON */ + CborErrorJsonObjectKeyIsAggregate = 1280, + CborErrorJsonObjectKeyNotString, + CborErrorJsonNotImplemented, + + CborErrorOutOfMemory = (int) (~0U / 2 + 1), + CborErrorInternalError = (int) (~0U / 2) /* INT_MAX on two's complement machines */ +} CborError; + +CBOR_API const char *cbor_error_string(CborError error); + +/* Encoder API */ + +typedef enum CborEncoderAppendType +{ + CborEncoderAppendCborData = 0, + CborEncoderAppendStringData = 1 +} CborEncoderAppendType; + +typedef CborError (*CborEncoderWriteFunction)(void *, const void *, size_t, CborEncoderAppendType); + +enum CborEncoderFlags +{ + CborIteratorFlag_WriterFunction = 0x01, + CborIteratorFlag_ContainerIsMap_ = 0x20 +}; + +struct CborEncoder +{ + union { + uint8_t *ptr; + ptrdiff_t bytes_needed; + CborEncoderWriteFunction writer; + } data; + uint8_t *end; + size_t remaining; + int flags; +}; +typedef struct CborEncoder CborEncoder; + +static const size_t CborIndefiniteLength = SIZE_MAX; + +#ifndef CBOR_NO_ENCODER_API +CBOR_API void cbor_encoder_init(CborEncoder *encoder, uint8_t *buffer, size_t size, int flags); +CBOR_API void cbor_encoder_init_writer(CborEncoder *encoder, CborEncoderWriteFunction writer, void *); +CBOR_API CborError cbor_encode_uint(CborEncoder *encoder, uint64_t value); +CBOR_API CborError cbor_encode_int(CborEncoder *encoder, int64_t value); +CBOR_API CborError cbor_encode_negative_int(CborEncoder *encoder, uint64_t absolute_value); +CBOR_API CborError cbor_encode_simple_value(CborEncoder *encoder, uint8_t value); +CBOR_API CborError cbor_encode_tag(CborEncoder *encoder, CborTag tag); +CBOR_API CborError cbor_encode_text_string(CborEncoder *encoder, const char *string, size_t length); +CBOR_INLINE_API CborError cbor_encode_text_stringz(CborEncoder *encoder, const char *string) +{ return cbor_encode_text_string(encoder, string, strlen(string)); } +CBOR_API CborError cbor_encode_byte_string(CborEncoder *encoder, const uint8_t *string, size_t length); +CBOR_API CborError cbor_encode_floating_point(CborEncoder *encoder, CborType fpType, const void *value); + +CBOR_INLINE_API CborError cbor_encode_boolean(CborEncoder *encoder, bool value) +{ return cbor_encode_simple_value(encoder, (int)value - 1 + (CborBooleanType & 0x1f)); } +CBOR_INLINE_API CborError cbor_encode_null(CborEncoder *encoder) +{ return cbor_encode_simple_value(encoder, CborNullType & 0x1f); } +CBOR_INLINE_API CborError cbor_encode_undefined(CborEncoder *encoder) +{ return cbor_encode_simple_value(encoder, CborUndefinedType & 0x1f); } + +CBOR_INLINE_API CborError cbor_encode_half_float(CborEncoder *encoder, const void *value) +{ return cbor_encode_floating_point(encoder, CborHalfFloatType, value); } +CBOR_API CborError cbor_encode_float_as_half_float(CborEncoder *encoder, float value); +CBOR_INLINE_API CborError cbor_encode_float(CborEncoder *encoder, float value) +{ return cbor_encode_floating_point(encoder, CborFloatType, &value); } +CBOR_INLINE_API CborError cbor_encode_double(CborEncoder *encoder, double value) +{ return cbor_encode_floating_point(encoder, CborDoubleType, &value); } + +CBOR_API CborError cbor_encoder_create_array(CborEncoder *parentEncoder, CborEncoder *arrayEncoder, size_t length); +CBOR_API CborError cbor_encoder_create_map(CborEncoder *parentEncoder, CborEncoder *mapEncoder, size_t length); +CBOR_API CborError cbor_encoder_close_container(CborEncoder *parentEncoder, const CborEncoder *containerEncoder); +CBOR_API CborError cbor_encoder_close_container_checked(CborEncoder *parentEncoder, const CborEncoder *containerEncoder); + +CBOR_INLINE_API uint8_t *_cbor_encoder_get_buffer_pointer(const CborEncoder *encoder) +{ + return encoder->data.ptr; +} + +CBOR_INLINE_API size_t cbor_encoder_get_buffer_size(const CborEncoder *encoder, const uint8_t *buffer) +{ + return (size_t)(encoder->data.ptr - buffer); +} + +CBOR_INLINE_API size_t cbor_encoder_get_extra_bytes_needed(const CborEncoder *encoder) +{ + return encoder->end ? 0 : (size_t)encoder->data.bytes_needed; +} +#endif /* CBOR_NO_ENCODER_API */ + +/* Parser API */ + +enum CborParserGlobalFlags +{ + CborParserFlag_ExternalSource = 0x01 +}; + +enum CborParserIteratorFlags +{ + /* used for all types, but not during string chunk iteration + * (values are static-asserted, don't change) */ + CborIteratorFlag_IntegerValueIs64Bit = 0x01, + CborIteratorFlag_IntegerValueTooLarge = 0x02, + + /* used only for CborIntegerType */ + CborIteratorFlag_NegativeInteger = 0x04, + + /* used only during string iteration */ + CborIteratorFlag_BeforeFirstStringChunk = 0x04, + CborIteratorFlag_IteratingStringChunks = 0x08, + + /* used for arrays, maps and strings, including during chunk iteration */ + CborIteratorFlag_UnknownLength = 0x10, + + /* used for maps, but must be kept for all types + * (ContainerIsMap value must be CborMapType - CborArrayType) */ + CborIteratorFlag_ContainerIsMap = 0x20, + CborIteratorFlag_NextIsMapKey = 0x40 +}; + +struct CborValue; +struct CborParserOperations +{ + bool (*can_read_bytes)(void *token, size_t len); + void *(*read_bytes)(void *token, void *dst, size_t offset, size_t len); + void (*advance_bytes)(void *token, size_t len); + CborError (*transfer_string)(void *token, const void **userptr, size_t offset, size_t len); +}; + +struct CborParser +{ + union { + const uint8_t *end; + const struct CborParserOperations *ops; + } source; + enum CborParserGlobalFlags flags; +}; +typedef struct CborParser CborParser; + +struct CborValue +{ + const CborParser *parser; + union { + const uint8_t *ptr; + void *token; + } source; + uint32_t remaining; + uint16_t extra; + uint8_t type; + uint8_t flags; +}; +typedef struct CborValue CborValue; + +#ifndef CBOR_NO_PARSER_API +CBOR_API CborError cbor_parser_init(const uint8_t *buffer, size_t size, uint32_t flags, CborParser *parser, CborValue *it); +CBOR_API CborError cbor_parser_init_reader(const struct CborParserOperations *ops, CborParser *parser, CborValue *it, void *token); + +CBOR_API CborError cbor_value_validate_basic(const CborValue *it); + +CBOR_INLINE_API bool cbor_value_at_end(const CborValue *it) +{ return it->remaining == 0; } +CBOR_INLINE_API const uint8_t *cbor_value_get_next_byte(const CborValue *it) +{ return it->source.ptr; } +CBOR_API CborError cbor_value_reparse(CborValue *it); +CBOR_API CborError cbor_value_advance_fixed(CborValue *it); +CBOR_API CborError cbor_value_advance(CborValue *it); +CBOR_INLINE_API bool cbor_value_is_container(const CborValue *it) +{ return it->type == CborArrayType || it->type == CborMapType; } +CBOR_API CborError cbor_value_enter_container(const CborValue *it, CborValue *recursed); +CBOR_API CborError cbor_value_leave_container(CborValue *it, const CborValue *recursed); + +CBOR_PRIVATE_API uint64_t _cbor_value_decode_int64_internal(const CborValue *value); +CBOR_INLINE_API uint64_t _cbor_value_extract_int64_helper(const CborValue *value) +{ + return value->flags & CborIteratorFlag_IntegerValueTooLarge ? + _cbor_value_decode_int64_internal(value) : value->extra; +} + +CBOR_INLINE_API bool cbor_value_is_valid(const CborValue *value) +{ return value && value->type != CborInvalidType; } +CBOR_INLINE_API CborType cbor_value_get_type(const CborValue *value) +{ return (CborType)value->type; } + +/* Null & undefined type */ +CBOR_INLINE_API bool cbor_value_is_null(const CborValue *value) +{ return value->type == CborNullType; } +CBOR_INLINE_API bool cbor_value_is_undefined(const CborValue *value) +{ return value->type == CborUndefinedType; } + +/* Booleans */ +CBOR_INLINE_API bool cbor_value_is_boolean(const CborValue *value) +{ return value->type == CborBooleanType; } +CBOR_INLINE_API CborError cbor_value_get_boolean(const CborValue *value, bool *result) +{ + assert(cbor_value_is_boolean(value)); + *result = !!value->extra; + return CborNoError; +} + +/* Simple types */ +CBOR_INLINE_API bool cbor_value_is_simple_type(const CborValue *value) +{ return value->type == CborSimpleType; } +CBOR_INLINE_API CborError cbor_value_get_simple_type(const CborValue *value, uint8_t *result) +{ + assert(cbor_value_is_simple_type(value)); + *result = (uint8_t)value->extra; + return CborNoError; +} + +/* Integers */ +CBOR_INLINE_API bool cbor_value_is_integer(const CborValue *value) +{ return value->type == CborIntegerType; } +CBOR_INLINE_API bool cbor_value_is_unsigned_integer(const CborValue *value) +{ return cbor_value_is_integer(value) && (value->flags & CborIteratorFlag_NegativeInteger) == 0; } +CBOR_INLINE_API bool cbor_value_is_negative_integer(const CborValue *value) +{ return cbor_value_is_integer(value) && (value->flags & CborIteratorFlag_NegativeInteger); } + +CBOR_INLINE_API CborError cbor_value_get_raw_integer(const CborValue *value, uint64_t *result) +{ + assert(cbor_value_is_integer(value)); + *result = _cbor_value_extract_int64_helper(value); + return CborNoError; +} + +CBOR_INLINE_API CborError cbor_value_get_uint64(const CborValue *value, uint64_t *result) +{ + assert(cbor_value_is_unsigned_integer(value)); + *result = _cbor_value_extract_int64_helper(value); + return CborNoError; +} + +CBOR_INLINE_API CborError cbor_value_get_int64(const CborValue *value, int64_t *result) +{ + assert(cbor_value_is_integer(value)); + *result = (int64_t) _cbor_value_extract_int64_helper(value); + if (value->flags & CborIteratorFlag_NegativeInteger) + *result = -*result - 1; + return CborNoError; +} + +CBOR_INLINE_API CborError cbor_value_get_int(const CborValue *value, int *result) +{ + assert(cbor_value_is_integer(value)); + *result = (int) _cbor_value_extract_int64_helper(value); + if (value->flags & CborIteratorFlag_NegativeInteger) + *result = -*result - 1; + return CborNoError; +} + +CBOR_API CborError cbor_value_get_int64_checked(const CborValue *value, int64_t *result); +CBOR_API CborError cbor_value_get_int_checked(const CborValue *value, int *result); + +CBOR_INLINE_API bool cbor_value_is_length_known(const CborValue *value) +{ return (value->flags & CborIteratorFlag_UnknownLength) == 0; } + +/* Tags */ +CBOR_INLINE_API bool cbor_value_is_tag(const CborValue *value) +{ return value->type == CborTagType; } +CBOR_INLINE_API CborError cbor_value_get_tag(const CborValue *value, CborTag *result) +{ + assert(cbor_value_is_tag(value)); + *result = _cbor_value_extract_int64_helper(value); + return CborNoError; +} +CBOR_API CborError cbor_value_skip_tag(CborValue *it); + +/* Strings */ +CBOR_INLINE_API bool cbor_value_is_byte_string(const CborValue *value) +{ return value->type == CborByteStringType; } +CBOR_INLINE_API bool cbor_value_is_text_string(const CborValue *value) +{ return value->type == CborTextStringType; } + +CBOR_INLINE_API CborError cbor_value_get_string_length(const CborValue *value, size_t *length) +{ + uint64_t v; + assert(cbor_value_is_byte_string(value) || cbor_value_is_text_string(value)); + if (!cbor_value_is_length_known(value)) + return CborErrorUnknownLength; + v = _cbor_value_extract_int64_helper(value); + *length = (size_t)v; + if (*length != v) + return CborErrorDataTooLarge; + return CborNoError; +} + +CBOR_PRIVATE_API CborError _cbor_value_copy_string(const CborValue *value, void *buffer, + size_t *buflen, CborValue *next); +CBOR_PRIVATE_API CborError _cbor_value_dup_string(const CborValue *value, void **buffer, + size_t *buflen, CborValue *next); + +CBOR_API CborError cbor_value_calculate_string_length(const CborValue *value, size_t *length); + +CBOR_INLINE_API CborError cbor_value_copy_text_string(const CborValue *value, char *buffer, + size_t *buflen, CborValue *next) +{ + assert(cbor_value_is_text_string(value)); + return _cbor_value_copy_string(value, buffer, buflen, next); +} +CBOR_INLINE_API CborError cbor_value_copy_byte_string(const CborValue *value, uint8_t *buffer, + size_t *buflen, CborValue *next) +{ + assert(cbor_value_is_byte_string(value)); + return _cbor_value_copy_string(value, buffer, buflen, next); +} + +CBOR_INLINE_API CborError cbor_value_dup_text_string(const CborValue *value, char **buffer, + size_t *buflen, CborValue *next) +{ + assert(cbor_value_is_text_string(value)); + return _cbor_value_dup_string(value, (void **)buffer, buflen, next); +} +CBOR_INLINE_API CborError cbor_value_dup_byte_string(const CborValue *value, uint8_t **buffer, + size_t *buflen, CborValue *next) +{ + assert(cbor_value_is_byte_string(value)); + return _cbor_value_dup_string(value, (void **)buffer, buflen, next); +} + +CBOR_PRIVATE_API CborError _cbor_value_get_string_chunk_size(const CborValue *value, size_t *len); +CBOR_INLINE_API CborError cbor_value_get_string_chunk_size(const CborValue *value, size_t *len) +{ + assert(value->flags & CborIteratorFlag_IteratingStringChunks); + return _cbor_value_get_string_chunk_size(value, len); +} + +CBOR_INLINE_API bool cbor_value_string_iteration_at_end(const CborValue *value) +{ + size_t dummy; + return cbor_value_get_string_chunk_size(value, &dummy) == CborErrorNoMoreStringChunks; +} + +CBOR_PRIVATE_API CborError _cbor_value_begin_string_iteration(CborValue *value); +CBOR_INLINE_API CborError cbor_value_begin_string_iteration(CborValue *value) +{ + assert(cbor_value_is_text_string(value) || cbor_value_is_byte_string(value)); + assert(!(value->flags & CborIteratorFlag_IteratingStringChunks)); + return _cbor_value_begin_string_iteration(value); +} + +CBOR_PRIVATE_API CborError _cbor_value_finish_string_iteration(CborValue *value); +CBOR_INLINE_API CborError cbor_value_finish_string_iteration(CborValue *value) +{ + assert(cbor_value_string_iteration_at_end(value)); + return _cbor_value_finish_string_iteration(value); +} + +CBOR_PRIVATE_API CborError _cbor_value_get_string_chunk(const CborValue *value, const void **bufferptr, + size_t *len, CborValue *next); +CBOR_INLINE_API CborError cbor_value_get_text_string_chunk(const CborValue *value, const char **bufferptr, + size_t *len, CborValue *next) +{ + assert(cbor_value_is_text_string(value)); + return _cbor_value_get_string_chunk(value, (const void **)bufferptr, len, next); +} +CBOR_INLINE_API CborError cbor_value_get_byte_string_chunk(const CborValue *value, const uint8_t **bufferptr, + size_t *len, CborValue *next) +{ + assert(cbor_value_is_byte_string(value)); + return _cbor_value_get_string_chunk(value, (const void **)bufferptr, len, next); +} + +CBOR_API CborError cbor_value_text_string_equals(const CborValue *value, const char *string, bool *result); + +/* Maps and arrays */ +CBOR_INLINE_API bool cbor_value_is_array(const CborValue *value) +{ return value->type == CborArrayType; } +CBOR_INLINE_API bool cbor_value_is_map(const CborValue *value) +{ return value->type == CborMapType; } + +CBOR_INLINE_API CborError cbor_value_get_array_length(const CborValue *value, size_t *length) +{ + uint64_t v; + assert(cbor_value_is_array(value)); + if (!cbor_value_is_length_known(value)) + return CborErrorUnknownLength; + v = _cbor_value_extract_int64_helper(value); + *length = (size_t)v; + if (*length != v) + return CborErrorDataTooLarge; + return CborNoError; +} + +CBOR_INLINE_API CborError cbor_value_get_map_length(const CborValue *value, size_t *length) +{ + uint64_t v; + assert(cbor_value_is_map(value)); + if (!cbor_value_is_length_known(value)) + return CborErrorUnknownLength; + v = _cbor_value_extract_int64_helper(value); + *length = (size_t)v; + if (*length != v) + return CborErrorDataTooLarge; + return CborNoError; +} + +CBOR_API CborError cbor_value_map_find_value(const CborValue *map, const char *string, CborValue *element); + +/* Floating point */ +CBOR_INLINE_API bool cbor_value_is_half_float(const CborValue *value) +{ return value->type == CborHalfFloatType; } +CBOR_API CborError cbor_value_get_half_float_as_float(const CborValue *value, float *result); +CBOR_INLINE_API CborError cbor_value_get_half_float(const CborValue *value, void *result) +{ + assert(cbor_value_is_half_float(value)); + assert((value->flags & CborIteratorFlag_IntegerValueTooLarge) == 0); + + /* size has already been computed */ + memcpy(result, &value->extra, sizeof(value->extra)); + return CborNoError; +} + +CBOR_INLINE_API bool cbor_value_is_float(const CborValue *value) +{ return value->type == CborFloatType; } +CBOR_INLINE_API CborError cbor_value_get_float(const CborValue *value, float *result) +{ + uint32_t data; + assert(cbor_value_is_float(value)); + assert(value->flags & CborIteratorFlag_IntegerValueTooLarge); + data = (uint32_t)_cbor_value_decode_int64_internal(value); + memcpy(result, &data, sizeof(*result)); + return CborNoError; +} + +CBOR_INLINE_API bool cbor_value_is_double(const CborValue *value) +{ return value->type == CborDoubleType; } +CBOR_INLINE_API CborError cbor_value_get_double(const CborValue *value, double *result) +{ + uint64_t data; + assert(cbor_value_is_double(value)); + assert(value->flags & CborIteratorFlag_IntegerValueTooLarge); + data = _cbor_value_decode_int64_internal(value); + memcpy(result, &data, sizeof(*result)); + return CborNoError; +} + +/* Validation API */ +#ifndef CBOR_NO_VALIDATION_API + +enum CborValidationFlags { + /* Bit mapping: + * bits 0-7 (8 bits): canonical format + * bits 8-11 (4 bits): canonical format & strict mode + * bits 12-20 (8 bits): strict mode + * bits 21-31 (10 bits): other + */ + + CborValidateShortestIntegrals = 0x0001, + CborValidateShortestFloatingPoint = 0x0002, + CborValidateShortestNumbers = CborValidateShortestIntegrals | CborValidateShortestFloatingPoint, + CborValidateNoIndeterminateLength = 0x0100, + CborValidateMapIsSorted = 0x0200 | CborValidateNoIndeterminateLength, + + CborValidateCanonicalFormat = 0x0fff, + + CborValidateMapKeysAreUnique = 0x1000 | CborValidateMapIsSorted, + CborValidateTagUse = 0x2000, + CborValidateUtf8 = 0x4000, + + CborValidateStrictMode = 0xfff00, + + CborValidateMapKeysAreString = 0x100000, + CborValidateNoUndefined = 0x200000, + CborValidateNoTags = 0x400000, + CborValidateFiniteFloatingPoint = 0x800000, + /* unused = 0x1000000, */ + /* unused = 0x2000000, */ + + CborValidateNoUnknownSimpleTypesSA = 0x4000000, + CborValidateNoUnknownSimpleTypes = 0x8000000 | CborValidateNoUnknownSimpleTypesSA, + CborValidateNoUnknownTagsSA = 0x10000000, + CborValidateNoUnknownTagsSR = 0x20000000 | CborValidateNoUnknownTagsSA, + CborValidateNoUnknownTags = 0x40000000 | CborValidateNoUnknownTagsSR, + + CborValidateCompleteData = (int)0x80000000, + + CborValidateStrictest = (int)~0U, + CborValidateBasic = 0 +}; + +CBOR_API CborError cbor_value_validate(const CborValue *it, uint32_t flags); +#endif /* CBOR_NO_VALIDATION_API */ + +/* Human-readable (dump) API */ +#ifndef CBOR_NO_PRETTY_API + +enum CborPrettyFlags { + CborPrettyNumericEncodingIndicators = 0x01, + CborPrettyTextualEncodingIndicators = 0, + + CborPrettyIndicateIndeterminateLength = 0x02, + CborPrettyIndicateIndetermineLength = CborPrettyIndicateIndeterminateLength, /* deprecated */ + CborPrettyIndicateOverlongNumbers = 0x04, + + CborPrettyShowStringFragments = 0x100, + CborPrettyMergeStringFragments = 0, + + CborPrettyDefaultFlags = CborPrettyIndicateIndeterminateLength +}; + +typedef CborError (*CborStreamFunction)(void *token, const char *fmt, ...) +#ifdef __GNUC__ + __attribute__((__format__(printf, 2, 3))) +#endif +; + +CBOR_API CborError cbor_value_to_pretty_stream(CborStreamFunction streamFunction, void *token, CborValue *value, int flags); + +/* The following API requires a hosted C implementation (uses FILE*) */ +#if !defined(__STDC_HOSTED__) || __STDC_HOSTED__-0 == 1 +CBOR_API CborError cbor_value_to_pretty_advance_flags(FILE *out, CborValue *value, int flags); +CBOR_API CborError cbor_value_to_pretty_advance(FILE *out, CborValue *value); +CBOR_INLINE_API CborError cbor_value_to_pretty(FILE *out, const CborValue *value) +{ + CborValue copy = *value; + return cbor_value_to_pretty_advance_flags(out, ©, CborPrettyDefaultFlags); +} +#endif /* __STDC_HOSTED__ check */ + +#endif /* CBOR_NO_PRETTY_API */ + +#endif /* CBOR_NO_PARSER_API */ + +#ifdef __cplusplus +} +#endif + +#endif /* CBOR_H */ + diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder.c b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder.c new file mode 100644 index 000000000..a51f44515 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder.c @@ -0,0 +1,689 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#ifndef _BSD_SOURCE +#define _BSD_SOURCE 1 +#endif +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE 1 +#endif +#ifndef __STDC_LIMIT_MACROS +# define __STDC_LIMIT_MACROS 1 +#endif + +#include "cbor.h" +#include "cborinternal_p.h" +#include "compilersupport_p.h" + +#include +#include + +/** + * \defgroup CborEncoding Encoding to CBOR + * \brief Group of functions used to encode data to CBOR. + * + * CborEncoder is used to encode data into a CBOR stream. The outermost + * CborEncoder is initialized by calling cbor_encoder_init(), with the buffer + * where the CBOR stream will be stored. The outermost CborEncoder is usually + * used to encode exactly one item, most often an array or map. It is possible + * to encode more than one item, but care must then be taken on the decoder + * side to ensure the state is reset after each item was decoded. + * + * Nested CborEncoder objects are created using cbor_encoder_create_array() and + * cbor_encoder_create_map(), later closed with cbor_encoder_close_container() + * or cbor_encoder_close_container_checked(). The pairs of creation and closing + * must be exactly matched and their parameters are always the same. + * + * CborEncoder writes directly to the user-supplied buffer, without extra + * buffering. CborEncoder does not allocate memory and CborEncoder objects are + * usually created on the stack of the encoding functions. + * + * The example below initializes a CborEncoder object with a buffer and encodes + * a single integer. + * + * \code + * uint8_t buf[16]; + * CborEncoder encoder; + * cbor_encoder_init(&encoder, buf, sizeof(buf), 0); + * cbor_encode_int(&encoder, some_value); + * \endcode + * + * As explained before, usually the outermost CborEncoder object is used to add + * one array or map, which in turn contains multiple elements. The example + * below creates a CBOR map with one element: a key "foo" and a boolean value. + * + * \code + * uint8_t buf[16]; + * CborEncoder encoder, mapEncoder; + * cbor_encoder_init(&encoder, buf, sizeof(buf), 0); + * cbor_encoder_create_map(&encoder, &mapEncoder, 1); + * cbor_encode_text_stringz(&mapEncoder, "foo"); + * cbor_encode_boolean(&mapEncoder, some_value); + * cbor_encoder_close_container(&encoder, &mapEncoder); + * \endcode + * + *

Error checking and buffer size

+ * + * All functions operating on CborEncoder return a condition of type CborError. + * If the encoding was successful, they return CborNoError. Some functions do + * extra checking on the input provided and may return some other error + * conditions (for example, cbor_encode_simple_value() checks that the type is + * of the correct type). + * + * In addition, all functions check whether the buffer has enough bytes to + * encode the item being appended. If that is not possible, they return + * CborErrorOutOfMemory. + * + * It is possible to continue with the encoding of data past the first function + * that returns CborErrorOutOfMemory. CborEncoder functions will not overrun + * the buffer, but will instead count how many more bytes are needed to + * complete the encoding. At the end, you can obtain that count by calling + * cbor_encoder_get_extra_bytes_needed(). + * + * \section1 Finalizing the encoding + * + * Once all items have been appended and the containers have all been properly + * closed, the user-supplied buffer will contain the CBOR stream and may be + * immediately used. To obtain the size of the buffer, call + * cbor_encoder_get_buffer_size() with the original buffer pointer. + * + * The example below illustrates how one can encode an item with error checking + * and then pass on the buffer for network sending. + * + * \code + * uint8_t buf[16]; + * CborError err; + * CborEncoder encoder, mapEncoder; + * cbor_encoder_init(&encoder, buf, sizeof(buf), 0); + * err = cbor_encoder_create_map(&encoder, &mapEncoder, 1); + * if (err) + * return err; + * err = cbor_encode_text_stringz(&mapEncoder, "foo"); + * if (err) + * return err; + * err = cbor_encode_boolean(&mapEncoder, some_value); + * if (err) + * return err; + * err = cbor_encoder_close_container_checked(&encoder, &mapEncoder); + * if (err) + * return err; + * + * size_t len = cbor_encoder_get_buffer_size(&encoder, buf); + * send_payload(buf, len); + * return CborNoError; + * \endcode + * + * Finally, the example below expands on the one above and also + * deals with dynamically growing the buffer if the initial allocation wasn't + * big enough. Note the two places where the error checking was replaced with + * an cbor_assertion, showing where the author assumes no error can occur. + * + * \code + * uint8_t *encode_string_array(const char **strings, int n, size_t *bufsize) + * { + * CborError err; + * CborEncoder encoder, arrayEncoder; + * size_t size = 256; + * uint8_t *buf = NULL; + * + * while (1) { + * int i; + * size_t more_bytes; + * uint8_t *nbuf = realloc(buf, size); + * if (nbuf == NULL) + * goto error; + * buf = nbuf; + * + * cbor_encoder_init(&encoder, buf, size, 0); + * err = cbor_encoder_create_array(&encoder, &arrayEncoder, n); + * cbor_assert(!err); // can't fail, the buffer is always big enough + * + * for (i = 0; i < n; ++i) { + * err = cbor_encode_text_stringz(&arrayEncoder, strings[i]); + * if (err && err != CborErrorOutOfMemory) + * goto error; + * } + * + * err = cbor_encoder_close_container_checked(&encoder, &arrayEncoder); + * cbor_assert(!err); // shouldn't fail! + * + * more_bytes = cbor_encoder_get_extra_bytes_needed(encoder); + * if (more_size) { + * // buffer wasn't big enough, try again + * size += more_bytes; + * continue; + * } + * + * *bufsize = cbor_encoder_get_buffer_size(encoder, buf); + * return buf; + * } + * error: + * free(buf); + * return NULL; + * } + * \endcode + */ + +/** + * \addtogroup CborEncoding + * @{ + */ + +/** + * \struct CborEncoder + * Structure used to encode to CBOR. + */ + +/** + * Initializes a CborEncoder structure \a encoder by pointing it to buffer \a + * buffer of size \a size. The \a flags field is currently unused and must be + * zero. + */ +void cbor_encoder_init(CborEncoder *encoder, uint8_t *buffer, size_t size, int flags) +{ + encoder->data.ptr = buffer; + encoder->end = buffer + size; + encoder->remaining = 2; + encoder->flags = flags; +} + +void cbor_encoder_init_writer(CborEncoder *encoder, CborEncoderWriteFunction writer, void *token) +{ +#ifdef CBOR_ENCODER_WRITE_FUNCTION + (void) writer; +#else + encoder->data.writer = writer; +#endif + encoder->end = (uint8_t *)token; + encoder->remaining = 2; + encoder->flags = CborIteratorFlag_WriterFunction; +} + +static inline void put16(void *where, uint16_t v) +{ + uint16_t v_be = cbor_htons(v); + memcpy(where, &v_be, sizeof(v_be)); +} + +/* Note: Since this is currently only used in situations where OOM is the only + * valid error, we KNOW this to be true. Thus, this function now returns just 'true', + * but if in the future, any function starts returning a non-OOM error, this will need + * to be changed to the test. At the moment, this is done to prevent more branches + * being created in the tinycbor output */ +static inline bool isOomError(CborError err) +{ + if (CBOR_ENCODER_WRITER_CONTROL < 0) + return true; + + /* CborErrorOutOfMemory is the only negative error code, intentionally + * so we can write the test like this */ + return (int)err < 0; +} + +static inline void put32(void *where, uint32_t v) +{ + uint32_t v_be = cbor_htonl(v); + memcpy(where, &v_be, sizeof(v_be)); +} + +static inline void put64(void *where, uint64_t v) +{ + uint64_t v_be = cbor_htonll(v); + memcpy(where, &v_be, sizeof(v_be)); +} + +static inline bool would_overflow(CborEncoder *encoder, size_t len) +{ + ptrdiff_t remaining = (ptrdiff_t)encoder->end; + remaining -= remaining ? (ptrdiff_t)encoder->data.ptr : encoder->data.bytes_needed; + remaining -= (ptrdiff_t)len; + return unlikely(remaining < 0); +} + +static inline void advance_ptr(CborEncoder *encoder, size_t n) +{ + if (encoder->end) + encoder->data.ptr += n; + else + encoder->data.bytes_needed += n; +} + +static inline CborError append_to_buffer(CborEncoder *encoder, const void *data, size_t len, + CborEncoderAppendType appendType) +{ + if (CBOR_ENCODER_WRITER_CONTROL >= 0) { + if (encoder->flags & CborIteratorFlag_WriterFunction || CBOR_ENCODER_WRITER_CONTROL != 0) { +# ifdef CBOR_ENCODER_WRITE_FUNCTION + return CBOR_ENCODER_WRITE_FUNCTION(encoder->end, data, len, appendType); +# else + return encoder->data.writer(encoder->end, data, len, appendType); +# endif + } + } + +#if CBOR_ENCODER_WRITER_CONTROL <= 0 + if (would_overflow(encoder, len)) { + if (encoder->end != NULL) { + len -= encoder->end - encoder->data.ptr; + encoder->end = NULL; + encoder->data.bytes_needed = 0; + } + + advance_ptr(encoder, len); + return CborErrorOutOfMemory; + } + + memcpy(encoder->data.ptr, data, len); + encoder->data.ptr += len; +#endif + return CborNoError; +} + +static inline CborError append_byte_to_buffer(CborEncoder *encoder, uint8_t byte) +{ + return append_to_buffer(encoder, &byte, 1, CborEncoderAppendCborData); +} + +static inline CborError encode_number_no_update(CborEncoder *encoder, uint64_t ui, uint8_t shiftedMajorType) +{ + /* Little-endian would have been so much more convenient here: + * We could just write at the beginning of buf but append_to_buffer + * only the necessary bytes. + * Since it has to be big endian, do it the other way around: + * write from the end. */ + uint64_t buf[2]; + uint8_t *const bufend = (uint8_t *)buf + sizeof(buf); + uint8_t *bufstart = bufend - 1; + put64(buf + 1, ui); /* we probably have a bunch of zeros in the beginning */ + + if (ui < Value8Bit) { + *bufstart += shiftedMajorType; + } else { + uint8_t more = 0; + if (ui > 0xffU) + ++more; + if (ui > 0xffffU) + ++more; + if (ui > 0xffffffffU) + ++more; + bufstart -= (size_t)1 << more; + *bufstart = shiftedMajorType + Value8Bit + more; + } + + return append_to_buffer(encoder, bufstart, bufend - bufstart, CborEncoderAppendCborData); +} + +static inline void saturated_decrement(CborEncoder *encoder) +{ + if (encoder->remaining) + --encoder->remaining; +} + +static inline CborError encode_number(CborEncoder *encoder, uint64_t ui, uint8_t shiftedMajorType) +{ + saturated_decrement(encoder); + return encode_number_no_update(encoder, ui, shiftedMajorType); +} + +/** + * Appends the unsigned 64-bit integer \a value to the CBOR stream provided by + * \a encoder. + * + * \sa cbor_encode_negative_int, cbor_encode_int + */ +CborError cbor_encode_uint(CborEncoder *encoder, uint64_t value) +{ + return encode_number(encoder, value, UnsignedIntegerType << MajorTypeShift); +} + +/** + * Appends the negative 64-bit integer whose absolute value is \a + * absolute_value to the CBOR stream provided by \a encoder. + * + * If the value \a absolute_value is zero, this function encodes -2^64. + * + * \sa cbor_encode_uint, cbor_encode_int + */ +CborError cbor_encode_negative_int(CborEncoder *encoder, uint64_t absolute_value) +{ + return encode_number(encoder, absolute_value - 1, NegativeIntegerType << MajorTypeShift); +} + +/** + * Appends the signed 64-bit integer \a value to the CBOR stream provided by + * \a encoder. + * + * \sa cbor_encode_negative_int, cbor_encode_uint + */ +CborError cbor_encode_int(CborEncoder *encoder, int64_t value) +{ + /* adapted from code in RFC 7049 appendix C (pseudocode) */ + uint64_t ui = value >> 63; /* extend sign to whole length */ + uint8_t majorType = ui & 0x20; /* extract major type */ + ui ^= value; /* complement negatives */ + return encode_number(encoder, ui, majorType); +} + +/** + * Appends the CBOR Simple Type of value \a value to the CBOR stream provided by + * \a encoder. + * + * This function may return error CborErrorIllegalSimpleType if the \a value + * variable contains a number that is not a valid simple type. + */ +CborError cbor_encode_simple_value(CborEncoder *encoder, uint8_t value) +{ +#ifndef CBOR_ENCODER_NO_CHECK_USER + /* check if this is a valid simple type */ + if (value >= HalfPrecisionFloat && value <= Break) + return CborErrorIllegalSimpleType; +#endif + return encode_number(encoder, value, SimpleTypesType << MajorTypeShift); +} + +/** + * Appends the floating-point value of type \a fpType and pointed to by \a + * value to the CBOR stream provided by \a encoder. The value of \a fpType must + * be one of CborHalfFloatType, CborFloatType or CborDoubleType, otherwise the + * behavior of this function is undefined. + * + * This function is useful for code that needs to pass through floating point + * values but does not wish to have the actual floating-point code. + * + * \sa cbor_encode_half_float, cbor_encode_float_as_half_float, cbor_encode_float, cbor_encode_double + */ +CborError cbor_encode_floating_point(CborEncoder *encoder, CborType fpType, const void *value) +{ + unsigned size; + uint8_t buf[1 + sizeof(uint64_t)]; + cbor_assert(fpType == CborHalfFloatType || fpType == CborFloatType || fpType == CborDoubleType); + buf[0] = fpType; + + size = 2U << (fpType - CborHalfFloatType); + if (size == 8) + put64(buf + 1, *(const uint64_t*)value); + else if (size == 4) + put32(buf + 1, *(const uint32_t*)value); + else + put16(buf + 1, *(const uint16_t*)value); + saturated_decrement(encoder); + return append_to_buffer(encoder, buf, size + 1, CborEncoderAppendCborData); +} + +/** + * Appends the CBOR tag \a tag to the CBOR stream provided by \a encoder. + * + * \sa CborTag + */ +CborError cbor_encode_tag(CborEncoder *encoder, CborTag tag) +{ + /* tags don't count towards the number of elements in an array or map */ + return encode_number_no_update(encoder, tag, TagType << MajorTypeShift); +} + +static CborError encode_string(CborEncoder *encoder, size_t length, uint8_t shiftedMajorType, const void *string) +{ + CborError err = encode_number(encoder, length, shiftedMajorType); + if (err && !isOomError(err)) + return err; + return append_to_buffer(encoder, string, length, CborEncoderAppendStringData); +} + +/** + * \fn CborError cbor_encode_text_stringz(CborEncoder *encoder, const char *string) + * + * Appends the null-terminated text string \a string to the CBOR stream + * provided by \a encoder. CBOR requires that \a string be valid UTF-8, but + * TinyCBOR makes no verification of correctness. The terminating null is not + * included in the stream. + * + * \sa cbor_encode_text_string, cbor_encode_byte_string + */ + +/** + * Appends the byte string \a string of length \a length to the CBOR stream + * provided by \a encoder. CBOR byte strings are arbitrary raw data. + * + * \sa cbor_encode_text_stringz, cbor_encode_text_string + */ +CborError cbor_encode_byte_string(CborEncoder *encoder, const uint8_t *string, size_t length) +{ + return encode_string(encoder, length, ByteStringType << MajorTypeShift, string); +} + +/** + * Appends the text string \a string of length \a length to the CBOR stream + * provided by \a encoder. CBOR requires that \a string be valid UTF-8, but + * TinyCBOR makes no verification of correctness. + * + * \sa CborError cbor_encode_text_stringz, cbor_encode_byte_string + */ +CborError cbor_encode_text_string(CborEncoder *encoder, const char *string, size_t length) +{ + return encode_string(encoder, length, TextStringType << MajorTypeShift, string); +} + +#ifdef __GNUC__ +__attribute__((noinline)) +#endif +static CborError create_container(CborEncoder *encoder, CborEncoder *container, size_t length, uint8_t shiftedMajorType) +{ + CborError err; + container->data.ptr = encoder->data.ptr; + container->end = encoder->end; + saturated_decrement(encoder); + container->remaining = length + 1; /* overflow ok on CborIndefiniteLength */ + + cbor_static_assert((int)CborIteratorFlag_ContainerIsMap_ == (int)CborIteratorFlag_ContainerIsMap); + cbor_static_assert(((MapType << MajorTypeShift) & CborIteratorFlag_ContainerIsMap) == CborIteratorFlag_ContainerIsMap); + cbor_static_assert(((ArrayType << MajorTypeShift) & CborIteratorFlag_ContainerIsMap) == 0); + container->flags = shiftedMajorType & CborIteratorFlag_ContainerIsMap; + if (CBOR_ENCODER_WRITER_CONTROL == 0) + container->flags |= encoder->flags & CborIteratorFlag_WriterFunction; + + if (length == CborIndefiniteLength) { + container->flags |= CborIteratorFlag_UnknownLength; + err = append_byte_to_buffer(container, shiftedMajorType + IndefiniteLength); + } else { + if (shiftedMajorType & CborIteratorFlag_ContainerIsMap) + container->remaining += length; + err = encode_number_no_update(container, length, shiftedMajorType); + } + return err; +} + +/** + * Creates a CBOR array in the CBOR stream provided by \a parentEncoder and + * initializes \a arrayEncoder so that items can be added to the array using + * the CborEncoder functions. The array must be terminated by calling either + * cbor_encoder_close_container() or cbor_encoder_close_container_checked() + * with the same \a encoder and \a arrayEncoder parameters. + * + * The number of items inserted into the array must be exactly \a length items, + * otherwise the stream is invalid. If the number of items is not known when + * creating the array, the constant \ref CborIndefiniteLength may be passed as + * length instead, and an indefinite length array is created. + * + * \sa cbor_encoder_create_map + */ +CborError cbor_encoder_create_array(CborEncoder *parentEncoder, CborEncoder *arrayEncoder, size_t length) +{ + return create_container(parentEncoder, arrayEncoder, length, ArrayType << MajorTypeShift); +} + +/** + * Creates a CBOR map in the CBOR stream provided by \a parentEncoder and + * initializes \a mapEncoder so that items can be added to the map using + * the CborEncoder functions. The map must be terminated by calling either + * cbor_encoder_close_container() or cbor_encoder_close_container_checked() + * with the same \a encoder and \a mapEncoder parameters. + * + * The number of pair of items inserted into the map must be exactly \a length + * items, otherwise the stream is invalid. If the number is not known + * when creating the map, the constant \ref CborIndefiniteLength may be passed as + * length instead, and an indefinite length map is created. + * + * \b{Implementation limitation:} TinyCBOR cannot encode more than SIZE_MAX/2 + * key-value pairs in the stream. If the length \a length is larger than this + * value (and is not \ref CborIndefiniteLength), this function returns error + * CborErrorDataTooLarge. + * + * \sa cbor_encoder_create_array + */ +CborError cbor_encoder_create_map(CborEncoder *parentEncoder, CborEncoder *mapEncoder, size_t length) +{ + if (length != CborIndefiniteLength && length > SIZE_MAX / 2) + return CborErrorDataTooLarge; + return create_container(parentEncoder, mapEncoder, length, MapType << MajorTypeShift); +} + +/** + * Closes the CBOR container (array or map) provided by \a containerEncoder and + * updates the CBOR stream provided by \a encoder. Both parameters must be the + * same as were passed to cbor_encoder_create_array() or + * cbor_encoder_create_map(). + * + * Since version 0.5, this function verifies that the number of items (or pairs + * of items, in the case of a map) was correct. It is no longer necessary to call + * cbor_encoder_close_container_checked() instead. + * + * \sa cbor_encoder_create_array(), cbor_encoder_create_map() + */ +CborError cbor_encoder_close_container(CborEncoder *parentEncoder, const CborEncoder *containerEncoder) +{ + // synchronise buffer state with that of the container + parentEncoder->end = containerEncoder->end; + parentEncoder->data = containerEncoder->data; + + if (containerEncoder->flags & CborIteratorFlag_UnknownLength) + return append_byte_to_buffer(parentEncoder, BreakByte); + + if (containerEncoder->remaining != 1) + return containerEncoder->remaining == 0 ? CborErrorTooManyItems : CborErrorTooFewItems; + + if (!parentEncoder->end) + return CborErrorOutOfMemory; /* keep the state */ + + return CborNoError; +} + +/** + * \fn CborError cbor_encode_boolean(CborEncoder *encoder, bool value) + * + * Appends the boolean value \a value to the CBOR stream provided by \a encoder. + */ + +/** + * \fn CborError cbor_encode_null(CborEncoder *encoder) + * + * Appends the CBOR type representing a null value to the CBOR stream provided + * by \a encoder. + * + * \sa cbor_encode_undefined() + */ + +/** + * \fn CborError cbor_encode_undefined(CborEncoder *encoder) + * + * Appends the CBOR type representing an undefined value to the CBOR stream + * provided by \a encoder. + * + * \sa cbor_encode_null() + */ + +/** + * \fn CborError cbor_encode_half_float(CborEncoder *encoder, const void *value) + * + * Appends the IEEE 754 half-precision (16-bit) floating point value pointed to + * by \a value to the CBOR stream provided by \a encoder. + * + * \sa cbor_encode_floating_point(), cbor_encode_float(), cbor_encode_double() + */ + +/** + * \fn CborError cbor_encode_float_as_half_float(CborEncoder *encoder, float value) + * + * Convert the IEEE 754 single-precision (32-bit) floating point value \a value + * to the IEEE 754 half-precision (16-bit) floating point value and append it + * to the CBOR stream provided by \a encoder. + * The \a value should be in the range of the IEEE 754 half-precision floating point type, + * INFINITY, -INFINITY, or NAN, otherwise the behavior of this function is undefined. + * + * \sa cbor_encode_floating_point(), cbor_encode_float(), cbor_encode_double() + */ + +/** + * \fn CborError cbor_encode_float(CborEncoder *encoder, float value) + * + * Appends the IEEE 754 single-precision (32-bit) floating point value \a value + * to the CBOR stream provided by \a encoder. + * + * \sa cbor_encode_floating_point(), cbor_encode_half_float(), cbor_encode_float_as_half_float(), cbor_encode_double() + */ + +/** + * \fn CborError cbor_encode_double(CborEncoder *encoder, double value) + * + * Appends the IEEE 754 double-precision (64-bit) floating point value \a value + * to the CBOR stream provided by \a encoder. + * + * \sa cbor_encode_floating_point(), cbor_encode_half_float(), cbor_encode_float_as_half_float(), cbor_encode_float() + */ + +/** + * \fn size_t cbor_encoder_get_buffer_size(const CborEncoder *encoder, const uint8_t *buffer) + * + * Returns the total size of the buffer starting at \a buffer after the + * encoding finished without errors. The \a encoder and \a buffer arguments + * must be the same as supplied to cbor_encoder_init(). + * + * If the encoding process had errors, the return value of this function is + * meaningless. If the only errors were CborErrorOutOfMemory, instead use + * cbor_encoder_get_extra_bytes_needed() to find out by how much to grow the + * buffer before encoding again. + * + * See \ref CborEncoding for an example of using this function. + * + * \sa cbor_encoder_init(), cbor_encoder_get_extra_bytes_needed(), CborEncoding + */ + +/** + * \fn size_t cbor_encoder_get_extra_bytes_needed(const CborEncoder *encoder) + * + * Returns how many more bytes the original buffer supplied to + * cbor_encoder_init() needs to be extended by so that no CborErrorOutOfMemory + * condition will happen for the encoding. If the buffer was big enough, this + * function returns 0. The \a encoder must be the original argument as passed + * to cbor_encoder_init(). + * + * This function is usually called after an encoding sequence ended with one or + * more CborErrorOutOfMemory errors, but no other error. If any other error + * happened, the return value of this function is meaningless. + * + * See \ref CborEncoding for an example of using this function. + * + * \sa cbor_encoder_init(), cbor_encoder_get_buffer_size(), CborEncoding + */ + +/** @} */ diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder_close_container_checked.c b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder_close_container_checked.c new file mode 100644 index 000000000..5661e4d53 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder_close_container_checked.c @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** Copyright (C) 2015 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#define _BSD_SOURCE 1 +#define _DEFAULT_SOURCE 1 +#ifndef __STDC_LIMIT_MACROS +# define __STDC_LIMIT_MACROS 1 +#endif + +#include "cbor.h" + +/** + * \addtogroup CborEncoding + * @{ + */ + +/** + * @deprecated + * + * Closes the CBOR container (array or map) provided by \a containerEncoder and + * updates the CBOR stream provided by \a encoder. Both parameters must be the + * same as were passed to cbor_encoder_create_array() or + * cbor_encoder_create_map(). + * + * Prior to version 0.5, cbor_encoder_close_container() did not check the + * number of items added. Since that version, it does and now + * cbor_encoder_close_container_checked() is no longer needed. + * + * \sa cbor_encoder_create_array(), cbor_encoder_create_map() + */ +CborError cbor_encoder_close_container_checked(CborEncoder *encoder, const CborEncoder *containerEncoder) +{ + return cbor_encoder_close_container(encoder, containerEncoder); +} + +/** @} */ diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborerrorstrings.c b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborerrorstrings.c new file mode 100644 index 000000000..44f766a3c --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborerrorstrings.c @@ -0,0 +1,188 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#include "cbor.h" + +#ifndef _ +# define _(msg) msg +#endif + +/** + * \enum CborError + * \ingroup CborGlobals + * The CborError enum contains the possible error values used by the CBOR encoder and decoder. + * + * TinyCBOR functions report success by returning CborNoError, or one error + * condition by returning one of the values below. One exception is the + * out-of-memory condition (CborErrorOutOfMemory), which the functions for \ref + * CborEncoding may report in bit-wise OR with other conditions. + * + * This technique allows code to determine whether the only error condition was + * a lack of buffer space, which may not be a fatal condition if the buffer can + * be resized. Additionally, the functions for \ref CborEncoding may continue + * to be used even after CborErrorOutOfMemory is returned, and instead they + * will simply calculate the extra space needed. + * + * \value CborNoError No error occurred + * \omitvalue CborUnknownError + * \value CborErrorUnknownLength Request for the length of an array, map or string whose length is not provided in the CBOR stream + * \value CborErrorAdvancePastEOF Not enough data in the stream to decode item (decoding would advance past end of stream) + * \value CborErrorIO An I/O error occurred, probably due to an out-of-memory situation + * \value CborErrorGarbageAtEnd Bytes exist past the end of the CBOR stream + * \value CborErrorUnexpectedEOF End of stream reached unexpectedly + * \value CborErrorUnexpectedBreak A CBOR break byte was found where not expected + * \value CborErrorUnknownType An unknown type (future extension to CBOR) was found in the stream + * \value CborErrorIllegalType An invalid type was found while parsing a chunked CBOR string + * \value CborErrorIllegalNumber An illegal initial byte (encoding unspecified additional information) was found + * \value CborErrorIllegalSimpleType An illegal encoding of a CBOR Simple Type of value less than 32 was found + * \omitvalue CborErrorUnknownSimpleType + * \omitvalue CborErrorUnknownTag + * \omitvalue CborErrorInappropriateTagForType + * \omitvalue CborErrorDuplicateObjectKeys + * \value CborErrorInvalidUtf8TextString Illegal UTF-8 encoding found while parsing CBOR Text String + * \value CborErrorTooManyItems Too many items were added to CBOR map or array of pre-determined length + * \value CborErrorTooFewItems Too few items were added to CBOR map or array of pre-determined length + * \value CborErrorDataTooLarge Data item size exceeds TinyCBOR's implementation limits + * \value CborErrorNestingTooDeep Data item nesting exceeds TinyCBOR's implementation limits + * \omitvalue CborErrorUnsupportedType + * \value CborErrorJsonObjectKeyIsAggregate Conversion to JSON failed because the key in a map is a CBOR map or array + * \value CborErrorJsonObjectKeyNotString Conversion to JSON failed because the key in a map is not a text string + * \value CborErrorOutOfMemory During CBOR encoding, the buffer provided is insufficient for encoding the data item; + * in other situations, TinyCBOR failed to allocate memory + * \value CborErrorInternalError An internal error occurred in TinyCBOR + */ + +/** + * \ingroup CborGlobals + * Returns the error string corresponding to the CBOR error condition \a error. + */ +const char *cbor_error_string(CborError error) +{ + switch (error) { + case CborNoError: + return ""; + + case CborUnknownError: + return _("unknown error"); + + case CborErrorOutOfMemory: + return _("out of memory/need more memory"); + + case CborErrorUnknownLength: + return _("unknown length (attempted to get the length of a map/array/string of indeterminate length"); + + case CborErrorAdvancePastEOF: + return _("attempted to advance past EOF"); + + case CborErrorIO: + return _("I/O error"); + + case CborErrorGarbageAtEnd: + return _("garbage after the end of the content"); + + case CborErrorUnexpectedEOF: + return _("unexpected end of data"); + + case CborErrorUnexpectedBreak: + return _("unexpected 'break' byte"); + + case CborErrorUnknownType: + return _("illegal byte (encodes future extension type)"); + + case CborErrorIllegalType: + return _("mismatched string type in chunked string"); + + case CborErrorIllegalNumber: + return _("illegal initial byte (encodes unspecified additional information)"); + + case CborErrorIllegalSimpleType: + return _("illegal encoding of simple type smaller than 32"); + + case CborErrorNoMoreStringChunks: + return _("no more byte or text strings available"); + + case CborErrorUnknownSimpleType: + return _("unknown simple type"); + + case CborErrorUnknownTag: + return _("unknown tag"); + + case CborErrorInappropriateTagForType: + return _("inappropriate tag for type"); + + case CborErrorDuplicateObjectKeys: + return _("duplicate keys in object"); + + case CborErrorInvalidUtf8TextString: + return _("invalid UTF-8 content in string"); + + case CborErrorExcludedType: + return _("excluded type found"); + + case CborErrorExcludedValue: + return _("excluded value found"); + + case CborErrorImproperValue: + case CborErrorOverlongEncoding: + return _("value encoded in non-canonical form"); + + case CborErrorMapKeyNotString: + case CborErrorJsonObjectKeyNotString: + return _("key in map is not a string"); + + case CborErrorMapNotSorted: + return _("map is not sorted"); + + case CborErrorMapKeysNotUnique: + return _("map keys are not unique"); + + case CborErrorTooManyItems: + return _("too many items added to encoder"); + + case CborErrorTooFewItems: + return _("too few items added to encoder"); + + case CborErrorDataTooLarge: + return _("internal error: data too large"); + + case CborErrorNestingTooDeep: + return _("internal error: too many nested containers found in recursive function"); + + case CborErrorUnsupportedType: + return _("unsupported type"); + + case CborErrorUnimplementedValidation: + return _("validation not implemented for the current parser state"); + + case CborErrorJsonObjectKeyIsAggregate: + return _("conversion to JSON failed: key in object is an array or map"); + + case CborErrorJsonNotImplemented: + return _("conversion to JSON failed: open_memstream unavailable"); + + case CborErrorInternalError: + return _("internal error"); + } + return cbor_error_string(CborUnknownError); +} diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborinternal_p.h b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborinternal_p.h new file mode 100644 index 000000000..16269e630 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborinternal_p.h @@ -0,0 +1,316 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#ifndef CBORINTERNAL_P_H +#define CBORINTERNAL_P_H + +#include "compilersupport_p.h" + +#ifndef CBOR_NO_FLOATING_POINT +# include +# include +#else +# ifndef CBOR_NO_HALF_FLOAT_TYPE +# define CBOR_NO_HALF_FLOAT_TYPE 1 +# endif +#endif + +#ifndef CBOR_NO_HALF_FLOAT_TYPE +# if defined(__F16C__) || defined(__AVX2__) +# include +static inline unsigned short encode_half(float val) +{ + __m128i m = _mm_cvtps_ph(_mm_set_ss(val), _MM_FROUND_CUR_DIRECTION); + return _mm_extract_epi16(m, 0); +} +static inline float decode_half(unsigned short half) +{ + __m128i m = _mm_cvtsi32_si128(half); + return _mm_cvtss_f32(_mm_cvtph_ps(m)); +} +# else +/* software implementation of float-to-fp16 conversions */ +static inline unsigned short encode_half(double val) +{ + uint64_t v; + int sign, exp, mant; + memcpy(&v, &val, sizeof(v)); + sign = v >> 63 << 15; + exp = (v >> 52) & 0x7ff; + mant = v << 12 >> 12 >> (53-11); /* keep only the 11 most significant bits of the mantissa */ + exp -= 1023; + if (exp == 1024) { + /* infinity or NaN */ + exp = 16; + mant >>= 1; + } else if (exp >= 16) { + /* overflow, as largest number */ + exp = 15; + mant = 1023; + } else if (exp >= -14) { + /* regular normal */ + } else if (exp >= -24) { + /* subnormal */ + mant |= 1024; + mant >>= -(exp + 14); + exp = -15; + } else { + /* underflow, make zero */ + return 0; + } + + /* safe cast here as bit operations above guarantee not to overflow */ + return (unsigned short)(sign | ((exp + 15) << 10) | mant); +} + +/* this function was copied & adapted from RFC 7049 Appendix D */ +static inline double decode_half(unsigned short half) +{ + int exp = (half >> 10) & 0x1f; + int mant = half & 0x3ff; + double val; + if (exp == 0) val = ldexp(mant, -24); + else if (exp != 31) val = ldexp(mant + 1024, exp - 25); + else val = mant == 0 ? INFINITY : NAN; + return half & 0x8000 ? -val : val; +} +# endif +#endif /* CBOR_NO_HALF_FLOAT_TYPE */ + +#ifndef CBOR_INTERNAL_API +# define CBOR_INTERNAL_API +#endif + +#ifndef CBOR_PARSER_MAX_RECURSIONS +# define CBOR_PARSER_MAX_RECURSIONS 1024 +#endif + +#ifndef CBOR_ENCODER_WRITER_CONTROL +# define CBOR_ENCODER_WRITER_CONTROL 0 +#endif +#ifndef CBOR_PARSER_READER_CONTROL +# define CBOR_PARSER_READER_CONTROL 0 +#endif + +/* + * CBOR Major types + * Encoded in the high 3 bits of the descriptor byte + * See http://tools.ietf.org/html/rfc7049#section-2.1 + */ +typedef enum CborMajorTypes { + UnsignedIntegerType = 0U, + NegativeIntegerType = 1U, + ByteStringType = 2U, + TextStringType = 3U, + ArrayType = 4U, + MapType = 5U, /* a.k.a. object */ + TagType = 6U, + SimpleTypesType = 7U +} CborMajorTypes; + +/* + * CBOR simple and floating point types + * Encoded in the low 8 bits of the descriptor byte when the + * Major Type is 7. + */ +typedef enum CborSimpleTypes { + FalseValue = 20, + TrueValue = 21, + NullValue = 22, + UndefinedValue = 23, + SimpleTypeInNextByte = 24, /* not really a simple type */ + HalfPrecisionFloat = 25, /* ditto */ + SinglePrecisionFloat = 26, /* ditto */ + DoublePrecisionFloat = 27, /* ditto */ + Break = 31 +} CborSimpleTypes; + +enum { + SmallValueBitLength = 5U, + SmallValueMask = (1U << SmallValueBitLength) - 1, /* 31 */ + Value8Bit = 24U, + Value16Bit = 25U, + Value32Bit = 26U, + Value64Bit = 27U, + IndefiniteLength = 31U, + + MajorTypeShift = SmallValueBitLength, + MajorTypeMask = (int) (~0U << MajorTypeShift), + + BreakByte = (unsigned)Break | (SimpleTypesType << MajorTypeShift) +}; + +static inline void copy_current_position(CborValue *dst, const CborValue *src) +{ + /* This "if" is here for pedantry only: the two branches should perform + * the same memory operation. */ + if (src->parser->flags & CborParserFlag_ExternalSource) + dst->source.token = src->source.token; + else + dst->source.ptr = src->source.ptr; +} + +static inline bool can_read_bytes(const CborValue *it, size_t n) +{ + if (CBOR_PARSER_READER_CONTROL >= 0) { + if (it->parser->flags & CborParserFlag_ExternalSource || CBOR_PARSER_READER_CONTROL != 0) { +#ifdef CBOR_PARSER_CAN_READ_BYTES_FUNCTION + return CBOR_PARSER_CAN_READ_BYTES_FUNCTION(it->source.token, n); +#else + return it->parser->source.ops->can_read_bytes(it->source.token, n); +#endif + } + } + + /* Convert the pointer subtraction to size_t since end >= ptr + * (this prevents issues with (ptrdiff_t)n becoming negative). + */ + return (size_t)(it->parser->source.end - it->source.ptr) >= n; +} + +static inline void advance_bytes(CborValue *it, size_t n) +{ + if (CBOR_PARSER_READER_CONTROL >= 0) { + if (it->parser->flags & CborParserFlag_ExternalSource || CBOR_PARSER_READER_CONTROL != 0) { +#ifdef CBOR_PARSER_ADVANCE_BYTES_FUNCTION + CBOR_PARSER_ADVANCE_BYTES_FUNCTION(it->source.token, n); +#else + it->parser->source.ops->advance_bytes(it->source.token, n); +#endif + return; + } + } + + it->source.ptr += n; +} + +static inline CborError transfer_string(CborValue *it, const void **ptr, size_t offset, size_t len) +{ + if (CBOR_PARSER_READER_CONTROL >= 0) { + if (it->parser->flags & CborParserFlag_ExternalSource || CBOR_PARSER_READER_CONTROL != 0) { +#ifdef CBOR_PARSER_TRANSFER_STRING_FUNCTION + return CBOR_PARSER_TRANSFER_STRING_FUNCTION(it->source.token, ptr, offset, len); +#else + return it->parser->source.ops->transfer_string(it->source.token, ptr, offset, len); +#endif + } + } + + it->source.ptr += offset; + if (can_read_bytes(it, len)) { + *CONST_CAST(const void **, ptr) = it->source.ptr; + it->source.ptr += len; + return CborNoError; + } + return CborErrorUnexpectedEOF; +} + +static inline void *read_bytes_unchecked(const CborValue *it, void *dst, size_t offset, size_t n) +{ + if (CBOR_PARSER_READER_CONTROL >= 0) { + if (it->parser->flags & CborParserFlag_ExternalSource || CBOR_PARSER_READER_CONTROL != 0) { +#ifdef CBOR_PARSER_READ_BYTES_FUNCTION + return CBOR_PARSER_READ_BYTES_FUNCTION(it->source.token, dst, offset, n); +#else + return it->parser->source.ops->read_bytes(it->source.token, dst, offset, n); +#endif + } + } + + return memcpy(dst, it->source.ptr + offset, n); +} + +#ifdef __GNUC__ +__attribute__((warn_unused_result)) +#endif +static inline void *read_bytes(const CborValue *it, void *dst, size_t offset, size_t n) +{ + if (can_read_bytes(it, offset + n)) + return read_bytes_unchecked(it, dst, offset, n); + return NULL; +} + +static inline uint16_t read_uint8(const CborValue *it, size_t offset) +{ + uint8_t result; + read_bytes_unchecked(it, &result, offset, sizeof(result)); + return result; +} + +static inline uint16_t read_uint16(const CborValue *it, size_t offset) +{ + uint16_t result; + read_bytes_unchecked(it, &result, offset, sizeof(result)); + return cbor_ntohs(result); +} + +static inline uint32_t read_uint32(const CborValue *it, size_t offset) +{ + uint32_t result; + read_bytes_unchecked(it, &result, offset, sizeof(result)); + return cbor_ntohl(result); +} + +static inline uint64_t read_uint64(const CborValue *it, size_t offset) +{ + uint64_t result; + read_bytes_unchecked(it, &result, offset, sizeof(result)); + return cbor_ntohll(result); +} + +static inline CborError extract_number_checked(const CborValue *it, uint64_t *value, size_t *bytesUsed) +{ + uint8_t descriptor; + size_t bytesNeeded = 0; + + /* We've already verified that there's at least one byte to be read */ + read_bytes_unchecked(it, &descriptor, 0, 1); + descriptor &= SmallValueMask; + if (descriptor < Value8Bit) { + *value = descriptor; + } else if (unlikely(descriptor > Value64Bit)) { + return CborErrorIllegalNumber; + } else { + bytesNeeded = (size_t)(1 << (descriptor - Value8Bit)); + if (!can_read_bytes(it, 1 + bytesNeeded)) + return CborErrorUnexpectedEOF; + if (descriptor <= Value16Bit) { + if (descriptor == Value16Bit) + *value = read_uint16(it, 1); + else + *value = read_uint8(it, 1); + } else { + if (descriptor == Value32Bit) + *value = read_uint32(it, 1); + else + *value = read_uint64(it, 1); + } + } + + if (bytesUsed) + *bytesUsed = bytesNeeded; + return CborNoError; +} + +#endif /* CBORINTERNAL_P_H */ diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser.c b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser.c new file mode 100644 index 000000000..74d91a30e --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser.c @@ -0,0 +1,1529 @@ +/**************************************************************************** +** +** Copyright (C) 2021 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#ifndef _BSD_SOURCE +#define _BSD_SOURCE 1 +#endif +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE 1 +#endif +#ifndef __STDC_LIMIT_MACROS +# define __STDC_LIMIT_MACROS 1 +#endif + +#include "cbor.h" +#include "cborinternal_p.h" +#include "compilersupport_p.h" + +#include + +/** + * \defgroup CborParsing Parsing CBOR streams + * \brief Group of functions used to parse CBOR streams. + * + * TinyCBOR provides functions for pull-based stream parsing of a CBOR-encoded + * payload. The main data type for the parsing is a CborValue, which behaves + * like an iterator and can be used to extract the encoded data. It is first + * initialized with a call to cbor_parser_init() and is usually used to extract + * exactly one item, most often an array or map. + * + * Nested CborValue objects can be parsed using cbor_value_enter_container(). + * Each call to cbor_value_enter_container() must be matched by a call to + * cbor_value_leave_container(), with the exact same parameters. + * + * The example below initializes a CborParser object, begins the parsing with a + * CborValue and decodes a single integer: + * + * \code + * int extract_int(const uint8_t *buffer, size_t len) + * { + * CborParser parser; + * CborValue value; + * int result; + * cbor_parser_init(buffer, len, 0, &parser, &value); + * cbor_value_get_int(&value, &result); + * return result; + * } + * \endcode + * + * The code above does no error checking, which means it assumes the data comes + * from a source trusted to send one properly-encoded integer. The following + * example does the exact same operation, but includes error checking and + * returns 0 on parsing failure: + * + * \code + * int extract_int(const uint8_t *buffer, size_t len) + * { + * CborParser parser; + * CborValue value; + * int result; + * if (cbor_parser_init(buffer, len, 0, &parser, &value) != CborNoError) + * return 0; + * if (!cbor_value_is_integer(&value) || + * cbor_value_get_int(&value, &result) != CborNoError) + * return 0; + * return result; + * } + * \endcode + * + * Note, in the example above, that one can't distinguish a parsing failure + * from an encoded value of zero. Reporting a parsing error is left as an + * exercise to the reader. + * + * The code above does not execute a range-check either: it is possible that + * the value decoded from the CBOR stream encodes a number larger than what can + * be represented in a variable of type \c{int}. If detecting that case is + * important, the code should call cbor_value_get_int_checked() instead. + * + *

Memory and parsing constraints

+ * + * TinyCBOR is designed to run with little memory and with minimal overhead. + * Except where otherwise noted, the parser functions always run on constant + * time (O(1)), do not recurse and never allocate memory (thus, stack usage is + * bounded and is O(1)). + * + *

Error handling and preconditions

+ * + * All functions operating on a CborValue return a CborError condition, with + * CborNoError standing for the normal situation in which no parsing error + * occurred. All functions may return parsing errors in case the stream cannot + * be decoded properly, be it due to corrupted data or due to reaching the end + * of the input buffer. + * + * Error conditions must not be ignored. All decoder functions have undefined + * behavior if called after an error has been reported, and may crash. + * + * Some functions are also documented to have preconditions, like + * cbor_value_get_int() requiring that the input be an integral value. + * Violation of preconditions also results in undefined behavior and the + * program may crash. + */ + +/** + * \addtogroup CborParsing + * @{ + */ + +/** + * \struct CborValue + * + * This type contains one value parsed from the CBOR stream. Each CborValue + * behaves as an iterator in a StAX-style parser. + * + * \if privatedocs + * Implementation details: the CborValue contains these fields: + * \list + * \li ptr: pointer to the actual data + * \li flags: flags from the decoder + * \li extra: partially decoded integer value (0, 1 or 2 bytes) + * \li remaining: remaining items in this collection after this item or UINT32_MAX if length is unknown + * \endlist + * \endif + */ + +static uint64_t extract_number_and_advance(CborValue *it) +{ + /* This function is only called after we've verified that the number + * here is valid, so we can just use _cbor_value_extract_int64_helper. */ + uint8_t descriptor; + uint64_t v = _cbor_value_extract_int64_helper(it); + + read_bytes_unchecked(it, &descriptor, 0, 1); + descriptor &= SmallValueMask; + + size_t bytesNeeded = descriptor < Value8Bit ? 0 : (1 << (descriptor - Value8Bit)); + advance_bytes(it, bytesNeeded + 1); + + return v; +} + +static bool is_fixed_type(uint8_t type) +{ + return type != CborTextStringType && type != CborByteStringType && type != CborArrayType && + type != CborMapType; +} + +static CborError preparse_value(CborValue *it) +{ + enum { + /* flags to keep */ + FlagsToKeep = CborIteratorFlag_ContainerIsMap | CborIteratorFlag_NextIsMapKey + }; + uint8_t descriptor; + + /* are we at the end? */ + it->type = CborInvalidType; + it->flags &= FlagsToKeep; + if (!read_bytes(it, &descriptor, 0, 1)) + return CborErrorUnexpectedEOF; + + uint8_t type = descriptor & MajorTypeMask; + it->type = type; + it->extra = (descriptor &= SmallValueMask); + + if (descriptor > Value64Bit) { + if (unlikely(descriptor != IndefiniteLength)) + return type == CborSimpleType ? CborErrorUnknownType : CborErrorIllegalNumber; + if (likely(!is_fixed_type(type))) { + /* special case */ + it->flags |= CborIteratorFlag_UnknownLength; + it->type = type; + return CborNoError; + } + return type == CborSimpleType ? CborErrorUnexpectedBreak : CborErrorIllegalNumber; + } + + size_t bytesNeeded = descriptor < Value8Bit ? 0 : (1 << (descriptor - Value8Bit)); + + if (bytesNeeded) { + if (!can_read_bytes(it, bytesNeeded + 1)) + return CborErrorUnexpectedEOF; + + it->extra = 0; + + /* read up to 16 bits into it->extra */ + if (bytesNeeded == 1) { + uint8_t extra; + read_bytes_unchecked(it, &extra, 1, bytesNeeded); + it->extra = extra; + } else if (bytesNeeded == 2) { + read_bytes_unchecked(it, &it->extra, 1, bytesNeeded); + it->extra = cbor_ntohs(it->extra); + } else { + cbor_static_assert(CborIteratorFlag_IntegerValueTooLarge == (Value32Bit & 3)); + cbor_static_assert((CborIteratorFlag_IntegerValueIs64Bit | + CborIteratorFlag_IntegerValueTooLarge) == (Value64Bit & 3)); + it->flags |= (descriptor & 3); + } + } + + uint8_t majortype = type >> MajorTypeShift; + if (majortype == NegativeIntegerType) { + it->flags |= CborIteratorFlag_NegativeInteger; + it->type = CborIntegerType; + } else if (majortype == SimpleTypesType) { + switch (descriptor) { + case FalseValue: + it->extra = false; + it->type = CborBooleanType; + break; + + case SinglePrecisionFloat: + case DoublePrecisionFloat: + it->flags |= CborIteratorFlag_IntegerValueTooLarge; + /* fall through */ + case TrueValue: + case NullValue: + case UndefinedValue: + case HalfPrecisionFloat: + read_bytes_unchecked(it, &it->type, 0, 1); + break; + + case SimpleTypeInNextByte: +#ifndef CBOR_PARSER_NO_STRICT_CHECKS + if (unlikely(it->extra < 32)) { + it->type = CborInvalidType; + return CborErrorIllegalSimpleType; + } +#endif + break; + + case 28: + case 29: + case 30: + case Break: + cbor_assert(false); /* these conditions can't be reached */ + return CborErrorUnexpectedBreak; + } + } + + return CborNoError; +} + +static CborError preparse_next_value_nodecrement(CborValue *it) +{ + uint8_t byte; + if (it->remaining == UINT32_MAX && read_bytes(it, &byte, 0, 1) && byte == (uint8_t)BreakByte) { + /* end of map or array */ + if ((it->flags & CborIteratorFlag_ContainerIsMap && it->flags & CborIteratorFlag_NextIsMapKey) + || it->type == CborTagType) { + /* but we weren't expecting it! */ + return CborErrorUnexpectedBreak; + } + it->type = CborInvalidType; + it->remaining = 0; + it->flags |= CborIteratorFlag_UnknownLength; /* leave_container must consume the Break */ + return CborNoError; + } + + return preparse_value(it); +} + +static CborError preparse_next_value(CborValue *it) +{ + /* tags don't count towards item totals or whether we've successfully + * read a map's key or value */ + bool itemCounts = it->type != CborTagType; + + if (it->remaining != UINT32_MAX) { + if (itemCounts && --it->remaining == 0) { + it->type = CborInvalidType; + it->flags &= ~CborIteratorFlag_UnknownLength; /* no Break to consume */ + return CborNoError; + } + } + if (itemCounts) { + /* toggle the flag indicating whether this was a map key */ + it->flags ^= CborIteratorFlag_NextIsMapKey; + } + return preparse_next_value_nodecrement(it); +} + +static CborError advance_internal(CborValue *it) +{ + uint64_t length = extract_number_and_advance(it); + + if (it->type == CborByteStringType || it->type == CborTextStringType) { + cbor_assert(length == (size_t)length); + cbor_assert((it->flags & CborIteratorFlag_UnknownLength) == 0); + advance_bytes(it, length); + } + + return preparse_next_value(it); +} + +/** \internal + * + * Decodes the CBOR integer value when it is larger than the 16 bits available + * in value->extra. This function requires that value->flags have the + * CborIteratorFlag_IntegerValueTooLarge flag set. + * + * This function is also used to extract single- and double-precision floating + * point values (SinglePrecisionFloat == Value32Bit and DoublePrecisionFloat == + * Value64Bit). + */ +uint64_t _cbor_value_decode_int64_internal(const CborValue *value) +{ + cbor_assert(value->flags & CborIteratorFlag_IntegerValueTooLarge || + value->type == CborFloatType || value->type == CborDoubleType); + if (value->flags & CborIteratorFlag_IntegerValueIs64Bit) + return read_uint64(value, 1); + + return read_uint32(value, 1); +} + +/** + * Initializes the CBOR parser for parsing \a size bytes beginning at \a + * buffer. Parsing will use flags set in \a flags. The iterator to the first + * element is returned in \a it. + * + * The \a parser structure needs to remain valid throughout the decoding + * process. It is not thread-safe to share one CborParser among multiple + * threads iterating at the same time, but the object can be copied so multiple + * threads can iterate. + */ +CborError cbor_parser_init(const uint8_t *buffer, size_t size, uint32_t flags, CborParser *parser, CborValue *it) +{ + memset(parser, 0, sizeof(*parser)); + parser->source.end = buffer + size; + parser->flags = (enum CborParserGlobalFlags)flags; + it->parser = parser; + it->source.ptr = buffer; + it->remaining = 1; /* there's one type altogether, usually an array or map */ + it->flags = 0; + return preparse_value(it); +} + +CborError cbor_parser_init_reader(const struct CborParserOperations *ops, CborParser *parser, CborValue *it, void *token) +{ + memset(parser, 0, sizeof(*parser)); + parser->source.ops = ops; + parser->flags = CborParserFlag_ExternalSource; + it->parser = parser; + it->source.token = token; + it->remaining = 1; + return preparse_value(it); +} + +/** + * \fn bool cbor_value_at_end(const CborValue *it) + * + * Returns true if \a it has reached the end of the iteration, usually when + * advancing after the last item in an array or map. + * + * In the case of the outermost CborValue object, this function returns true + * after decoding a single element. A pointer to the first byte of the + * remaining data (if any) can be obtained with cbor_value_get_next_byte(). + * + * \sa cbor_value_advance(), cbor_value_is_valid(), cbor_value_get_next_byte() + */ + +/** + * \fn const uint8_t *cbor_value_get_next_byte(const CborValue *it) + * + * Returns a pointer to the next byte that would be decoded if this CborValue + * object were advanced. + * + * This function is useful if cbor_value_at_end() returns true for the + * outermost CborValue: the pointer returned is the first byte of the data + * remaining in the buffer, if any. Code can decide whether to begin decoding a + * new CBOR data stream from this point, or parse some other data appended to + * the same buffer. + * + * This function may be used even after a parsing error. If that occurred, + * then this function returns a pointer to where the parsing error occurred. + * Note that the error recovery is not precise and the pointer may not indicate + * the exact byte containing bad data. + * + * This function makes sense only when using a linear buffer (that is, when the + * parser is initialize by cbor_parser_init()). If using an external source, + * this function may return garbage; instead, consult the external source itself + * to find out more details about the presence of more data. + * + * \sa cbor_value_at_end() + */ + +CborError cbor_value_reparse(CborValue *it) +{ + if (it->flags & CborIteratorFlag_IteratingStringChunks) + return CborNoError; + return preparse_next_value_nodecrement(it); +} + +/** + * \fn bool cbor_value_is_valid(const CborValue *it) + * + * Returns true if the iterator \a it contains a valid value. Invalid iterators + * happen when iteration reaches the end of a container (see \ref + * cbor_value_at_end()) or when a search function resulted in no matches. + * + * \sa cbor_value_advance(), cbor_value_at_end(), cbor_value_get_type() + */ + +/** + * Performs a basic validation of the CBOR stream pointed by \a it and returns + * the error it found. If no error was found, it returns CborNoError and the + * application can iterate over the items with certainty that no other errors + * will appear during parsing. + * + * A basic validation checks for: + * \list + * \li absence of undefined additional information bytes; + * \li well-formedness of all numbers, lengths, and simple values; + * \li string contents match reported sizes; + * \li arrays and maps contain the number of elements they are reported to have; + * \endlist + * + * For further checks, see cbor_value_validate(). + * + * This function has the same timing and memory requirements as + * cbor_value_advance(). + * + * \sa cbor_value_validate(), cbor_value_advance() + */ +CborError cbor_value_validate_basic(const CborValue *it) +{ + CborValue value = *it; + return cbor_value_advance(&value); +} + +/** + * Advances the CBOR value \a it by one fixed-size position. Fixed-size types + * are: integers, tags, simple types (including boolean, null and undefined + * values) and floating point types. + * + * If the type is not of fixed size, this function has undefined behavior. Code + * must be sure that the current type is one of the fixed-size types before + * calling this function. This function is provided because it can guarantee + * that it runs in constant time (O(1)). + * + * If the caller is not able to determine whether the type is fixed or not, code + * can use the cbor_value_advance() function instead. + * + * \sa cbor_value_at_end(), cbor_value_advance(), cbor_value_enter_container(), cbor_value_leave_container() + */ +CborError cbor_value_advance_fixed(CborValue *it) +{ + cbor_assert(it->type != CborInvalidType); + cbor_assert(is_fixed_type(it->type)); + if (!it->remaining) + return CborErrorAdvancePastEOF; + return advance_internal(it); +} + +static CborError advance_recursive(CborValue *it, int nestingLevel) +{ + CborError err; + CborValue recursed; + + if (is_fixed_type(it->type)) + return advance_internal(it); + + if (!cbor_value_is_container(it)) { + size_t len = SIZE_MAX; + return _cbor_value_copy_string(it, NULL, &len, it); + } + + /* map or array */ + if (nestingLevel == 0) + return CborErrorNestingTooDeep; + + err = cbor_value_enter_container(it, &recursed); + if (err) + return err; + while (!cbor_value_at_end(&recursed)) { + err = advance_recursive(&recursed, nestingLevel - 1); + if (err) + return err; + } + return cbor_value_leave_container(it, &recursed); +} + + +/** + * Advances the CBOR value \a it by one element, skipping over containers. + * Unlike cbor_value_advance_fixed(), this function can be called on a CBOR + * value of any type. However, if the type is a container (map or array) or a + * string with a chunked payload, this function will not run in constant time + * and will recurse into itself (it will run on O(n) time for the number of + * elements or chunks and will use O(n) memory for the number of nested + * containers). + * + * The number of recursions can be limited at compile time to avoid stack + * exhaustion in constrained systems. + * + * \sa cbor_value_at_end(), cbor_value_advance_fixed(), cbor_value_enter_container(), cbor_value_leave_container() + */ +CborError cbor_value_advance(CborValue *it) +{ + cbor_assert(it->type != CborInvalidType); + if (!it->remaining) + return CborErrorAdvancePastEOF; + return advance_recursive(it, CBOR_PARSER_MAX_RECURSIONS); +} + +/** + * \fn bool cbor_value_is_tag(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR tag. + * + * \sa cbor_value_get_tag(), cbor_value_skip_tag() + */ + +/** + * \fn CborError cbor_value_get_tag(const CborValue *value, CborTag *result) + * + * Retrieves the CBOR tag value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to a CBOR tag value, the + * behavior is undefined, so checking with \ref cbor_value_get_type or with + * \ref cbor_value_is_tag is recommended. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_tag() + */ + +/** + * Advances the CBOR value \a it until it no longer points to a tag. If \a it is + * already not pointing to a tag, then this function returns it unchanged. + * + * This function does not run in constant time: it will run on O(n) for n being + * the number of tags. It does use constant memory (O(1) memory requirements). + * + * \sa cbor_value_advance_fixed(), cbor_value_advance() + */ +CborError cbor_value_skip_tag(CborValue *it) +{ + while (cbor_value_is_tag(it)) { + CborError err = cbor_value_advance_fixed(it); + if (err) + return err; + } + return CborNoError; +} + +/** + * \fn bool cbor_value_is_container(const CborValue *it) + * + * Returns true if the \a it value is a container and requires recursion in + * order to decode (maps and arrays), false otherwise. + */ + +/** + * Creates a CborValue iterator pointing to the first element of the container + * represented by \a it and saves it in \a recursed. The \a it container object + * needs to be kept and passed again to cbor_value_leave_container() in order + * to continue iterating past this container. + * + * The \a it CborValue iterator must point to a container. + * + * \sa cbor_value_is_container(), cbor_value_leave_container(), cbor_value_advance() + */ +CborError cbor_value_enter_container(const CborValue *it, CborValue *recursed) +{ + cbor_static_assert(CborIteratorFlag_ContainerIsMap == (CborMapType & ~CborArrayType)); + cbor_assert(cbor_value_is_container(it)); + *recursed = *it; + + if (it->flags & CborIteratorFlag_UnknownLength) { + recursed->remaining = UINT32_MAX; + advance_bytes(recursed, 1); + } else { + uint64_t len = extract_number_and_advance(recursed); + + recursed->remaining = (uint32_t)len; + if (recursed->remaining != len || len == UINT32_MAX) { + /* back track the pointer to indicate where the error occurred */ + copy_current_position(recursed, it); + return CborErrorDataTooLarge; + } + if (recursed->type == CborMapType) { + /* maps have keys and values, so we need to multiply by 2 */ + if (recursed->remaining > UINT32_MAX / 2) { + /* back track the pointer to indicate where the error occurred */ + copy_current_position(recursed, it); + return CborErrorDataTooLarge; + } + recursed->remaining *= 2; + } + if (len == 0) { + /* the case of the empty container */ + recursed->type = CborInvalidType; + return CborNoError; + } + } + recursed->flags = (recursed->type & CborIteratorFlag_ContainerIsMap); + return preparse_next_value_nodecrement(recursed); +} + +/** + * Updates \a it to point to the next element after the container. The \a + * recursed object needs to point to the element obtained either by advancing + * the last element of the container (via cbor_value_advance(), + * cbor_value_advance_fixed(), a nested cbor_value_leave_container(), or the \c + * next pointer from cbor_value_copy_string() or cbor_value_dup_string()). + * + * The \a it and \a recursed parameters must be the exact same as passed to + * cbor_value_enter_container(). + * + * \sa cbor_value_enter_container(), cbor_value_at_end() + */ +CborError cbor_value_leave_container(CborValue *it, const CborValue *recursed) +{ + cbor_assert(cbor_value_is_container(it)); + cbor_assert(recursed->type == CborInvalidType); + + copy_current_position(it, recursed); + if (recursed->flags & CborIteratorFlag_UnknownLength) + advance_bytes(it, 1); + return preparse_next_value(it); +} + + +/** + * \fn CborType cbor_value_get_type(const CborValue *value) + * + * Returns the type of the CBOR value that the iterator \a value points to. If + * \a value does not point to a valid value, this function returns \ref + * CborInvalidType. + * + * TinyCBOR also provides functions to test directly if a given CborValue object + * is of a given type, like cbor_value_is_text_string() and cbor_value_is_null(). + * + * \sa cbor_value_is_valid() + */ + +/** + * \fn bool cbor_value_is_null(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR null type. + * + * \sa cbor_value_is_valid(), cbor_value_is_undefined() + */ + +/** + * \fn bool cbor_value_is_undefined(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR undefined type. + * + * \sa cbor_value_is_valid(), cbor_value_is_null() + */ + +/** + * \fn bool cbor_value_is_boolean(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR boolean + * type (true or false). + * + * \sa cbor_value_is_valid(), cbor_value_get_boolean() + */ + +/** + * \fn CborError cbor_value_get_boolean(const CborValue *value, bool *result) + * + * Retrieves the boolean value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to a boolean value, the + * behavior is undefined, so checking with \ref cbor_value_get_type or with + * \ref cbor_value_is_boolean is recommended. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_boolean() + */ + +/** + * \fn bool cbor_value_is_simple_type(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR Simple Type + * type (other than true, false, null and undefined). + * + * \sa cbor_value_is_valid(), cbor_value_get_simple_type() + */ + +/** + * \fn CborError cbor_value_get_simple_type(const CborValue *value, uint8_t *result) + * + * Retrieves the CBOR Simple Type value that \a value points to and stores it + * in \a result. If the iterator \a value does not point to a simple_type + * value, the behavior is undefined, so checking with \ref cbor_value_get_type + * or with \ref cbor_value_is_simple_type is recommended. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_simple_type() + */ + +/** + * \fn bool cbor_value_is_integer(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR integer + * type. + * + * \sa cbor_value_is_valid(), cbor_value_get_int, cbor_value_get_int64, cbor_value_get_uint64, cbor_value_get_raw_integer + */ + +/** + * \fn bool cbor_value_is_unsigned_integer(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR unsigned + * integer type (positive values or zero). + * + * \sa cbor_value_is_valid(), cbor_value_get_uint64() + */ + +/** + * \fn bool cbor_value_is_negative_integer(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR negative + * integer type. + * + * \sa cbor_value_is_valid(), cbor_value_get_int, cbor_value_get_int64, cbor_value_get_raw_integer + */ + +/** + * \fn CborError cbor_value_get_int(const CborValue *value, int *result) + * + * Retrieves the CBOR integer value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to an integer value, the + * behavior is undefined, so checking with \ref cbor_value_get_type or with + * \ref cbor_value_is_integer is recommended. + * + * Note that this function does not do range-checking: integral values that do + * not fit in a variable of type \c{int} are silently truncated to fit. Use + * cbor_value_get_int_checked() if that is not acceptable. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_integer() + */ + +/** + * \fn CborError cbor_value_get_int64(const CborValue *value, int64_t *result) + * + * Retrieves the CBOR integer value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to an integer value, the + * behavior is undefined, so checking with \ref cbor_value_get_type or with + * \ref cbor_value_is_integer is recommended. + * + * Note that this function does not do range-checking: integral values that do + * not fit in a variable of type \c{int64_t} are silently truncated to fit. Use + * cbor_value_get_int64_checked() that is not acceptable. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_integer() + */ + +/** + * \fn CborError cbor_value_get_uint64(const CborValue *value, uint64_t *result) + * + * Retrieves the CBOR integer value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to an unsigned integer + * value, the behavior is undefined, so checking with \ref cbor_value_get_type + * or with \ref cbor_value_is_unsigned_integer is recommended. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_unsigned_integer() + */ + +/** + * \fn CborError cbor_value_get_raw_integer(const CborValue *value, uint64_t *result) + * + * Retrieves the CBOR integer value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to an integer value, the + * behavior is undefined, so checking with \ref cbor_value_get_type or with + * \ref cbor_value_is_integer is recommended. + * + * This function is provided because CBOR negative integers can assume values + * that cannot be represented with normal 64-bit integer variables. + * + * If the integer is unsigned (that is, if cbor_value_is_unsigned_integer() + * returns true), then \a result will contain the actual value. If the integer + * is negative, then \a result will contain the absolute value of that integer, + * minus one. That is, \c {actual = -result - 1}. On architectures using two's + * complement for representation of negative integers, it is equivalent to say + * that \a result will contain the bitwise negation of the actual value. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_integer() + */ + +/** + * Retrieves the CBOR integer value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to an integer value, the + * behavior is undefined, so checking with \ref cbor_value_get_type or with + * \ref cbor_value_is_integer is recommended. + * + * Unlike \ref cbor_value_get_int64(), this function performs a check to see if the + * stored integer fits in \a result without data loss. If the number is outside + * the valid range for the data type, this function returns the recoverable + * error CborErrorDataTooLarge. In that case, use either + * cbor_value_get_uint64() (if the number is positive) or + * cbor_value_get_raw_integer(). + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_integer(), cbor_value_get_int64() + */ +CborError cbor_value_get_int64_checked(const CborValue *value, int64_t *result) +{ + uint64_t v; + cbor_assert(cbor_value_is_integer(value)); + v = _cbor_value_extract_int64_helper(value); + + /* Check before converting, as the standard says (C11 6.3.1.3 paragraph 3): + * "[if] the new type is signed and the value cannot be represented in it; either the + * result is implementation-defined or an implementation-defined signal is raised." + * + * The range for int64_t is -2^63 to 2^63-1 (int64_t is required to be + * two's complement, C11 7.20.1.1 paragraph 3), which in CBOR is + * represented the same way, differing only on the "sign bit" (the major + * type). + */ + + if (unlikely(v > (uint64_t)INT64_MAX)) + return CborErrorDataTooLarge; + + *result = v; + if (value->flags & CborIteratorFlag_NegativeInteger) + *result = -*result - 1; + return CborNoError; +} + +/** + * Retrieves the CBOR integer value that \a value points to and stores it in \a + * result. If the iterator \a value does not point to an integer value, the + * behavior is undefined, so checking with \ref cbor_value_get_type or with + * \ref cbor_value_is_integer is recommended. + * + * Unlike \ref cbor_value_get_int(), this function performs a check to see if the + * stored integer fits in \a result without data loss. If the number is outside + * the valid range for the data type, this function returns the recoverable + * error CborErrorDataTooLarge. In that case, use one of the other integer + * functions to obtain the value. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_integer(), cbor_value_get_int64(), + * cbor_value_get_uint64(), cbor_value_get_int64_checked(), cbor_value_get_raw_integer() + */ +CborError cbor_value_get_int_checked(const CborValue *value, int *result) +{ + uint64_t v; + cbor_assert(cbor_value_is_integer(value)); + v = _cbor_value_extract_int64_helper(value); + + /* Check before converting, as the standard says (C11 6.3.1.3 paragraph 3): + * "[if] the new type is signed and the value cannot be represented in it; either the + * result is implementation-defined or an implementation-defined signal is raised." + * + * But we can convert from signed to unsigned without fault (paragraph 2). + * + * The range for int is implementation-defined and int is not guaranteed to use + * two's complement representation (although int32_t is). + */ + + if (value->flags & CborIteratorFlag_NegativeInteger) { + if (unlikely(v > (unsigned) -(INT_MIN + 1))) + return CborErrorDataTooLarge; + + *result = (int)v; + *result = -*result - 1; + } else { + if (unlikely(v > (uint64_t)INT_MAX)) + return CborErrorDataTooLarge; + + *result = (int)v; + } + return CborNoError; + +} + +/** + * \fn bool cbor_value_is_length_known(const CborValue *value) + * + * Returns true if the length of this type is known without calculation. That + * is, if the length of this CBOR string, map or array is encoded in the data + * stream, this function returns true. If the length is not encoded, it returns + * false. + * + * If the length is known, code can call cbor_value_get_string_length(), + * cbor_value_get_array_length() or cbor_value_get_map_length() to obtain the + * length. If the length is not known but is necessary, code can use the + * cbor_value_calculate_string_length() function (no equivalent function is + * provided for maps and arrays). + */ + +/** + * \fn bool cbor_value_is_text_string(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR text + * string. CBOR text strings are UTF-8 encoded and usually contain + * human-readable text. + * + * \sa cbor_value_is_valid(), cbor_value_get_string_length(), cbor_value_calculate_string_length(), + * cbor_value_copy_text_string(), cbor_value_dup_text_string() + */ + +/** + * \fn bool cbor_value_is_byte_string(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR byte + * string. CBOR byte strings are binary data with no specified encoding or + * format. + * + * \sa cbor_value_is_valid(), cbor_value_get_string_length(), cbor_value_calculate_string_length(), + * cbor_value_copy_byte_string(), cbor_value_dup_byte_string() + */ + +/** + * \fn CborError cbor_value_get_string_length(const CborValue *value, size_t *length) + * + * Extracts the length of the byte or text string that \a value points to and + * stores it in \a result. If the iterator \a value does not point to a text + * string or a byte string, the behaviour is undefined, so checking with \ref + * cbor_value_get_type, with \ref cbor_value_is_text_string or \ref + * cbor_value_is_byte_string is recommended. + * + * If the length of this string is not encoded in the CBOR data stream, this + * function will return the recoverable error CborErrorUnknownLength. You may + * also check whether that is the case by using cbor_value_is_length_known(). + * + * If the length of the string is required but the length was not encoded, use + * cbor_value_calculate_string_length(), but note that that function does not + * run in constant time. + * + * \note On 32-bit platforms, this function will return error condition of \ref + * CborErrorDataTooLarge if the stream indicates a length that is too big to + * fit in 32-bit. + * + * \sa cbor_value_is_valid(), cbor_value_is_length_known(), cbor_value_calculate_string_length() + */ + +/** + * Calculates the length of the byte or text string that \a value points to and + * stores it in \a len. If the iterator \a value does not point to a text + * string or a byte string, the behaviour is undefined, so checking with \ref + * cbor_value_get_type, with \ref cbor_value_is_text_string or \ref + * cbor_value_is_byte_string is recommended. + * + * This function is different from cbor_value_get_string_length() in that it + * calculates the length even for strings sent in chunks. For that reason, this + * function may not run in constant time (it will run in O(n) time on the + * number of chunks). It does use constant memory (O(1)). + * + * \note On 32-bit platforms, this function will return error condition of \ref + * CborErrorDataTooLarge if the stream indicates a length that is too big to + * fit in 32-bit. + * + * \sa cbor_value_get_string_length(), cbor_value_copy_text_string(), cbor_value_copy_byte_string(), cbor_value_is_length_known() + */ +CborError cbor_value_calculate_string_length(const CborValue *value, size_t *len) +{ + *len = SIZE_MAX; + return _cbor_value_copy_string(value, NULL, len, NULL); +} + +CborError _cbor_value_begin_string_iteration(CborValue *it) +{ + it->flags |= CborIteratorFlag_IteratingStringChunks | + CborIteratorFlag_BeforeFirstStringChunk; + if (!cbor_value_is_length_known(it)) { + /* chunked string: we're before the first chunk; + * advance to the first chunk */ + advance_bytes(it, 1); + } + + return CborNoError; +} + +CborError _cbor_value_finish_string_iteration(CborValue *it) +{ + if (!cbor_value_is_length_known(it)) + advance_bytes(it, 1); /* skip the Break */ + + return preparse_next_value(it); +} + +static CborError get_string_chunk_size(const CborValue *it, size_t *offset, size_t *len) +{ + uint8_t descriptor; + size_t bytesNeeded = 1; + + if (cbor_value_is_length_known(it) && (it->flags & CborIteratorFlag_BeforeFirstStringChunk) == 0) + return CborErrorNoMoreStringChunks; + + /* are we at the end? */ + if (!read_bytes(it, &descriptor, 0, 1)) + return CborErrorUnexpectedEOF; + + if (descriptor == BreakByte) + return CborErrorNoMoreStringChunks; + if ((descriptor & MajorTypeMask) != it->type) + return CborErrorIllegalType; + + /* find the string length */ + descriptor &= SmallValueMask; + if (descriptor < Value8Bit) { + *len = descriptor; + } else if (unlikely(descriptor > Value64Bit)) { + return CborErrorIllegalNumber; + } else { + uint64_t val; + bytesNeeded = (size_t)(1 << (descriptor - Value8Bit)); + if (!can_read_bytes(it, 1 + bytesNeeded)) + return CborErrorUnexpectedEOF; + + if (descriptor <= Value16Bit) { + if (descriptor == Value16Bit) + val = read_uint16(it, 1); + else + val = read_uint8(it, 1); + } else { + if (descriptor == Value32Bit) + val = read_uint32(it, 1); + else + val = read_uint64(it, 1); + } + + *len = val; + if (*len != val) + return CborErrorDataTooLarge; + + ++bytesNeeded; + } + + *offset = bytesNeeded; + return CborNoError; +} + +CborError _cbor_value_get_string_chunk_size(const CborValue *value, size_t *len) +{ + size_t offset; + return get_string_chunk_size(value, &offset, len); +} + +static CborError get_string_chunk(CborValue *it, const void **bufferptr, size_t *len) +{ + size_t offset; + CborError err = get_string_chunk_size(it, &offset, len); + if (err) + return err; + + /* we're good, transfer the string now */ + err = transfer_string(it, bufferptr, offset, *len); + if (err) + return err; + + /* we've iterated at least once */ + it->flags &= ~CborIteratorFlag_BeforeFirstStringChunk; + return CborNoError; +} + +/** + * \fn CborError cbor_value_get_text_string_chunk(const CborValue *value, const char **bufferptr, size_t *len, CborValue *next) + * + * Extracts one text string chunk pointed to by \a value and stores a pointer + * to the data in \a buffer and the size in \a len, which must not be null. If + * no more chunks are available, then \a bufferptr will be set to null. This + * function may be used to iterate over any string without causing its contents + * to be copied to a separate buffer, like the convenience function + * cbor_value_copy_text_string() does. + * + * It is designed to be used in code like: + * + * \code + * if (cbor_value_is_text_string(value)) { + * char *ptr; + * size_t len; + * while (1) { + * err = cbor_value_get_text_string_chunk(value, &ptr, &len, &value)); + * if (err) return err; + * if (ptr == NULL) return CborNoError; + * consume(ptr, len); + * } + * } + * \endcode + * + * If the iterator \a value does not point to a text string, the behaviour is + * undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_text_string is recommended. + * + * The \a next pointer, if not null, will be updated to point to the next item + * after this string. During iteration, the pointer must only be passed back + * again to this function; passing it to any other function in this library + * results in undefined behavior. If there are no more chunks to be read from + * \a value, then \a next will be set to the next item after this string; if \a + * value points to the last item, then \a next will be invalid. + * + * \note This function does not perform UTF-8 validation on the incoming text + * string. + * + * \sa cbor_value_dup_text_string(), cbor_value_copy_text_string(), cbor_value_caculate_string_length(), cbor_value_get_byte_string_chunk() + */ + +/** + * \fn CborError cbor_value_get_byte_string_chunk(const CborValue *value, const char **bufferptr, size_t *len, CborValue *next) + * + * Extracts one byte string chunk pointed to by \a value and stores a pointer + * to the data in \a buffer and the size in \a len, which must not be null. If + * no more chunks are available, then \a bufferptr will be set to null. This + * function may be used to iterate over any string without causing its contents + * to be copied to a separate buffer, like the convenience function + * cbor_value_copy_byte_string() does. + * + * It is designed to be used in code like: + * + * \code + * if (cbor_value_is_byte_string(value)) { + * char *ptr; + * size_t len; + * while (1) { + * err = cbor_value_get_byte_string_chunk(value, &ptr, &len, &value)); + * if (err) return err; + * if (ptr == NULL) return CborNoError; + * consume(ptr, len); + * } + * } + * \endcode + * + * If the iterator \a value does not point to a byte string, the behaviour is + * undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_byte_string is recommended. + * + * The \a next pointer, if not null, will be updated to point to the next item + * after this string. During iteration, the pointer must only be passed back + * again to this function; passing it to any other function in this library + * results in undefined behavior. If there are no more chunks to be read from + * \a value, then \a next will be set to the next item after this string; if \a + * value points to the last item, then \a next will be invalid. + * + * \sa cbor_value_dup_byte_string(), cbor_value_copy_byte_string(), cbor_value_caculate_string_length(), cbor_value_get_text_string_chunk() + */ + +CborError _cbor_value_get_string_chunk(const CborValue *value, const void **bufferptr, + size_t *len, CborValue *next) +{ + CborValue tmp; + if (!next) + next = &tmp; + *next = *value; + return get_string_chunk(next, bufferptr, len); +} + +/* We return uintptr_t so that we can pass memcpy directly as the iteration + * function. The choice is to optimize for memcpy, which is used in the base + * parser API (cbor_value_copy_string), while memcmp is used in convenience API + * only. */ +typedef uintptr_t (*IterateFunction)(char *, const uint8_t *, size_t); + +static uintptr_t iterate_noop(char *dest, const uint8_t *src, size_t len) +{ + (void)dest; + (void)src; + (void)len; + return true; +} + +static uintptr_t iterate_memcmp(char *s1, const uint8_t *s2, size_t len) +{ + return memcmp(s1, (const char *)s2, len) == 0; +} + +static uintptr_t iterate_memcpy(char *dest, const uint8_t *src, size_t len) +{ + return (uintptr_t)memcpy(dest, src, len); +} + +static CborError iterate_string_chunks(const CborValue *value, char *buffer, size_t *buflen, + bool *result, CborValue *next, IterateFunction func) +{ + CborError err; + CborValue tmp; + size_t total = 0; + const void *ptr; + + cbor_assert(cbor_value_is_byte_string(value) || cbor_value_is_text_string(value)); + if (!next) + next = &tmp; + *next = *value; + *result = true; + + err = _cbor_value_begin_string_iteration(next); + if (err) + return err; + + while (1) { + size_t newTotal; + size_t chunkLen; + err = get_string_chunk(next, &ptr, &chunkLen); + if (err == CborErrorNoMoreStringChunks) + break; + if (err) + return err; + + if (unlikely(add_check_overflow(total, chunkLen, &newTotal))) + return CborErrorDataTooLarge; + + if (*result && *buflen >= newTotal) + *result = !!func(buffer + total, (const uint8_t *)ptr, chunkLen); + else + *result = false; + + total = newTotal; + } + + /* is there enough room for the ending NUL byte? */ + if (*result && *buflen > total) { + uint8_t nul[] = { 0 }; + *result = !!func(buffer + total, nul, 1); + } + *buflen = total; + return _cbor_value_finish_string_iteration(next); +} + +/** + * \fn CborError cbor_value_copy_text_string(const CborValue *value, char *buffer, size_t *buflen, CborValue *next) + * + * Copies the string pointed to by \a value into the buffer provided at \a buffer + * of \a buflen bytes. If \a buffer is a NULL pointer, this function will not + * copy anything and will only update the \a next value. + * + * If the iterator \a value does not point to a text string, the behaviour is + * undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_text_string is recommended. + * + * If the provided buffer length was too small, this function returns an error + * condition of \ref CborErrorOutOfMemory. If you need to calculate the length + * of the string in order to preallocate a buffer, use + * cbor_value_calculate_string_length(). + * + * On success, this function sets the number of bytes copied to \c{*buflen}. If + * the buffer is large enough, this function will insert a null byte after the + * last copied byte, to facilitate manipulation of text strings. That byte is + * not included in the returned value of \c{*buflen}. If there was no space for + * the terminating null, no error is returned, so callers must check the value + * of *buflen after the call, before relying on the '\0'; if it has not been + * changed by the call, there is no '\0'-termination on the buffer's contents. + * + * The \a next pointer, if not null, will be updated to point to the next item + * after this string. If \a value points to the last item, then \a next will be + * invalid. + * + * This function may not run in constant time (it will run in O(n) time on the + * number of chunks). It requires constant memory (O(1)). + * + * \note This function does not perform UTF-8 validation on the incoming text + * string. + * + * \sa cbor_value_get_text_string_chunk() cbor_value_dup_text_string(), cbor_value_copy_byte_string(), cbor_value_get_string_length(), cbor_value_calculate_string_length() + */ + +/** + * \fn CborError cbor_value_copy_byte_string(const CborValue *value, uint8_t *buffer, size_t *buflen, CborValue *next) + * + * Copies the string pointed by \a value into the buffer provided at \a buffer + * of \a buflen bytes. If \a buffer is a NULL pointer, this function will not + * copy anything and will only update the \a next value. + * + * If the iterator \a value does not point to a byte string, the behaviour is + * undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_byte_string is recommended. + * + * If the provided buffer length was too small, this function returns an error + * condition of \ref CborErrorOutOfMemory. If you need to calculate the length + * of the string in order to preallocate a buffer, use + * cbor_value_calculate_string_length(). + * + * On success, this function sets the number of bytes copied to \c{*buflen}. If + * the buffer is large enough, this function will insert a null byte after the + * last copied byte, to facilitate manipulation of null-terminated strings. + * That byte is not included in the returned value of \c{*buflen}. + * + * The \a next pointer, if not null, will be updated to point to the next item + * after this string. If \a value points to the last item, then \a next will be + * invalid. + * + * This function may not run in constant time (it will run in O(n) time on the + * number of chunks). It requires constant memory (O(1)). + * + * \sa cbor_value_get_byte_string_chunk(), cbor_value_dup_text_string(), cbor_value_copy_text_string(), cbor_value_get_string_length(), cbor_value_calculate_string_length() + */ + +CborError _cbor_value_copy_string(const CborValue *value, void *buffer, + size_t *buflen, CborValue *next) +{ + bool copied_all; + CborError err = iterate_string_chunks(value, (char*)buffer, buflen, &copied_all, next, + buffer ? iterate_memcpy : iterate_noop); + return err ? err : + copied_all ? CborNoError : CborErrorOutOfMemory; +} + +/** + * Compares the entry \a value with the string \a string and stores the result + * in \a result. If the value is different from \a string \a result will + * contain \c false. + * + * The entry at \a value may be a tagged string. If \a value is not a string or + * a tagged string, the comparison result will be false. + * + * CBOR requires text strings to be encoded in UTF-8, but this function does + * not validate either the strings in the stream or the string \a string to be + * matched. Moreover, comparison is done on strict codepoint comparison, + * without any Unicode normalization. + * + * This function may not run in constant time (it will run in O(n) time on the + * number of chunks). It requires constant memory (O(1)). + * + * \sa cbor_value_skip_tag(), cbor_value_copy_text_string() + */ +CborError cbor_value_text_string_equals(const CborValue *value, const char *string, bool *result) +{ + size_t len; + CborValue copy = *value; + CborError err = cbor_value_skip_tag(©); + if (err) + return err; + if (!cbor_value_is_text_string(©)) { + *result = false; + return CborNoError; + } + + len = strlen(string); + return iterate_string_chunks(©, CONST_CAST(char *, string), &len, result, NULL, iterate_memcmp); +} + +/** + * \fn bool cbor_value_is_array(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR array. + * + * \sa cbor_value_is_valid(), cbor_value_is_map() + */ + +/** + * \fn CborError cbor_value_get_array_length(const CborValue *value, size_t *length) + * + * Extracts the length of the CBOR array that \a value points to and stores it + * in \a result. If the iterator \a value does not point to a CBOR array, the + * behaviour is undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_array is recommended. + * + * If the length of this array is not encoded in the CBOR data stream, this + * function will return the recoverable error CborErrorUnknownLength. You may + * also check whether that is the case by using cbor_value_is_length_known(). + * + * \note On 32-bit platforms, this function will return error condition of \ref + * CborErrorDataTooLarge if the stream indicates a length that is too big to + * fit in 32-bit. + * + * \sa cbor_value_is_valid(), cbor_value_is_length_known() + */ + +/** + * \fn bool cbor_value_is_map(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR map. + * + * \sa cbor_value_is_valid(), cbor_value_is_array() + */ + +/** + * \fn CborError cbor_value_get_map_length(const CborValue *value, size_t *length) + * + * Extracts the length of the CBOR map that \a value points to and stores it in + * \a result. If the iterator \a value does not point to a CBOR map, the + * behaviour is undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_map is recommended. + * + * If the length of this map is not encoded in the CBOR data stream, this + * function will return the recoverable error CborErrorUnknownLength. You may + * also check whether that is the case by using cbor_value_is_length_known(). + * + * \note On 32-bit platforms, this function will return error condition of \ref + * CborErrorDataTooLarge if the stream indicates a length that is too big to + * fit in 32-bit. + * + * \sa cbor_value_is_valid(), cbor_value_is_length_known() + */ + +/** + * Attempts to find the value in map \a map that corresponds to the text string + * entry \a string. If the iterator \a value does not point to a CBOR map, the + * behaviour is undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_map is recommended. + * + * If the item is found, it is stored in \a result. If no item is found + * matching the key, then \a result will contain an element of type \ref + * CborInvalidType. Matching is performed using + * cbor_value_text_string_equals(), so tagged strings will also match. + * + * This function has a time complexity of O(n) where n is the number of + * elements in the map to be searched. In addition, this function is has O(n) + * memory requirement based on the number of nested containers (maps or arrays) + * found as elements of this map. + * + * \sa cbor_value_is_valid(), cbor_value_text_string_equals(), cbor_value_advance() + */ +CborError cbor_value_map_find_value(const CborValue *map, const char *string, CborValue *element) +{ + CborError err; + size_t len = strlen(string); + cbor_assert(cbor_value_is_map(map)); + err = cbor_value_enter_container(map, element); + if (err) + goto error; + + while (!cbor_value_at_end(element)) { + /* find the non-tag so we can compare */ + err = cbor_value_skip_tag(element); + if (err) + goto error; + if (cbor_value_is_text_string(element)) { + bool equals; + size_t dummyLen = len; + err = iterate_string_chunks(element, CONST_CAST(char *, string), &dummyLen, + &equals, element, iterate_memcmp); + if (err) + goto error; + if (equals) + return preparse_value(element); + } else { + /* skip this key */ + err = cbor_value_advance(element); + if (err) + goto error; + } + + /* skip this value */ + err = cbor_value_skip_tag(element); + if (err) + goto error; + err = cbor_value_advance(element); + if (err) + goto error; + } + + /* not found */ + element->type = CborInvalidType; + return CborNoError; + +error: + element->type = CborInvalidType; + return err; +} + +/** + * \fn bool cbor_value_is_float(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR + * single-precision floating point (32-bit). + * + * \sa cbor_value_is_valid(), cbor_value_is_double(), cbor_value_is_half_float() + */ + +/** + * \fn CborError cbor_value_get_float(const CborValue *value, float *result) + * + * Retrieves the CBOR single-precision floating point (32-bit) value that \a + * value points to and stores it in \a result. If the iterator \a value does + * not point to a single-precision floating point value, the behavior is + * undefined, so checking with \ref cbor_value_get_type or with \ref + * cbor_value_is_float is recommended. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_float(), cbor_value_get_double() + */ + +/** + * \fn bool cbor_value_is_double(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR + * double-precision floating point (64-bit). + * + * \sa cbor_value_is_valid(), cbor_value_is_float(), cbor_value_is_half_float() + */ + +/** + * \fn CborError cbor_value_get_double(const CborValue *value, float *result) + * + * Retrieves the CBOR double-precision floating point (64-bit) value that \a + * value points to and stores it in \a result. If the iterator \a value does + * not point to a double-precision floating point value, the behavior is + * undefined, so checking with \ref cbor_value_get_type or with \ref + * cbor_value_is_double is recommended. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_double(), cbor_value_get_float() + */ + +/** + * \fn bool cbor_value_is_half_float(const CborValue *value) + * + * Returns true if the iterator \a value is valid and points to a CBOR + * single-precision floating point (16-bit). + * + * \sa cbor_value_is_valid(), cbor_value_is_double(), cbor_value_is_float() + */ + +/** + * \fn CborError cbor_value_get_half_float(const CborValue *value, void *result) + * + * Retrieves the CBOR half-precision floating point (16-bit) value that \a + * value points to and stores it in \a result. If the iterator \a value does + * not point to a half-precision floating point value, the behavior is + * undefined, so checking with \ref cbor_value_get_type or with \ref + * cbor_value_is_half_float is recommended. + * + * Note: since the C language does not have a standard type for half-precision + * floating point, this function takes a \c{void *} as a parameter for the + * storage area, which must be at least 16 bits wide. + * + * \sa cbor_value_get_type(), cbor_value_is_valid(), cbor_value_is_half_float(), cbor_value_get_half_float_as_float(), cbor_value_get_float() + */ + +/** @} */ diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser_dup_string.c b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser_dup_string.c new file mode 100644 index 000000000..061c5ac77 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser_dup_string.c @@ -0,0 +1,119 @@ +/**************************************************************************** +** +** Copyright (C) 2016 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#ifndef _BSD_SOURCE +#define _BSD_SOURCE 1 +#endif +#ifndef _DEFAULT_SOURCE +#define _DEFAULT_SOURCE 1 +#endif +#ifndef __STDC_LIMIT_MACROS +# define __STDC_LIMIT_MACROS 1 +#endif + +#include "cbor.h" +#include "compilersupport_p.h" +#include + +/** + * \fn CborError cbor_value_dup_text_string(const CborValue *value, char **buffer, size_t *buflen, CborValue *next) + * + * Allocates memory for the string pointed by \a value and copies it into this + * buffer. The pointer to the buffer is stored in \a buffer and the number of + * bytes copied is stored in \a buflen (those variables must not be NULL). + * + * If the iterator \a value does not point to a text string, the behaviour is + * undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_text_string is recommended. + * + * If \c malloc returns a NULL pointer, this function will return error + * condition \ref CborErrorOutOfMemory. + * + * On success, \c{*buffer} will contain a valid pointer that must be freed by + * calling \c{free()}. This is the case even for zero-length strings. + * + * The \a next pointer, if not null, will be updated to point to the next item + * after this string. If \a value points to the last item, then \a next will be + * invalid. + * + * This function may not run in constant time (it will run in O(n) time on the + * number of chunks). It requires constant memory (O(1)) in addition to the + * malloc'ed block. + * + * \note This function does not perform UTF-8 validation on the incoming text + * string. + * + * \sa cbor_value_get_text_string_chunk(), cbor_value_copy_text_string(), cbor_value_dup_byte_string() + */ + +/** + * \fn CborError cbor_value_dup_byte_string(const CborValue *value, uint8_t **buffer, size_t *buflen, CborValue *next) + * + * Allocates memory for the string pointed by \a value and copies it into this + * buffer. The pointer to the buffer is stored in \a buffer and the number of + * bytes copied is stored in \a buflen (those variables must not be NULL). + * + * If the iterator \a value does not point to a byte string, the behaviour is + * undefined, so checking with \ref cbor_value_get_type or \ref + * cbor_value_is_byte_string is recommended. + * + * If \c malloc returns a NULL pointer, this function will return error + * condition \ref CborErrorOutOfMemory. + * + * On success, \c{*buffer} will contain a valid pointer that must be freed by + * calling \c{free()}. This is the case even for zero-length strings. + * + * The \a next pointer, if not null, will be updated to point to the next item + * after this string. If \a value points to the last item, then \a next will be + * invalid. + * + * This function may not run in constant time (it will run in O(n) time on the + * number of chunks). It requires constant memory (O(1)) in addition to the + * malloc'ed block. + * + * \sa cbor_value_get_text_string_chunk(), cbor_value_copy_byte_string(), cbor_value_dup_text_string() + */ +CborError _cbor_value_dup_string(const CborValue *value, void **buffer, size_t *buflen, CborValue *next) +{ + CborError err; + cbor_assert(buffer); + cbor_assert(buflen); + *buflen = SIZE_MAX; + err = _cbor_value_copy_string(value, NULL, buflen, NULL); + if (err) + return err; + + ++*buflen; + *buffer = malloc(*buflen); + if (!*buffer) { + /* out of memory */ + return CborErrorOutOfMemory; + } + err = _cbor_value_copy_string(value, *buffer, buflen, next); + if (err) { + free(*buffer); + return err; + } + return CborNoError; +} diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/compilersupport_p.h b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/compilersupport_p.h new file mode 100644 index 000000000..087980161 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/compilersupport_p.h @@ -0,0 +1,205 @@ +/**************************************************************************** +** +** Copyright (C) 2017 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#ifndef COMPILERSUPPORT_H +#define COMPILERSUPPORT_H + +#include "cbor.h" + +#ifndef _BSD_SOURCE +# define _BSD_SOURCE +#endif +#ifndef _DEFAULT_SOURCE +# define _DEFAULT_SOURCE +#endif +#ifndef assert +# include +#endif +#include +#include +#include + +#ifndef __cplusplus +# include +#endif + +#if __STDC_VERSION__ >= 201112L || (defined(__cplusplus) && __cplusplus >= 201103L) || (defined(__cpp_static_assert) && __cpp_static_assert >= 200410) +# define cbor_static_assert(x) static_assert(x, #x) +#elif !defined(__cplusplus) && defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__ >= 406) && (__STDC_VERSION__ > 199901L) +# define cbor_static_assert(x) _Static_assert(x, #x) +#else +# define cbor_static_assert(x) ((void)sizeof(char[2*!!(x) - 1])) +#endif +#if __STDC_VERSION__ >= 199901L || defined(__cplusplus) +/* inline is a keyword */ +#else +/* use the definition from cbor.h */ +# define inline CBOR_INLINE +#endif + +#ifdef NDEBUG +# define cbor_assert(cond) do { if (!(cond)) unreachable(); } while (0) +#else +# define cbor_assert(cond) assert(cond) +#endif + +#ifndef STRINGIFY +#define STRINGIFY(x) STRINGIFY2(x) +#endif +#define STRINGIFY2(x) #x + +#if !defined(UINT32_MAX) || !defined(INT64_MAX) +/* C89? We can define UINT32_MAX portably, but not INT64_MAX */ +# error "Your system has stdint.h but that doesn't define UINT32_MAX or INT64_MAX" +#endif + +#ifndef DBL_DECIMAL_DIG +/* DBL_DECIMAL_DIG is C11 */ +# define DBL_DECIMAL_DIG 17 +#endif +#define DBL_DECIMAL_DIG_STR STRINGIFY(DBL_DECIMAL_DIG) + +#if defined(__GNUC__) && defined(__i386__) && !defined(__iamcu__) +# define CBOR_INTERNAL_API_CC __attribute__((regparm(3))) +#elif defined(_MSC_VER) && defined(_M_IX86) +# define CBOR_INTERNAL_API_CC __fastcall +#else +# define CBOR_INTERNAL_API_CC +#endif + +#ifndef __has_builtin +# define __has_builtin(x) 0 +#endif + +#if (defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__ >= 403)) || \ + (__has_builtin(__builtin_bswap64) && __has_builtin(__builtin_bswap32)) +# if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ +# define cbor_ntohll __builtin_bswap64 +# define cbor_htonll __builtin_bswap64 +# define cbor_ntohl __builtin_bswap32 +# define cbor_htonl __builtin_bswap32 +# ifdef __INTEL_COMPILER +# define cbor_ntohs _bswap16 +# define cbor_htons _bswap16 +# elif (__GNUC__ * 100 + __GNUC_MINOR__ >= 608) || __has_builtin(__builtin_bswap16) +# define cbor_ntohs __builtin_bswap16 +# define cbor_htons __builtin_bswap16 +# else +# define cbor_ntohs(x) (((uint16_t)(x) >> 8) | ((uint16_t)(x) << 8)) +# define cbor_htons cbor_ntohs +# endif +# else +# define cbor_ntohll +# define cbor_htonll +# define cbor_ntohl +# define cbor_htonl +# define cbor_ntohs +# define cbor_htons +# endif +#elif defined(__sun) +# include +#elif defined(_MSC_VER) +/* MSVC, which implies Windows, which implies little-endian and sizeof(long) == 4 */ +# include +# define cbor_ntohll _byteswap_uint64 +# define cbor_htonll _byteswap_uint64 +# define cbor_ntohl _byteswap_ulong +# define cbor_htonl _byteswap_ulong +# define cbor_ntohs _byteswap_ushort +# define cbor_htons _byteswap_ushort +#endif +#ifndef cbor_ntohs +# include +# define cbor_ntohs ntohs +# define cbor_htons htons +#endif +#ifndef cbor_ntohl +# include +# define cbor_ntohl ntohl +# define cbor_htonl htonl +#endif +#ifndef cbor_ntohll +# define cbor_ntohll ntohll +# define cbor_htonll htonll +/* ntohll isn't usually defined */ +# ifndef ntohll +# if (defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) || \ + (defined(__BYTE_ORDER) && defined(__BIG_ENDIAN) && __BYTE_ORDER == __BIG_ENDIAN) || \ + (defined(BYTE_ORDER) && defined(BIG_ENDIAN) && BYTE_ORDER == BIG_ENDIAN) || \ + (defined(_BIG_ENDIAN) && !defined(_LITTLE_ENDIAN)) || (defined(__BIG_ENDIAN__) && !defined(__LITTLE_ENDIAN__)) || \ + defined(__ARMEB__) || defined(__MIPSEB__) || defined(__s390__) || defined(__sparc__) +# define ntohll +# define htonll +# elif (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) || \ + (defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && __BYTE_ORDER == __LITTLE_ENDIAN) || \ + (defined(BYTE_ORDER) && defined(LITTLE_ENDIAN) && BYTE_ORDER == LITTLE_ENDIAN) || \ + defined(_LITTLE_ENDIAN) || defined(__LITTLE_ENDIAN__) || defined(__ARMEL__) || defined(__MIPSEL__) || \ + defined(__i386) || defined(__i386__) || defined(__x86_64) || defined(__x86_64__) || defined(__amd64) +# define ntohll(x) ((ntohl((uint32_t)(x)) * UINT64_C(0x100000000)) + (ntohl((x) >> 32))) +# define htonll ntohll +# else +# error "Unable to determine byte order!" +# endif +# endif +#endif + + +#ifdef __cplusplus +# define CONST_CAST(t, v) const_cast(v) +#else +/* C-style const_cast without triggering a warning with -Wcast-qual */ +# define CONST_CAST(t, v) (t)(uintptr_t)(v) +#endif + +#ifdef __GNUC__ +#ifndef likely +# define likely(x) __builtin_expect(!!(x), 1) +#endif +#ifndef unlikely +# define unlikely(x) __builtin_expect(!!(x), 0) +#endif +# define unreachable() __builtin_unreachable() +#elif defined(_MSC_VER) +# define likely(x) (x) +# define unlikely(x) (x) +# define unreachable() __assume(0) +#else +# define likely(x) (x) +# define unlikely(x) (x) +# define unreachable() do {} while (0) +#endif + +static inline bool add_check_overflow(size_t v1, size_t v2, size_t *r) +{ +#if ((defined(__GNUC__) && (__GNUC__ >= 5)) && !defined(__INTEL_COMPILER)) || __has_builtin(__builtin_add_overflow) + return __builtin_add_overflow(v1, v2, r); +#else + /* unsigned additions are well-defined */ + *r = v1 + v2; + return v1 > v1 + v2; +#endif +} + +#endif /* COMPILERSUPPORT_H */ + diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/tinycbor-version.h b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/tinycbor-version.h new file mode 100644 index 000000000..c26560cce --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/tinycbor-version.h @@ -0,0 +1,3 @@ +#define TINYCBOR_VERSION_MAJOR 0 +#define TINYCBOR_VERSION_MINOR 6 +#define TINYCBOR_VERSION_PATCH 0 diff --git a/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/utf8_p.h b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/utf8_p.h new file mode 100644 index 000000000..ca438350d --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/cpp/vendor/tinycbor/utf8_p.h @@ -0,0 +1,104 @@ +/**************************************************************************** +** +** Copyright (C) 2017 Intel Corporation +** +** Permission is hereby granted, free of charge, to any person obtaining a copy +** of this software and associated documentation files (the "Software"), to deal +** in the Software without restriction, including without limitation the rights +** to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +** copies of the Software, and to permit persons to whom the Software is +** furnished to do so, subject to the following conditions: +** +** The above copyright notice and this permission notice shall be included in +** all copies or substantial portions of the Software. +** +** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +** THE SOFTWARE. +** +****************************************************************************/ + +#ifndef CBOR_UTF8_H +#define CBOR_UTF8_H + +#include "compilersupport_p.h" + +#include + +static inline uint32_t get_utf8(const uint8_t **buffer, const uint8_t *end) +{ + int charsNeeded; + uint32_t uc, min_uc; + uint8_t b; + ptrdiff_t n = end - *buffer; + if (n == 0) + return ~0U; + + uc = *(*buffer)++; + if (uc < 0x80) { + /* single-byte UTF-8 */ + return uc; + } + + /* multi-byte UTF-8, decode it */ + if (unlikely(uc <= 0xC1)) + return ~0U; + if (uc < 0xE0) { + /* two-byte UTF-8 */ + charsNeeded = 2; + min_uc = 0x80; + uc &= 0x1f; + } else if (uc < 0xF0) { + /* three-byte UTF-8 */ + charsNeeded = 3; + min_uc = 0x800; + uc &= 0x0f; + } else if (uc < 0xF5) { + /* four-byte UTF-8 */ + charsNeeded = 4; + min_uc = 0x10000; + uc &= 0x07; + } else { + return ~0U; + } + + if (n < charsNeeded) + return ~0U; + + /* first continuation character */ + b = *(*buffer)++; + if ((b & 0xc0) != 0x80) + return ~0U; + uc <<= 6; + uc |= b & 0x3f; + + if (charsNeeded > 2) { + /* second continuation character */ + b = *(*buffer)++; + if ((b & 0xc0) != 0x80) + return ~0U; + uc <<= 6; + uc |= b & 0x3f; + + if (charsNeeded > 3) { + /* third continuation character */ + b = *(*buffer)++; + if ((b & 0xc0) != 0x80) + return ~0U; + uc <<= 6; + uc |= b & 0x3f; + } + } + + /* overlong sequence? surrogate pair? out or range? */ + if (uc < min_uc || uc - 0xd800U < 2048U || uc > 0x10ffff) + return ~0U; + + return uc; +} + +#endif /* CBOR_UTF8_H */ diff --git a/wasm-deps/ffi/ffi/codegen/templates/nim_ffi_lib.cmake b/wasm-deps/ffi/ffi/codegen/templates/nim_ffi_lib.cmake new file mode 100644 index 000000000..a9037c773 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/templates/nim_ffi_lib.cmake @@ -0,0 +1,87 @@ +# Shared CMake logic for nim-ffi generated bindings. Builds the Nim library as +# a shared object and the vendored TinyCBOR as a static library, and exposes +# them as the imported target `${NIM_FFI_LIB}` (+ `${NIM_FFI_LIB}_nim_lib`) and +# the `tinycbor` target. Included by the per-language generated CMakeLists, +# which set REPO_ROOT, NIM_FFI_LIB (library name) and NIM_FFI_SRC (path to the +# .nim root, relative to the including CMakeLists) before including this file. + +get_filename_component(NIM_SRC + "${CMAKE_CURRENT_SOURCE_DIR}/${NIM_FFI_SRC}" + ABSOLUTE) + +find_program(NIM_EXECUTABLE nim REQUIRED) + +if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(NIM_LIB_FILE "${REPO_ROOT}/lib${NIM_FFI_LIB}.dylib") +elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(NIM_LIB_FILE "${REPO_ROOT}/${NIM_FFI_LIB}.dll") + set(NIM_IMPLIB_FILE "${REPO_ROOT}/${NIM_FFI_LIB}.lib") +else() + set(NIM_LIB_FILE "${REPO_ROOT}/lib${NIM_FFI_LIB}.so") +endif() + +# On Windows the default Nim toolchain (mingw gcc) doesn't emit an import +# library unless told to; without it MSVC consumers can't resolve any exported +# symbol at link time. +set(NIM_IMPLIB_PASSL "") +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + set(NIM_IMPLIB_PASSL "--passL:-Wl,--out-implib,${NIM_IMPLIB_FILE}") +endif() + +add_custom_command( + OUTPUT "${NIM_LIB_FILE}" + COMMAND "${NIM_EXECUTABLE}" c + --mm:orc + -d:chronicles_log_level=WARN + --app:lib + --noMain + "--nimMainPrefix:lib${NIM_FFI_LIB}" + ${NIM_IMPLIB_PASSL} + "-o:${NIM_LIB_FILE}" + "${NIM_SRC}" + WORKING_DIRECTORY "${REPO_ROOT}" + DEPENDS "${NIM_SRC}" + BYPRODUCTS "${NIM_IMPLIB_FILE}" + COMMENT "Compiling Nim library lib${NIM_FFI_LIB}" + VERBATIM +) +add_custom_target(${NIM_FFI_LIB}_nim_lib ALL DEPENDS "${NIM_LIB_FILE}") + +# On Windows an IMPORTED SHARED target needs IMPORTED_IMPLIB, but the Visual +# Studio multi-config generator did not pick it up and emitted +# `${NIM_FFI_LIB}-NOTFOUND.obj`. Side-step the IMPORTED machinery there by +# exposing the import library through a plain INTERFACE library. +if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_library(${NIM_FFI_LIB} INTERFACE) + target_link_libraries(${NIM_FFI_LIB} INTERFACE "${NIM_IMPLIB_FILE}") +else() + add_library(${NIM_FFI_LIB} SHARED IMPORTED GLOBAL) + set_target_properties(${NIM_FFI_LIB} PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}") +endif() +add_dependencies(${NIM_FFI_LIB} ${NIM_FFI_LIB}_nim_lib) + +# Absolute path to the runtime library (DLL/dylib/so). Exposed via the cache so +# consumers in other directories can stage the DLL next to their executable on +# Windows. +set(${NIM_FFI_LIB}_RUNTIME_LIB "${NIM_LIB_FILE}" CACHE INTERNAL + "Absolute path to the ${NIM_FFI_LIB} runtime library") + +# ── TinyCBOR (vendored at ffi/codegen/templates/cpp/vendor/tinycbor) ───────── +# The C and C++ backends share one vendored TinyCBOR copy. Guarded so two +# sibling bindings dirs in one parent project don't redefine the target. +set(TINYCBOR_SRC_DIR "${REPO_ROOT}/ffi/codegen/templates/cpp/vendor") +if(NOT TARGET tinycbor) + add_library(tinycbor STATIC + "${TINYCBOR_SRC_DIR}/tinycbor/cborencoder.c" + "${TINYCBOR_SRC_DIR}/tinycbor/cborencoder_close_container_checked.c" + "${TINYCBOR_SRC_DIR}/tinycbor/cborparser.c" + "${TINYCBOR_SRC_DIR}/tinycbor/cborparser_dup_string.c" + "${TINYCBOR_SRC_DIR}/tinycbor/cborerrorstrings.c" + ) + target_include_directories(tinycbor PUBLIC + "${TINYCBOR_SRC_DIR}" # consumer uses #include + "${TINYCBOR_SRC_DIR}/tinycbor" # internal _p.h includes resolve here + ) + set_property(TARGET tinycbor PROPERTY C_STANDARD 99) + set_property(TARGET tinycbor PROPERTY POSITION_INDEPENDENT_CODE ON) +endif() diff --git a/wasm-deps/ffi/ffi/codegen/types_ir.nim b/wasm-deps/ffi/ffi/codegen/types_ir.nim new file mode 100644 index 000000000..feb3cb529 --- /dev/null +++ b/wasm-deps/ffi/ffi/codegen/types_ir.nim @@ -0,0 +1,129 @@ +## Structured type model shared by the C / C++ / Rust binding generators: +## `parseFFIType` parses a Nim type string, `renderNative` walks it per backend. + +import std/[strutils, options] + +type + ScalarKind* {.pure.} = enum + skBool + skI8 + skI16 + skI32 + skI64 + skU8 + skU16 + skU32 + skU64 + skF32 + skF64 + + FFITypeKind* {.pure.} = enum + ftScalar + ftStr + ftBytes + ftSeq + ftOpt + ftPtr + ftStruct + + FFIType* = ref object + case kind*: FFITypeKind + of ftScalar: + scalar*: ScalarKind + of ftSeq, ftOpt: + elem*: FFIType + of ftStruct: + name*: string + else: + discard + + NativeTypeMap* = object + ## Per-backend type names; `structName` nil ⇒ user type name passes through. + scalar*: proc(s: ScalarKind): string {.noSideEffect, nimcall.} + str*: string + bytes*: string + ptrType*: string + seqOf*: proc(elem: string): string {.noSideEffect, nimcall.} + optOf*: proc(elem: string): string {.noSideEffect, nimcall.} + structName*: proc(name: string): string {.noSideEffect, nimcall.} + +func genericInnerType*(typeName, prefix: string): string = + ## Inner type of `Prefix[Inner]`, e.g. ("seq[int]", "seq[") → "int"; "" if no match. + if typeName.startsWith(prefix) and typeName.endsWith("]"): + return typeName[prefix.len .. ^2] + return "" + +func scalarKind(t: string): Option[ScalarKind] = + case t + of "bool": + some(skBool) + of "int8": + some(skI8) + of "int16": + some(skI16) + of "int32": + some(skI32) + of "int", "int64": + some(skI64) + of "uint8", "byte": + some(skU8) + of "uint16": + some(skU16) + of "uint32": + some(skU32) + of "uint", "uint64": + some(skU64) + of "float32": + some(skF32) + of "float", "float64": + some(skF64) + else: + none(ScalarKind) + +func parseFFIType*(typeName: string): FFIType = + ## Nim type string → shared `FFIType`: ptr/pointer, seq[byte]→bytes, seq/Option/Maybe, + ## scalars, string, else struct. + let t = typeName.strip() + if t.startsWith("ptr ") or t == "pointer": + return FFIType(kind: ftPtr) + + let seqInner = genericInnerType(t, "seq[") + if seqInner.len > 0: + let inner = seqInner.strip() + if inner == "byte" or inner == "uint8": + return FFIType(kind: ftBytes) + return FFIType(kind: ftSeq, elem: parseFFIType(inner)) + + var optInner = genericInnerType(t, "Option[") + if optInner.len == 0: + optInner = genericInnerType(t, "Maybe[") + if optInner.len > 0: + return FFIType(kind: ftOpt, elem: parseFFIType(optInner.strip())) + + let sc = scalarKind(t) + if sc.isSome(): + return FFIType(kind: ftScalar, scalar: sc.get()) + if t == "string" or t == "cstring": + return FFIType(kind: ftStr) + FFIType(kind: ftStruct, name: t) + +func renderNative*(m: NativeTypeMap, t: FFIType): string = + ## Recursively walks `t` into a native type string for backend `m`. + case t.kind + of ftScalar: + m.scalar(t.scalar) + of ftStr: + m.str + of ftBytes: + m.bytes + of ftPtr: + m.ptrType + of ftSeq: + m.seqOf(renderNative(m, t.elem)) + of ftOpt: + m.optOf(renderNative(m, t.elem)) + of ftStruct: + if m.structName.isNil(): + t.name + else: + m.structName(t.name) diff --git a/wasm-deps/ffi/ffi/event_thread.nim b/wasm-deps/ffi/ffi/event_thread.nim new file mode 100644 index 000000000..af8376d8a --- /dev/null +++ b/wasm-deps/ffi/ffi/event_thread.nim @@ -0,0 +1,134 @@ +## Event-thread body and FFI-thread liveness monitoring. Included from +## `ffi_context.nim`. Drains queued events into listeners and emits +## NotResponding/Responding on FFI-heartbeat stall/recovery. + +type + NotRespondingEvent* = object + RespondingEvent* = object + +const + NotRespondingEventName* = "not_responding" + RespondingEventName* = "responding" + +proc dispatchToListeners[T]( + ctx: ptr FFIContext[T], eventName: string, data: pointer, dataLen: int +) = + ## Holds reg.lock across snapshot + invocation so concurrent add/remove blocks + ## until dispatch returns. + withLock ctx[].eventRegistry.lock: + let listeners = ctx[].eventRegistry.byEvent.getOrDefault(eventName) + if listeners.len == 0: + chronicles.debug "no listener registered", event = eventName + return + foreignThreadGc: + try: + notifyListeners(listeners, RET_OK, data, dataLen) + except Exception, CatchableError: + notifyListenersErr( + listeners, + "Exception dispatching " & eventName & ": " & getCurrentExceptionMsg(), + ) + +proc emitLivenessEvent[T, P](ctx: ptr FFIContext[T], name: string, payload: P) = + ## Dispatches directly to listeners, bypassing the (possibly wedged) queue. + let event = + try: + EventEnvelope[P](eventType: name, payload: payload).cborEncode() + except CatchableError as e: + chronicles.error "liveness event encode failed", name = name, err = e.msg + return + let dataPtr: pointer = + if event.len > 0: + cast[pointer](unsafeAddr event[0]) + else: + cast[pointer](emptyListenerPayload) + ctx.dispatchToListeners(name, dataPtr, event.len) + +proc onNotResponding*(ctx: ptr FFIContext) = + emitLivenessEvent(ctx, NotRespondingEventName, NotRespondingEvent()) + +proc onResponding*(ctx: ptr FFIContext) = + ## Fired once when the heartbeat resumes after a NotRespondingEvent. + emitLivenessEvent(ctx, RespondingEventName, RespondingEvent()) + +proc dispatchQueuedEvent[T](ctx: ptr FFIContext[T], qe: QueuedEvent) = + ## Reads the borrowed slab payload; `commitDequeue` frees any heap fallback. + ctx.dispatchToListeners($qe.name, qe.data, qe.dataLen) + +proc drainOneEvent[T](ctx: ptr FFIContext[T]): bool = + ## Peek → dispatch → commit; slot stays pinned across dispatch, `defer` commits + ## even if a listener raises. False when the queue is empty. + let opt = ctx.eventQueue.peekEvent() + if opt.isNone(): + return false + defer: + ctx.eventQueue.commitDequeue() + ctx.dispatchQueuedEvent(opt.get()) + true + +proc drainEventQueue[T](ctx: ptr FFIContext[T]) = + while ctx.drainOneEvent(): + discard + +type HeartbeatMonitor = object + startedAt: Moment + lastChange: Moment + lastValue: int64 + notifiedStale: bool + +proc init(T: type HeartbeatMonitor, ctx: ptr FFIContext): T = + let now = Moment.now() + T( + startedAt: now, + lastChange: now, + lastValue: ctx.ffiHeartbeat.load(), + notifiedStale: false, + ) + +proc check[T](hb: var HeartbeatMonitor, ctx: ptr FFIContext[T]) = + ## Fires onNotResponding/onResponding on stall/recovery; each latches once per episode. + if Moment.now() - hb.startedAt <= FFIHeartbeatStartDelay: + return + let cur = ctx.ffiHeartbeat.load() + if cur != hb.lastValue: + if hb.notifiedStale: + onResponding(ctx) + hb.lastValue = cur + hb.lastChange = Moment.now() + hb.notifiedStale = false + elif not hb.notifiedStale and Moment.now() - hb.lastChange > FFIHeartbeatStaleThreshold: + onNotResponding(ctx) + hb.notifiedStale = true + +proc eventRun[T](ctx: ptr FFIContext[T]) {.async.} = + var hb = HeartbeatMonitor.init(ctx) + var notifiedStuck = false # latched forever — eventQueueStuck is sticky terminal. + + # Keep draining after `running` flips false until the FFI thread exits, so events from an async {.ffiDtor.} teardown are still dispatched. + while ctx.running.load() or not ctx.ffiThreadExited.load(): + discard await ctx.eventQueueSignal.wait().withTimeout(EventThreadTickInterval) + + ctx.drainEventQueue() + + # Liveness only while running; skip during the teardown drain. + if ctx.running.load(): + # Fire after drain so reg.lock is free (FFI thread would deadlock here). + if not notifiedStuck and ctx.eventQueueStuck.load(): + onNotResponding(ctx) + notifiedStuck = true + hb.check(ctx) + + # Catch anything enqueued between the last drain and the FFI thread's exit. + ctx.drainEventQueue() + +proc eventThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} = + ## Drains the event queue and runs the FFI-thread heartbeat check. + defer: + let fireRes = ctx.eventThreadExitSignal.fireSync() + if fireRes.isErr(): + error "failed to fire eventThreadExitSignal", err = fireRes.error + + try: + waitFor eventRun(ctx) + except CatchableError as e: + error "event thread exited with exception", error = e.msg diff --git a/wasm-deps/ffi/ffi/ffi_config.nim b/wasm-deps/ffi/ffi/ffi_config.nim index 0b0012217..580387efb 100644 --- a/wasm-deps/ffi/ffi/ffi_config.nim +++ b/wasm-deps/ffi/ffi/ffi_config.nim @@ -1,11 +1,12 @@ ## Compile-time selection of the execution transport. ## -## Default (threaded): each FFIContext spawns an FFI worker thread + a watchdog -## thread and hands requests over a chronos ThreadSignalPtr + SPSC channel. -## Those rely on OS threads + eventfd-style signalling, absent in a baseline -## WebAssembly sandbox. +## Default (threaded): each FFIContext owns an FFI worker thread and an event +## thread, woken over chronos ThreadSignalPtr with requests carried on a queue +## bank. Those need OS threads and eventfd-style signalling, neither of which +## exists in a baseline WebAssembly sandbox. ## -## `singleThreaded` collapses the worker onto the calling thread: a request runs -## inline to completion. Auto-selected for Emscripten/WASM; forceable anywhere -## with `-d:ffiSingleThreaded`. +## `singleThreaded` collapses the workers onto the calling thread: a request is +## spawned on the caller's chronos loop and driven by the host through +## `ffi_poll()`. Auto-selected for Emscripten/WASM; forceable anywhere with +## `-d:ffiSingleThreaded`. const singleThreaded* = defined(ffiSingleThreaded) or defined(emscripten) diff --git a/wasm-deps/ffi/ffi/ffi_context.nim b/wasm-deps/ffi/ffi/ffi_context.nim index e3d276c94..bd5ff31e6 100644 --- a/wasm-deps/ffi/ffi/ffi_context.nim +++ b/wasm-deps/ffi/ffi/ffi_context.nim @@ -1,302 +1,273 @@ -{.pragma: exported, exportc, cdecl, raises: [].} -{.pragma: callback, cdecl, raises: [], gcsafe.} +## FFIContext type plus lifecycle (init / signal-stop / join / destroy). + {.passc: "-fPIC".} -import std/[options, atomics, os, net, locks, json, tables] +import std/[atomics, locks, options, sequtils, tables] import chronicles, chronos, results import ./ffi_config -when not singleThreaded: - # ThreadSignalPtr requires threads enabled; the SPSC channel only carries - # requests across the worker-thread boundary. Neither exists inline. - import chronos/threadsync, taskpools/channels_spsc_single -import ./ffi_types, ./ffi_thread_request, ./internal/ffi_macro, ./logging +when singleThreaded: + # chronos/threadsync is a {.fatal.} under --threads:off, and so is + # system.Thread. ffi_singlethread supplies API-compatible no-ops so the + # lifecycle code below compiles unchanged. See ffi_config.nim. + import ./ffi_singlethread +else: + import chronos/threadsync +import + ./ffi_types, + ./ffi_events, + ./ffi_handles, + ./ffi_thread_request, + ./ffi_request_queue, + ./logging, + ./cbor_serial + +export ffi_events, ffi_handles + +type CtxLifecycle* {.pure.} = enum + ## State machine guarding a pooled FFI context (Atomic on FFIContext). + ## Active -> RecyclePending when the ffiDtor requests recycle + ## RecyclePending -> Recycling FFI loop claimed it, draining handlers + ## Recycling -> Active createFFIContext reuses the slot + Active + RecyclePending + Recycling type FFIContext*[T] = object - myLib*: ptr T - # main library object (e.g., Waku, LibP2P, SDS, the one to be exposed as a library) - when not singleThreaded: - ffiThread: Thread[(ptr FFIContext[T])] - # represents the main FFI thread in charge of attending API consumer actions - watchdogThread: Thread[(ptr FFIContext[T])] - # monitors the FFI thread and notifies the FFI API consumer if it hangs - reqChannel: ChannelSPSCSingle[ptr FFIThreadRequest] - reqSignal: ThreadSignalPtr # to notify the FFI Thread that a new request is sent - reqReceivedSignal: ThreadSignalPtr - # to signal main thread, interfacing with the FFI thread, that FFI thread received the request - else: - myLibStorage: T - # Threaded mode roots the library object on the FFI worker thread's stack - # (`ffiReqHandler`). With no worker thread we keep that backing store in - # the context instead, GC-rooted via the holder in createFFIContext. - lock: Lock + myLib*: ptr T # main library object (Waku, LibP2P, SDS, …) + myLibRefd*: bool + # refc only: true once myLib[] (a ref) has been GC_ref'd to root it against + # the cycle collector. Balanced by GC_unref in freeLib. + myLibOwned*: bool + # true once a ctor stored a createShared'd lib into myLib (vs the worker's + # stack fallback). freeLib only frees/destroys owned libs. + inUse*: Atomic[bool] + # Whether this pooled context is claimed. The recycle handler clears it on + # the FFI thread so the slot returns to the pool without recreating threads. + lifecycle*: Atomic[CtxLifecycle] + recycleDoneSignal: ThreadSignalPtr + # fired by the recycle handler once the lib is freed, just before it releases + # the slot; the synchronous recycleFFIContext caller waits on it. + libReady*: Atomic[bool] + # False until a {.ffiCtor.} stores the library. Before that, `myLib` points + # at the default fallback of the FFI thread. For a `ref` type that fallback + # is nil. + ffiThread: Thread[(ptr FFIContext[T])] + eventThread: Thread[(ptr FFIContext[T])] + reqQueueBank: RequestQueueBank + reqSignal: ThreadSignalPtr + stopSignal: ThreadSignalPtr + threadExitSignal: ThreadSignalPtr + eventQueueSignal: ThreadSignalPtr + eventThreadExitSignal: ThreadSignalPtr userData*: pointer - eventCallback*: pointer - eventUserdata*: pointer - running: Atomic[bool] # To control when the threads are running + eventRegistry*: FFIEventRegistry + handles*: FFIHandleRegistry + eventQueue*: EventQueue + ffiHeartbeat*: Atomic[int64] + eventQueueStuck*: Atomic[bool] + ffiThreadExited*: Atomic[bool] + # set once FFI thread (incl. async {.ffiDtor.}) is done; event thread drains until then + running: Atomic[bool] registeredRequests: ptr Table[cstring, FFIRequestProc] - # Pointer to with the registered requests at compile time + staleWarnInterval*: Duration + +var onFFIThread* {.threadvar.}: bool const git_version* {.strdefine.} = "n/a" -template callEventCallback*(ctx: ptr FFIContext, eventName: string, body: untyped) = - if isNil(ctx[].eventCallback): - chronicles.error eventName & " - eventCallback is nil" - return +const RecycleTimeoutMs* {.intdefine: "ffiRecycleTimeoutMs".} = 1500 + ## Bounds one drain round of the recycle handler. The handler runs at most two + ## rounds: it waits for the in-flight handlers, then cancels them and waits + ## again. Override with `-d:ffiRecycleTimeoutMs=`. +const RecycleTimeout* = RecycleTimeoutMs.milliseconds - foreignThreadGc: - try: - let event = body - cast[FFICallBack](ctx[].eventCallback)( - RET_OK, unsafeAddr event[0], cast[csize_t](len(event)), ctx[].eventUserData - ) - except Exception, CatchableError: - let msg = - "Exception " & eventName & " when calling 'eventCallBack': " & - getCurrentExceptionMsg() - cast[FFICallBack](ctx[].eventCallback)( - RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), ctx[].eventUserData - ) +const + RecycleWaitTimeout* = 2 * RecycleTimeout + 2.seconds + ## Caller-side bound for synchronous recycle. It covers both drain rounds + ## plus slack, so it only fires when the worker itself is wedged. + EventThreadTickInterval* = 1.seconds + FFIHeartbeatStartDelay* = 10.seconds + FFIHeartbeatStaleThreshold* = 1.seconds -when not singleThreaded: - proc sendRequestToFFIThread*( - ctx: ptr FFIContext, ffiRequest: ptr FFIThreadRequest, timeout = InfiniteDuration - ): Result[void, string] = - ctx.lock.acquire() - # This lock is only necessary while we use a SP Channel and while the signalling - # between threads assumes that there aren't concurrent requests. - # Rearchitecting the signaling + migrating to a MP Channel will allow us to receive - # requests concurrently and spare us the need of locks - defer: - ctx.lock.release() +const StaleWarnIntervalMs* {.intdefine: "ffiStaleWarnIntervalMs".} = 5000 + ## `RET_STALE_WARN` cadence; handlers are never timed out. +const StaleWarnInterval* = StaleWarnIntervalMs.milliseconds - ## Sending the request - let sentOk = ctx.reqChannel.trySend(ffiRequest) - if not sentOk: - return err("Couldn't send a request to the ffi thread") +type FFITeardownProc*[T] = proc(lib: ptr T): Future[void] {.async.} - let fireSyncRes = ctx.reqSignal.fireSync() - if fireSyncRes.isErr(): - return err("failed fireSync: " & $fireSyncRes.error) +proc ffiTeardownHook*[T](): var FFITeardownProc[T] = + ## Per-library teardown slot (one `{.global.}` per `T`), awaited by the FFI thread before exit. + ## Runtime slot not an overload: an overload would bind the no-op default before the dtor is visible. + var hook {.global.}: FFITeardownProc[T] + hook - if fireSyncRes.get() == false: - return err("Couldn't fireSync in time") +include ./event_thread +include ./ffi_thread - ## wait until the FFI working thread properly received the request - let res = ctx.reqReceivedSignal.waitSync(timeout) - if res.isErr(): - return err("Couldn't receive reqReceivedSignal signal") +template closeAndNil(field: untyped) = + if not field.isNil(): + ?field.close() + field = nil - ## Notice that in case of "ok", the deallocShared(req) is performed by the FFI Thread in the - ## process proc. - return ok() +proc deinitContextResources*[T](ctx: ptr FFIContext[T]): Result[void, string] = + ## Mirror of `initContextResources`. Threads MUST be joined first; fields nil'd after close. + deinitRequestQueue(ctx[].reqQueueBank) + deinitEventRegistry(ctx[].eventRegistry) + deinitHandleRegistry(ctx[].handles) + deinitEventQueue(ctx[].eventQueue) + when defined(gcRefc): + # ThreadSignalPtr.close() under refc hangs via signal-handler re-entry; the + # recycle pool makes full destroy rare, so the leaked fd stays bounded. + discard + else: + closeAndNil(ctx.reqSignal) + closeAndNil(ctx.stopSignal) + closeAndNil(ctx.threadExitSignal) + closeAndNil(ctx.eventQueueSignal) + closeAndNil(ctx.eventThreadExitSignal) + closeAndNil(ctx.recycleDoneSignal) + ok() -type Foo = object -registerReqFFI(WatchdogReq, foo: ptr Foo): - proc(): Future[Result[string, string]] {.async.} = - return ok("FFI thread is not blocked") +template newSignalOrErr(field: untyped, name: string) = + field = ThreadSignalPtr.new().valueOr: + return err("couldn't create ThreadSignalPtr: " & name & ": " & $error) -type JsonNotRespondingEvent = object - eventType: string +proc initContextResources*[T](ctx: ptr FFIContext[T]): Result[void, string] = + ## On failure, deferred cleanup closes partial state; caller releases the slot. + # Nil first so deferred cleanup can't double-close a reused pool slot. + ctx.reqSignal = nil + ctx.stopSignal = nil + ctx.threadExitSignal = nil + ctx.eventQueueSignal = nil + ctx.eventThreadExitSignal = nil + ctx.recycleDoneSignal = nil + ctx.myLibOwned = false + ctx.myLibRefd = false + ctx.lifecycle.store(CtxLifecycle.Active) + initRequestQueue(ctx[].reqQueueBank) + initEventRegistry(ctx[].eventRegistry) + initHandleRegistry(ctx[].handles) + initEventQueue(ctx[].eventQueue) + ctx.ffiHeartbeat.store(0) + ctx.libReady.store(false) + ctx.eventQueueStuck.store(false) + ctx.ffiThreadExited.store(false) + ctx.staleWarnInterval = StaleWarnInterval -proc init(T: type JsonNotRespondingEvent): T = - return JsonNotRespondingEvent(eventType: "not_responding") + var success = false + defer: + if not success: + # `ctx` is a pool slot the caller owns; close what was opened, never free it. + ctx.deinitContextResources().isOkOr: + error "failed to clean up resources after createFFIContext failure", + error = error -proc `$`(event: JsonNotRespondingEvent): string = - $(%*event) + newSignalOrErr(ctx.reqSignal, "reqSignal") + newSignalOrErr(ctx.stopSignal, "stopSignal") + newSignalOrErr(ctx.threadExitSignal, "threadExitSignal") + newSignalOrErr(ctx.eventQueueSignal, "eventQueueSignal") + newSignalOrErr(ctx.eventThreadExitSignal, "eventThreadExitSignal") + newSignalOrErr(ctx.recycleDoneSignal, "recycleDoneSignal") -proc onNotResponding*(ctx: ptr FFIContext) = - callEventCallback(ctx, "onNotResponding"): - $JsonNotRespondingEvent.init() + ctx.registeredRequests = addr ffi_types.registeredRequests -when not singleThreaded: - proc watchdogThreadBody(ctx: ptr FFIContext) {.thread.} = - ## Watchdog thread that monitors the FFI thread and notifies the library user if it hangs. - ## This thread never blocks. + ctx.running.store(true) - let watchdogRun = proc(ctx: ptr FFIContext) {.async.} = - const WatchdogStartDelay = 10.seconds - const WatchdogTimeinterval = 1.seconds - const WatchdogTimeout = 20.seconds + try: + createThread(ctx.ffiThread, ffiThreadBody[T], ctx) + except ValueError, ResourceExhaustedError: + return err("failed to create the FFI thread: " & getCurrentExceptionMsg()) - # Give time for the node to be created and up before sending watchdog requests - await sleepAsync(WatchdogStartDelay) - while true: - await sleepAsync(WatchdogTimeinterval) - - if ctx.running.load == false: - debug "Watchdog thread exiting because FFIContext is not running" - break - - let callback = proc( - callerRet: cint, msg: ptr cchar, len: csize_t, userData: pointer - ) {.cdecl, gcsafe, raises: [].} = - discard ## Don't do anything. Just respecting the callback signature. - const nilUserData = nil - - trace "Sending watchdog request to FFI thread" - - sendRequestToFFIThread(ctx, WatchdogReq.ffiNewReq(callback, nilUserData), WatchdogTimeout).isOkOr: - error "Failed to send watchdog request to FFI thread", error = $error - onNotResponding(ctx) - - waitFor watchdogRun(ctx) - -proc processRequest[T]( - request: ptr FFIThreadRequest, ctx: ptr FFIContext[T] -) {.async.} = - ## Invoked within the FFI thread to process a request coming from the FFI API consumer thread. - - let reqId = $request[].reqId - ## The reqId determines which proc will handle the request. - ## The registeredRequests represents a table defined at compile time. - ## Then, registeredRequests == Table[reqId, proc-handling-the-request-asynchronously] - - let retFut = - if not ctx[].registeredRequests[].contains(reqId): - ## That shouldn't happen because only registered requests should be sent to the FFI thread. - nilProcess(request[].reqId) - else: - ctx[].registeredRequests[][reqId](request[].reqContent, ctx) - handleRes(await retFut, request) - -when not singleThreaded: - proc ffiThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} = - ## FFI thread body that attends library user API requests - - logging.setupLog(logging.LogLevel.DEBUG, logging.LogFormat.TEXT) - - let ffiRun = proc(ctx: ptr FFIContext[T]) {.async.} = - var ffiReqHandler: T - ## Holds the main library object, i.e., in charge of handling the ffi requests. - ## e.g., Waku, LibP2P, SDS, etc. - - while true: - await ctx.reqSignal.wait() - - if ctx.running.load == false: - break - - ## Wait for a request from the ffi consumer thread - var request: ptr FFIThreadRequest - let recvOk = ctx.reqChannel.tryRecv(request) - if not recvOk: - chronicles.error "ffi thread could not receive a request" - continue - - ctx.myLib = addr ffiReqHandler - - ## Handle the request - asyncSpawn processRequest(request, ctx) - - let fireRes = ctx.reqReceivedSignal.fireSync() - if fireRes.isErr(): - error "could not fireSync back to requester thread", error = fireRes.error - - waitFor ffiRun(ctx) - -when singleThreaded: - type SingleThreadedHolder[T] = ref object of RootObj - ## GC-traced cell so the library object stored in `ctx.myLibStorage` (a `ref` - ## for e.g. Waku) stays scanned. Kept alive in `gSingleThreadedRoots`. - ## `of RootObj` so holders can be stored uniformly as `RootRef`. - ctx: FFIContext[T] - - var gSingleThreadedRoots {.threadvar.}: seq[RootRef] - - proc sendRequestToFFIThread*( - ctx: ptr FFIContext, ffiRequest: ptr FFIThreadRequest, timeout = InfiniteDuration - ): Result[void, string] = - ## Single-threaded transport. `processRequest` fires the callback and frees - ## the request via `handleRes`. - when defined(emscripten): - # Browser: handlers await the network (WebSocket). Blocking with `waitFor` - # would starve the JS event loop and deadlock. Fire-and-forget instead; the - # host drives chronos via `ffi_poll()` and the callback fires on completion. - asyncSpawn processRequest(ffiRequest, ctx) - poll() # kick the handler up to its first await - else: - try: - waitFor processRequest(ffiRequest, ctx) - except CatchableError as e: - return err("processRequest failed: " & e.msg) - return ok() - - proc ffiPoll*() {.exportc: "ffi_poll", cdecl.} = - ## Advance chronos one step. The browser host calls this from its event loop - ## (setTimeout / requestAnimationFrame) so async handlers progress without - ## blocking the JS thread; callbacks fire as work completes. - poll() - - proc createFFIContext*[T](): Result[ptr FFIContext[T], string] = - ## No worker/watchdog threads. The context lives inside a GC-rooted holder so - ## `myLibStorage` (the library `ref`) is scanned; `myLib` points at it. - let holder = SingleThreadedHolder[T]() - gSingleThreadedRoots.add(holder) - let ctx = addr holder.ctx - ctx.lock.initLock() - ctx.registeredRequests = addr ffi_types.registeredRequests - ctx.running.store(true) - ctx.myLib = addr ctx.myLibStorage - return ok(ctx) - - proc destroyFFIContext*[T](ctx: ptr FFIContext[T]): Result[void, string] = + try: + createThread(ctx.eventThread, eventThreadBody[T], ctx) + except ValueError, ResourceExhaustedError: + # Join ffiThread before deferred cleanup closes signals it's waiting on. ctx.running.store(false) - ctx.lock.deinitLock() - # Drop the GC root so the holder (and its library object) can be collected. - for i in 0 ..< gSingleThreadedRoots.len: - let h = cast[SingleThreadedHolder[T]](gSingleThreadedRoots[i]) - if cast[pointer](addr h.ctx) == cast[pointer](ctx): - gSingleThreadedRoots.del(i) - break - return ok() -else: - proc createFFIContext*[T](): Result[ptr FFIContext[T], string] = - ## This proc is called from the main thread and it creates - ## the FFI working thread. - var ctx = createShared(FFIContext[T], 1) - ctx.reqSignal = ThreadSignalPtr.new().valueOr: - return err("couldn't create reqSignal ThreadSignalPtr") - ctx.reqReceivedSignal = ThreadSignalPtr.new().valueOr: - return err("couldn't create reqReceivedSignal ThreadSignalPtr") - ctx.lock.initLock() - ctx.registeredRequests = addr ffi_types.registeredRequests - - ctx.running.store(true) - - try: - createThread(ctx.ffiThread, ffiThreadBody[T], ctx) - except ValueError, ResourceExhaustedError: - freeShared(ctx) - return err("failed to create the FFI thread: " & getCurrentExceptionMsg()) - - try: - createThread(ctx.watchdogThread, watchdogThreadBody, ctx) - except ValueError, ResourceExhaustedError: - freeShared(ctx) - return err("failed to create the watchdog thread: " & getCurrentExceptionMsg()) - - return ok(ctx) - - proc destroyFFIContext*[T](ctx: ptr FFIContext[T]): Result[void, string] = - ctx.running.store(false) - - let signaledOnTime = ctx.reqSignal.fireSync().valueOr: - return err("error in destroyFFIContext: " & $error) - if not signaledOnTime: - return err("failed to signal reqSignal on time in destroyFFIContext") - + let fireRes = ctx.reqSignal.fireSync() + if fireRes.isErr(): + error "failed to signal ffiThread during event-thread cleanup", + error = fireRes.error joinThread(ctx.ffiThread) - joinThread(ctx.watchdogThread) - ctx.lock.deinitLock() - ?ctx.reqSignal.close() - ?ctx.reqReceivedSignal.close() - freeShared(ctx) + return err("failed to create the event thread: " & getCurrentExceptionMsg()) - return ok() + success = true + ok() -template checkParams*(ctx: ptr FFIContext, callback: FFICallBack, userData: pointer) = - if not isNil(ctx): - ctx[].userData = userData +proc fireOrErr(sig: ThreadSignalPtr, name: string): Result[void, string] = + let fired = sig.fireSync().valueOr: + return err("error signaling: " & name & ": " & $error) + if not fired: + return err("failed to signal: " & name & " on time") + ok() - if isNil(callback): - return RET_MISSING_CALLBACK +proc waitExitOrErr( + sig: ThreadSignalPtr, name: string, timeout: Duration +): Result[void, string] = + let exited = sig.waitSync(timeout).valueOr: + return err("error waiting for exit: " & name & ": " & $error) + if not exited: + return err("did not exit in time: " & name & " (leaking ctx to avoid hang)") + ok() + +proc signalStop*[T](ctx: ptr FFIContext[T]): Result[void, string] = + # Skip onNotResponding on error: it takes reg.lock a stuck listener may hold (deadlock risk). + ctx.running.store(false) + ?ctx.reqSignal.fireOrErr("reqSignal") + ?ctx.stopSignal.fireOrErr("stopSignal") + ctx.eventQueueSignal.fireOrErr("eventQueueSignal").isOkOr: + error "failed to signal eventQueueSignal in signalStop", error = error + ok() + +proc tryClaim*[T](ctx: ptr FFIContext[T]): bool = + ## Atomically claim a free pooled context (false -> true). + var expected = false + ctx.inUse.compareExchange(expected, true) + +proc releaseClaim*[T](ctx: ptr FFIContext[T]) = + ctx.inUse.store(false) + +proc isInUse*[T](ctx: ptr FFIContext[T]): bool = + ctx.inUse.load() + +proc markAsActive*[T](ctx: ptr FFIContext[T]) = + ## Reused context: its worker threads are still alive; re-arm for requests. + ctx.lifecycle.store(CtxLifecycle.Active) + +proc requestRecycle*[T](ctx: ptr FFIContext[T]): Result[void, string] = + ## Ask the FFI thread to drain, free the lib and release the slot, WITHOUT + ## stopping its worker/event threads, so the next createFFIContext reuses them. + ## Synchronous: waits on recycleDoneSignal. No fd churn -> no select() limit. + var expected = CtxLifecycle.Active + if not ctx.lifecycle.compareExchange(expected, CtxLifecycle.RecyclePending): + return err("requestRecycle: context is not Active (already recycling)") + + # A recycle that timed out can fire late. The CAS makes this the only recycle + # in flight, so drop that stale fire before the wait below can answer to it. + discard ctx.recycleDoneSignal.waitSync(ZeroDuration) + + let fired = ctx.reqSignal.fireSync().valueOr: + return err("requestRecycle: failed to signal the FFI thread: " & $error) + if not fired: + return err("requestRecycle: failed to signal the FFI thread in time") + + let done = ctx.recycleDoneSignal.waitSync(RecycleWaitTimeout).valueOr: + return err("requestRecycle: failed waiting for recycle: " & $error) + if not done: + return err("requestRecycle: recycle did not complete in time") + ok() + +## Per-thread exit wait before stopAndJoinThreads leaks ctx rather than hanging; async +## `{.ffiDtor.}` teardown can outlast the default. Override `-d:ffiThreadExitTimeoutMs=`. +const ThreadExitTimeoutMs* {.intdefine: "ffiThreadExitTimeoutMs".} = 1500 +const ThreadExitTimeout* = ThreadExitTimeoutMs.milliseconds + +proc stopAndJoinThreads*[T](ctx: ptr FFIContext[T]): Result[void, string] = + ## On timeout, returns err and skips remaining joins (leaves threads live); caller cleans up. + ctx.signalStop().isOkOr: + return err("signalStop failed: " & $error) + + ?ctx.threadExitSignal.waitExitOrErr("FFI thread", ThreadExitTimeout) + joinThread(ctx.ffiThread) + ?ctx.eventThreadExitSignal.waitExitOrErr("event thread", ThreadExitTimeout) + joinThread(ctx.eventThread) + ok() diff --git a/wasm-deps/ffi/ffi/ffi_context_pool.nim b/wasm-deps/ffi/ffi/ffi_context_pool.nim new file mode 100644 index 000000000..8bb6cc9f7 --- /dev/null +++ b/wasm-deps/ffi/ffi/ffi_context_pool.nim @@ -0,0 +1,142 @@ +import std/[atomics, sysatomics] +import results +import ./ffi_context + +const MaxFFIContexts* = 32 + +type + StaticCtxState = enum + ## Lifecycle of the pool's `{.ffiStatic.}` context; see `staticFFIContext`. + StaticCtxNone + StaticCtxCreating + StaticCtxDestroying + StaticCtxReady + + FFIContextPool*[T] = object + ## Fixed pool of FFI contexts, plus the one `{.ffiStatic.}` context. Each + ## slot's worker + event threads and signal fds are built once (on first + ## use) and reused across create/recycle cycles — recycle keeps them alive, + ## so repeated create/destroy does not churn fds. Bounds ThreadSignalPtr fds + ## at MaxFFIContexts * (signals per ctx). + contexts: array[MaxFFIContexts, FFIContext[T]] + initialized: array[MaxFFIContexts, Atomic[bool]] + staticCtx: Atomic[pointer] + staticState: Atomic[StaticCtxState] + +proc releaseSlot[T](pool: var FFIContextPool[T], ctx: ptr FFIContext[T]) = + ## Full-teardown release: the slot must be rebuilt before it serves again. + for i in 0 ..< MaxFFIContexts: + if pool.contexts[i].addr == ctx: + pool.initialized[i].store(false) + break + ctx.releaseClaim() + +proc createFFIContext*[T]( + pool: var FFIContextPool[T] +): Result[ptr FFIContext[T], string] = + ## Acquires a context from the fixed pool. A slot's worker is built once on + ## first use and reused (markAsActive) on every later acquisition. + for i in 0 ..< MaxFFIContexts: + let ctx = pool.contexts[i].addr + if not ctx.tryClaim(): + continue + if pool.initialized[i].load(): + # Reused slot: a prior recycle drained and released it; worker still alive. + ctx.markAsActive() + return ok(ctx) + initContextResources(ctx).isOkOr: + ctx.releaseClaim() + return err("createFFIContext: initContextResources failed: " & $error) + pool.initialized[i].store(true) + return ok(ctx) + err("FFI context pool exhausted (max " & $MaxFFIContexts & " contexts)") + +proc isStaticCtx[T](pool: var FFIContextPool[T], ctx: ptr FFIContext[T]): bool = + ## True while `ctx` is the pool's static context, including mid-teardown. + # `staticCtx` is cleared only once the slot is released, so matching on the + # pointer covers `Destroying` too. + pool.staticCtx.load() == cast[pointer](ctx) + +proc recycleFFIContext*[T]( + pool: var FFIContextPool[T], ctx: ptr FFIContext[T] +): Result[void, string] = + ## Normal teardown: drains in-flight handlers, frees the lib and returns the + ## slot to the pool WITHOUT stopping its threads, so a later createFFIContext + ## reuses them. Synchronous (waits for the FFI thread to finish draining). + # Recycling it would release the slot while `staticState` still points at it. + if pool.isStaticCtx(ctx): + return err("recycleFFIContext(pool): the {.ffiStatic.} context outlives every ctx") + ctx.requestRecycle() + +proc destroyFFIContext*[T]( + pool: var FFIContextPool[T], ctx: ptr FFIContext[T] +): Result[void, string] = + ## Full teardown: stops/joins the threads and frees resources, marking the slot + ## uninitialised so a later createFFIContext rebuilds it; normal cleanup uses + ## recycleFFIContext. On thread-exit timeout the slot is leaked; closing + ## live-thread resources is unsafe. + # Destroying it would release the slot while `staticState` still points at it. + if pool.isStaticCtx(ctx): + return err("destroyFFIContext(pool): the {.ffiStatic.} context outlives every ctx") + ctx.stopAndJoinThreads().isOkOr: + return err("destroyFFIContext(pool): " & $error) + let deinitRes = ctx.deinitContextResources() + pool.releaseSlot(ctx) + deinitRes.isOkOr: + return err("destroyFFIContext(pool): " & $error) + ok() + +proc staticFFIContext*[T]( + pool: var FFIContextPool[T] +): Result[ptr FFIContext[T], string] = + ## The pool's `{.ffiStatic.}` context, created on first use: a static proc has + ## no ctx of its own, but its handler still needs an FFI thread. + # Holds its slot until `destroyStaticFFIContext`, so `pool` must outlive its + # threads: only call this on the global `declareLibrary` emits. `myLib` stays + # the zero value. A failed create resets to `StaticCtxNone` so waiters retry. + while true: + case pool.staticState.load() + of StaticCtxReady: + return ok(cast[ptr FFIContext[T]](pool.staticCtx.load())) + of StaticCtxCreating, StaticCtxDestroying: + cpuRelax() + of StaticCtxNone: + var expected = StaticCtxNone + if not pool.staticState.compareExchange(expected, StaticCtxCreating): + continue + let ctx = pool.createFFIContext().valueOr: + pool.staticState.store(StaticCtxNone) + return err("staticFFIContext: " & error) + pool.staticCtx.store(cast[pointer](ctx)) + pool.staticState.store(StaticCtxReady) + return ok(ctx) + +proc destroyStaticFFIContext*[T](pool: var FFIContextPool[T]): Result[void, string] = + ## Teardown counterpart to `staticFFIContext`: stops the static context's + ## threads and frees its slot. A no-op when there is no static context. + # Claiming `Ready -> Destroying` serialises concurrent teardowns; it does not + # make teardown safe against a static call already in flight. + var expected = StaticCtxReady + if not pool.staticState.compareExchange(expected, StaticCtxDestroying): + return ok() + let ctx = cast[ptr FFIContext[T]](pool.staticCtx.load()) + ctx.stopAndJoinThreads().isOkOr: + # Threads are still live: leak the slot rather than free resources under them. + pool.staticState.store(StaticCtxReady) + return err("destroyStaticFFIContext: " & $error) + let deinitRes = ctx.deinitContextResources() + pool.releaseSlot(ctx) + pool.staticCtx.store(nil) + pool.staticState.store(StaticCtxNone) + deinitRes.isOkOr: + return err("destroyStaticFFIContext: " & $error) + ok() + +proc isValidCtx*[T](pool: var FFIContextPool[T], ctx: pointer): bool = + ## Rejects nil / dangling pointers at the API boundary. + if ctx.isNil(): + return false + for i in 0 ..< MaxFFIContexts: + if cast[pointer](pool.contexts[i].addr) == ctx: + return pool.contexts[i].addr.isInUse() + false diff --git a/wasm-deps/ffi/ffi/ffi_events.nim b/wasm-deps/ffi/ffi/ffi_events.nim new file mode 100644 index 000000000..f5c722e74 --- /dev/null +++ b/wasm-deps/ffi/ffi/ffi_events.nim @@ -0,0 +1,330 @@ +## Per-context event registry + bounded SPSC queue. FFI thread enqueues, event +## thread drains; payloads use c_malloc so they survive cross-thread heap reuse. + +{.pragma: callback, cdecl, raises: [], gcsafe.} + +import system/ansi_c +import std/[atomics, locks, sequtils, options, tables] +import chronicles +import ./ffi_types, ./cbor_serial, ./alloc + +type EventEnvelope*[T] = object ## CBOR wire shape: { eventType: tstr, payload: }. + eventType*: string + payload*: T + +type + FFIEventListener* = object + id*: uint64 + callback*: FFICallBack + userData*: pointer + + FFIEventRegistry* = object + lock*: Lock + nextId*: uint64 # 0 is reserved as "invalid"; ids start at 1. + byEvent*: Table[string, seq[FFIEventListener]] + +proc initEventRegistry*(reg: var FFIEventRegistry) = + ## Run once on the owning thread before sharing (re-initLock is UB). + reg.lock.initLock() + reg.nextId = 0'u64 + reg.byEvent = initTable[string, seq[FFIEventListener]]() + +proc deinitEventRegistry*(reg: var FFIEventRegistry) = + ## Mirror of `initEventRegistry`; resets GC fields so slot reuse sees no dtor. + reg.lock.deinitLock() + reg.byEvent = default(Table[string, seq[FFIEventListener]]) + reg.nextId = 0'u64 + +proc clearListeners*(reg: var FFIEventRegistry) {.raises: [].} = + ## Removes all listeners. The pool calls this when it recycles a context. The + ## lock stays in place, because the event thread uses it across recycles. + withLock reg.lock: + reg.byEvent.clear() + reg.nextId = 0'u64 + +proc addEventListener*( + reg: var FFIEventRegistry, + eventName: string, + callback: FFICallBack, + userData: pointer, +): uint64 {.raises: [].} = + ## Returns the listener id (>0), or 0 if `callback` is nil. + if callback.isNil(): + return 0 + + var assigned: uint64 = 0 + + withLock reg.lock: + reg.nextId.inc() + assigned = reg.nextId + let listener = + FFIEventListener(id: assigned, callback: callback, userData: userData) + reg.byEvent.mgetOrPut(eventName, @[]).add(listener) + assigned + +proc removeEventListener*(reg: var FFIEventRegistry, id: uint64): bool {.raises: [].} = + ## Safe from inside a dispatch; the in-flight snapshot still delivers once. + if id == 0'u64: + return false + + var removed = false + + withLock reg.lock: + var + pruneKey = "" + prune = false + for key, listeners in reg.byEvent.mpairs: + let before = listeners.len + listeners.keepItIf(it.id != id) + if listeners.len < before: + removed = true + if listeners.len == 0: + pruneKey = key + prune = true + break + if prune: + reg.byEvent.del(pruneKey) + removed + +proc removeAllEventListeners*(reg: var FFIEventRegistry) {.raises: [].} = + ## Does not reset the id counter. + withLock reg.lock: + reg.byEvent.clear() + +proc snapshotListeners*( + reg: var FFIEventRegistry, eventName: string +): seq[FFIEventListener] {.raises: [].} = + ## Lock held only across the copy so re-entrant add/remove can't deadlock. + var listeners: seq[FFIEventListener] = @[] + withLock reg.lock: + for l in reg.byEvent.getOrDefault(eventName): + listeners.add(l) + listeners + +const EventQueueCapacity* {.intdefine.} = 1024 + ## Sustained backlog here means a listener is wedged. Override `-d:EventQueueCapacity=N`. + +const MaxEventPayloadBytes* {.intdefine.} = 512 + ## Per-slot payload slab; larger payloads take a one-off c_malloc freed on + ## commit. Override `-d:MaxEventPayloadBytes=N`. + +const MaxEventNameBytes* {.intdefine.} = 64 + ## Per-slot name slab (incl. NUL); longer names take the heap fallback. + ## Override `-d:MaxEventNameBytes=N`. + +const emptyListenerPayload*: cstring = "" + ## Non-nil zero-length stand-in for empty payloads/names (nil would be UB for + ## consumers doing memcpy even at len 0). + +type + QueuedEvent* = object + # `name`/`data` point into reused per-slot buffers, or a one-off c_malloc marked by `*HeapOwned` when oversize; both c_malloc'd so they outlive the FFI thread's heap. + name*: cstring + nameHeapOwned*: bool + data*: ptr UncheckedArray[byte] + dataLen*: int + dataHeapOwned*: bool + + EventQueue* = object # SPSC ring; plain lock since ops are short and uncontended. + lock*: Lock + head*: int + tail*: int + count*: int + buf*: array[EventQueueCapacity, QueuedEvent] + slab*: array[EventQueueCapacity, ptr UncheckedArray[byte]] + nameSlab*: array[EventQueueCapacity, ptr UncheckedArray[byte]] + +proc allocSlot(nbytes: int): ptr UncheckedArray[byte] {.raises: [].} = + if nbytes <= 0: + return nil + cast[ptr UncheckedArray[byte]](c_malloc(csize_t(nbytes))) + +proc initEventQueue*(q: var EventQueue) {.raises: [].} = + q.lock.initLock() + q.head = 0 + q.tail = 0 + q.count = 0 + for i in 0 ..< EventQueueCapacity: + q.buf[i] = QueuedEvent() + q.slab[i] = allocSlot(MaxEventPayloadBytes) + q.nameSlab[i] = allocSlot(MaxEventNameBytes) + +proc releaseEvent*(qe: QueuedEvent) {.raises: [], gcsafe.} = + ## Frees only heap-fallback buffers; reused slot buffers persist. + if qe.nameHeapOwned and not qe.name.isNil(): + c_free(cast[pointer](qe.name)) + if qe.dataHeapOwned and not qe.data.isNil(): + c_free(qe.data) + +proc deinitEventQueue*(q: var EventQueue) {.raises: [].} = + ## Both producer and consumer must have stopped. + for i in 0 ..< EventQueueCapacity: + releaseEvent(q.buf[i]) + q.buf[i] = QueuedEvent() + if not q.slab[i].isNil(): + c_free(q.slab[i]) + q.slab[i] = nil + if not q.nameSlab[i].isNil(): + c_free(q.nameSlab[i]) + q.nameSlab[i] = nil + q.head = 0 + q.tail = 0 + q.count = 0 + q.lock.deinitLock() + +proc copyIntoSlot( + slot: ptr UncheckedArray[byte], slotCap, nbytes: int, src: pointer +): tuple[buf: ptr UncheckedArray[byte], heap: bool, ok: bool] {.raises: [].} = + ## Copies into `slot` when it fits, else a one-off c_malloc; `ok=false` only on + ## alloc failure. + if nbytes <= 0: + return (nil, false, true) + if nbytes <= slotCap and not slot.isNil(): + copyMem(slot, src, nbytes) + return (slot, false, true) + let heapBuf = cast[ptr UncheckedArray[byte]](c_malloc(csize_t(nbytes))) + if heapBuf.isNil(): + return (nil, false, false) + copyMem(heapBuf, src, nbytes) + (heapBuf, true, true) + +proc tryEnqueueEvent*( + q: var EventQueue, name: cstring, src: pointer, dataLen: int +): bool {.raises: [], gcsafe.} = + ## Copies `name` (NUL included) and payload into the tail slot's reused buffers + ## or a heap fallback; false when the ring is full or a fallback alloc fails. + withLock q.lock: + if q.count >= EventQueueCapacity: + return false + let slot = q.tail + # Include the NUL so the stored copy stays a valid cstring. + let nameBytes = + if name.isNil(): + 0 + else: + name.len + 1 + let nameRes = + copyIntoSlot(q.nameSlab[slot], MaxEventNameBytes, nameBytes, cast[pointer](name)) + if not nameRes.ok: + return false + let dataRes = copyIntoSlot(q.slab[slot], MaxEventPayloadBytes, dataLen, src) + if not dataRes.ok: + if nameRes.heap: + c_free(nameRes.buf) + return false + let nameCStr = + if nameRes.buf.isNil(): + emptyListenerPayload + else: + cast[cstring](nameRes.buf) + q.buf[slot] = QueuedEvent( + name: nameCStr, + nameHeapOwned: nameRes.heap, + data: dataRes.buf, + dataLen: dataLen, + dataHeapOwned: dataRes.heap, + ) + q.tail = (q.tail + 1) mod EventQueueCapacity + q.count.inc() + true + +proc peekEvent*(q: var EventQueue): Option[QueuedEvent] {.raises: [], gcsafe.} = + ## Returns the head without advancing (slot stays pinned so the producer can't + ## reuse it mid-read); pair each non-none peek with a `commitDequeue`. + withLock q.lock: + if q.count == 0: + return none(QueuedEvent) + return some(q.buf[q.head]) + +proc commitDequeue*(q: var EventQueue) {.raises: [], gcsafe.} = + ## Retires the dispatched head slot: frees any heap fallback and frees the slot. + withLock q.lock: + if q.count == 0: + return + releaseEvent(q.buf[q.head]) + q.buf[q.head] = QueuedEvent() + q.head = (q.head + 1) mod EventQueueCapacity + q.count.dec() + +proc eventQueueLen*(q: var EventQueue): int {.raises: [], gcsafe.} = + withLock q.lock: + return q.count + +proc notifyListeners*( + listeners: seq[FFIEventListener], retCode: cint, data: pointer, dataLen: int +) = + ## Empty payloads use `emptyListenerPayload` so consumers never see a nil ptr. + let n = max(dataLen, 0) + let dataPtr = + if n > 0 and not data.isNil(): + cast[ptr cchar](data) + else: + cast[ptr cchar](emptyListenerPayload) + for listener in listeners: + listener.callback(retCode, dataPtr, cast[csize_t](n), listener.userData) + +proc notifyListenersErr*(listeners: seq[FFIEventListener], msg: string) = + let p = + if msg.len > 0: + cast[pointer](unsafeAddr msg[0]) + else: + cast[pointer](emptyListenerPayload) + notifyListeners(listeners, RET_ERR, p, msg.len) + +var ffiCurrentEventRegistry* {.threadvar.}: ptr FFIEventRegistry + +var ffiCurrentEventQueue* {.threadvar.}: ptr EventQueue + # Installed by the FFI thread so dispatch templates need no `ctx`. + +var ffiCurrentEventQueueStuck* {.threadvar.}: ptr Atomic[bool] + # Sticky overflow flag; FFI request entry point reads it to reject. + +var ffiCurrentNotifyEventEnqueued* {.threadvar.}: proc() {.gcsafe, raises: [].} + # Wake hook so this module needn't depend on chronos; nil-safe. + +template enqueueOrMarkStuck(eventName: string, src: pointer, dataLen: int) = + ## Enqueues into the reused slot buffers; on queue-full sets the sticky stuck + ## flag and wakes the event thread (firing onNotResponding here could deadlock). + block enqueueBlock: + let q = ffiCurrentEventQueue + if q.isNil(): + chronicles.error "event queue not set on this thread", event = eventName + break enqueueBlock + if not q[].tryEnqueueEvent(cstring(eventName), src, dataLen): + chronicles.error "event queue full; library marked stuck", + event = eventName, capacity = EventQueueCapacity + if not ffiCurrentEventQueueStuck.isNil(): + ffiCurrentEventQueueStuck[].store(true) + if not ffiCurrentNotifyEventEnqueued.isNil(): + ffiCurrentNotifyEventEnqueued() + break enqueueBlock + if not ffiCurrentNotifyEventEnqueued.isNil(): + ffiCurrentNotifyEventEnqueued() + +template dispatchFFIEvent*(eventName: string, body: untyped) = + ## `body` yields string/seq[byte]. FFI thread only: enqueues; event thread fans out. + block: + let evtName: string = eventName + let bodyVal = body + let dataLen = bodyVal.len + let src: pointer = + if dataLen > 0: + unsafeAddr bodyVal[0] + else: + nil + enqueueOrMarkStuck(evtName, src, dataLen) + +template dispatchFFIEventCbor*(eventName: string, eventPayload: typed) = + ## Typed CBOR variant; param is `eventPayload` to avoid clobbering + ## `EventEnvelope.payload` substitution. + block: + let evtName: string = eventName + let encoded = cborEncode( + EventEnvelope[typeof(eventPayload)](eventType: evtName, payload: eventPayload) + ) + let src: pointer = + if encoded.len > 0: + unsafeAddr encoded[0] + else: + nil + enqueueOrMarkStuck(evtName, src, encoded.len) diff --git a/wasm-deps/ffi/ffi/ffi_handles.nim b/wasm-deps/ffi/ffi/ffi_handles.nim new file mode 100644 index 000000000..22a598aa9 --- /dev/null +++ b/wasm-deps/ffi/ffi/ffi_handles.nim @@ -0,0 +1,60 @@ +## Per-context registry of live `{.ffiHandle.}` objects; only the `uint64` id crosses the +## boundary. Ids are monotonic, never recycled (0 = null). FFI-thread-only, so no locking. + +import std/tables +import results +import ./cbor_serial + +type + FFIHandleRoot* = ref object of RootObj ## Base of every `{.ffiHandle.}` type. + + FFIHandleEntry = object + obj: FFIHandleRoot + typeName: string + + FFIHandleRegistry* = object + nextId*: uint64 + byHandle*: Table[uint64, FFIHandleEntry] + +proc initHandleRegistry*(reg: var FFIHandleRegistry) = + reg.nextId = 0'u64 + reg.byHandle = initTable[uint64, FFIHandleEntry]() + +proc deinitHandleRegistry*(reg: var FFIHandleRegistry) = + reg.byHandle = default(Table[uint64, FFIHandleEntry]) + reg.nextId = 0'u64 + +proc register*( + reg: var FFIHandleRegistry, obj: FFIHandleRoot, typeName: string +): uint64 = + reg.nextId.inc() + reg.byHandle[reg.nextId] = FFIHandleEntry(obj: obj, typeName: typeName) + reg.nextId + +proc lookup*( + reg: var FFIHandleRegistry, handle: uint64, typeName: string +): Result[FFIHandleRoot, string] = + ## Live ref for `handle`; err if absent or registered under another type. + let entry = reg.byHandle.getOrDefault(handle) + if entry.obj.isNil(): + return err("no ffiHandle with id " & $handle) + if entry.typeName != typeName: + return err( + "ffiHandle " & $handle & " has type '" & entry.typeName & "', expected '" & + typeName & "'" + ) + ok(entry.obj) + +proc release*(reg: var FFIHandleRegistry, handle: uint64): bool {.discardable.} = + if not reg.byHandle.hasKey(handle): + return false + reg.byHandle.del(handle) + return true + +proc releaseAll*(reg: var FFIHandleRegistry) = + ## Must run on the FFI thread that allocated the refs. + reg.byHandle.clear() + +proc encodeHandle*(id: uint64): seq[byte] = + ## Single ABI seam for the handle-id wire format. + cborEncode(id) diff --git a/wasm-deps/ffi/ffi/ffi_request_queue.nim b/wasm-deps/ffi/ffi/ffi_request_queue.nim new file mode 100644 index 000000000..6e50d4f2a --- /dev/null +++ b/wasm-deps/ffi/ffi/ffi_request_queue.nim @@ -0,0 +1,92 @@ +## Sharded, mutex-guarded MPSC ingress for `ptr FFIThreadRequest`: N intrusive +## FIFOs (one per producer) spread lock contention; the request is its own node +## so enqueue never touches a Nim GC heap. Unbounded — submit never blocks. + +import std/[atomics, locks] +import ./ffi_thread_request + +const + RequestQueueCount* = 16 + ## Independent ingress queues; ≥ concurrent producer count keeps collisions low. + QueuePadBytes = 192 + ## Pads each queue past a cache line (128B on Apple silicon) to avoid false + ## sharing between adjacent queues. + +static: + # `myQueueIndex` masks with `and`, so the count must be a power of two. + doAssert (RequestQueueCount and (RequestQueueCount - 1)) == 0, + "RequestQueueCount must be a power of two" + +type + RequestQueue = object + lock: Lock + head: ptr FFIThreadRequest ## consumer pops here (oldest) + tail: ptr FFIThreadRequest ## producers append here (newest) + pad: array[QueuePadBytes, byte] + + RequestQueueBank* = object + queues: array[RequestQueueCount, RequestQueue] + +var gRequestQueue {.threadvar.}: int +var gRequestQueueAssigned {.threadvar.}: bool +var gRequestQueueCounter: Atomic[int] + ## Round-robins producers onto distinct queues on first use so they fill evenly. + +proc myQueueIndex(): int {.raises: [].} = + if not gRequestQueueAssigned: + gRequestQueue = gRequestQueueCounter.fetchAdd(1) + gRequestQueueAssigned = true + return gRequestQueue and (RequestQueueCount - 1) + +proc initRequestQueue*(bank: var RequestQueueBank) {.raises: [].} = + for queue in bank.queues.mitems: + queue.lock.initLock() + queue.head = nil + queue.tail = nil + +proc deinitRequestQueue*(bank: var RequestQueueBank) {.raises: [].} = + ## Both producers and consumer must have stopped. Frees any still-queued request + ## (e.g. one raced in after the final drain) so a teardown race leaks nothing. + for queue in bank.queues.mitems: + var request = queue.head + while not request.isNil(): + let nextRequest = request[].next + deleteRequest(request) + request = nextRequest + queue.head = nil + queue.tail = nil + queue.lock.deinitLock() + +proc pushRequest*( + bank: var RequestQueueBank, request: ptr FFIThreadRequest +): bool {.raises: [].} = + ## Append `request` to this thread's queue (takes ownership). True only when the + ## queue was empty — the one push that must wake the sleeping consumer. + request[].next = nil + let idx = myQueueIndex() + withLock bank.queues[idx].lock: + let wasEmpty = bank.queues[idx].tail.isNil() + if bank.queues[idx].tail.isNil(): + bank.queues[idx].head = request + else: + bank.queues[idx].tail[].next = request + bank.queues[idx].tail = request + return wasEmpty + +proc mergeQueues*(bank: var RequestQueueBank): ptr FFIThreadRequest {.raises: [].} = + ## Single-consumer: splice every queue into one chain and reset them. Caller owns + ## the chain and must read each `next` before dispatch (dispatch frees the request). + var head: ptr FFIThreadRequest = nil + var tail: ptr FFIThreadRequest = nil + for queue in bank.queues.mitems: + withLock queue.lock: + let h = queue.head + if not h.isNil(): + if head.isNil(): + head = h + else: + tail[].next = h + tail = queue.tail + queue.head = nil + queue.tail = nil + return head diff --git a/wasm-deps/ffi/ffi/ffi_singlethread.nim b/wasm-deps/ffi/ffi/ffi_singlethread.nim new file mode 100644 index 000000000..2d4291220 --- /dev/null +++ b/wasm-deps/ffi/ffi/ffi_singlethread.nim @@ -0,0 +1,73 @@ +## Threads-off stand-ins for the two primitives nim-ffi's context is built on. +## +## `chronos/threadsync` is a hard `{.fatal.}` under `--threads:off`, and +## `system.Thread` / `createThread` / `joinThread` do not exist there either -- +## so the FFIContext object cannot even be *declared*, let alone used. +## +## Rather than gate all ~20 use sites (and re-gate them on every nim-ffi bump), +## this module supplies API-compatible no-ops. The upstream lifecycle code then +## compiles unchanged: it "creates" threads that do not exist and "fires" +## signals nobody waits on. That is sound only because the single-threaded +## transport never enqueues -- `sendRequestToFFIThread` runs the handler inline +## on the caller's chronos loop -- so no worker is needed to drain anything. +## +## Keep this in step with chronos' ThreadSignalPtr surface when bumping nim-ffi; +## a missing proc shows up as a plain "undeclared field" at compile time. + +{.push raises: [].} + +import chronos, results + +type + ThreadSignalPtr* = ptr object + ## No-op stand-in for chronos' cross-thread signal. Pointer-shaped, not an + ## object: the lifecycle code assigns `nil` to these fields on teardown and + ## nil-checks them before use, so a value type does not typecheck. + + Thread*[T] = object ## No-op stand-in for system.Thread. + started: bool + +var dummySignal: int + ## Address handed out by `new` so signals read as non-nil (nil means + ## "not initialised" upstream). Never dereferenced. + +proc new*(T: typedesc[ThreadSignalPtr]): Result[ThreadSignalPtr, string] = + ok(cast[ThreadSignalPtr](addr dummySignal)) + +proc close*(signal: ThreadSignalPtr): Result[void, string] = + ok() + +proc fireSync*( + signal: ThreadSignalPtr, timeout = InfiniteDuration +): Result[bool, string] = + ## Nothing waits on these threads-off, so a "fire" is a successful no-op. + ok(true) + +proc wait*( + signal: ThreadSignalPtr +): Future[void] {.async: (raises: [CancelledError]).} = + ## Never completes on its own. The only callers are the worker loops, which + ## are never started threads-off, so this is unreachable rather than a hang. + await sleepAsync(InfiniteDuration) + +proc waitSync*( + signal: ThreadSignalPtr, timeout = InfiniteDuration +): Result[bool, string] = + ## Reports "signalled" immediately. Callers use this to block until a worker + ## acknowledges something; with no worker there is nothing to wait for, and + ## returning false would stall shutdown on a signal that can never arrive. + ok(true) + + +proc createThread*[T]( + thread: var Thread[T], body: proc(arg: T) {.thread, nimcall.}, arg: T +) = + ## Deliberately does not run `body`: the worker loops block on signals that + ## never fire. Requests are dispatched inline instead. + thread.started = true + +proc joinThread*[T](thread: Thread[T]) = + discard + +proc running*[T](thread: Thread[T]): bool = + false diff --git a/wasm-deps/ffi/ffi/ffi_thread.nim b/wasm-deps/ffi/ffi/ffi_thread.nim new file mode 100644 index 000000000..fec2acda4 --- /dev/null +++ b/wasm-deps/ffi/ffi/ffi_thread.nim @@ -0,0 +1,295 @@ +## FFI-thread body and request submission API. Included from `ffi_context.nim`. +## Dispatches `FFIThreadRequest`s from `reqQueueBank` and advances +## `ctx.ffiHeartbeat` so the event thread can spot a wedged FFI thread. + +proc sendRequestToFFIThreadQueued( + ctx: ptr FFIContext, ffiRequest: ptr FFIThreadRequest +): Result[void, string] = + if ctx.eventQueueStuck.load(): + deleteRequest(ffiRequest) + return err("event queue stuck - library cannot accept new requests") + + if onFFIThread: + # A handler re-dispatching onto its own FFI thread would deadlock; reject. + deleteRequest(ffiRequest) + return err( + "reentrant ffi call: a handler invoked sendRequestToFFIThread on its own context" + ) + + if ctx.lifecycle.load() != CtxLifecycle.Active: + deleteRequest(ffiRequest) + return err("FFI context is not accepting requests (being recycled)") + + # Wake only when the push found the queue empty: waking per submit kills scaling, and a skipped wake just waits the consumer's 100ms poll. + let shouldWake = ctx.reqQueueBank.pushRequest(ffiRequest) + + # A failed wake is non-fatal (poll-drain still dispatches); erroring here would double-fire the callback for a request that still completes. + if shouldWake: + ctx.reqSignal.fireSync().isOkOr: + error "failed to wake FFI thread after enqueue (request still queued)", + error = error + + ok() + +when not singleThreaded: + proc sendRequestToFFIThread*( + ctx: ptr FFIContext, ffiRequest: ptr FFIThreadRequest + ): Result[void, string] = + sendRequestToFFIThreadQueued(ctx, ffiRequest) + +proc awaitWithStaleWarnings( + retFut: Future[Result[seq[byte], string]], + request: ptr FFIThreadRequest, + interval: Duration, + reqId: string, +): Future[Result[seq[byte], string]] {.async.} = + ## Pings RET_STALE_WARN every `interval` while the handler runs, then returns + ## its real result. Never cancels the handler: a hard-cancel mid-call could + ## leave the underlying library partially applied. + let intervalMs = interval.milliseconds + if intervalMs <= 0: + return await retFut + var elapsed = 0'i64 + while not retFut.finished(): + let timer = sleepAsync(interval) + # `race` doesn't cancel the loser, so the handler keeps running. + discard await race(retFut, timer) + if retFut.finished(): + if not timer.finished(): + await timer.cancelAndWait() + break + elapsed += intervalMs + warn "ffi request still in flight; caller notified via RET_STALE_WARN", + reqId = reqId, elapsedMs = elapsed + fireStaleWarn(request, elapsed) + return await retFut + +proc processRequest[T]( + request: ptr FFIThreadRequest, ctx: ptr FFIContext[T] +) {.async.} = + ## Processes one request on the FFI thread. + + let reqId = $request[].reqId + let reqIdCs = reqId.cstring # keeps reqId alive + + let retFut = + if not ctx[].registeredRequests[].contains(reqIdCs): + nilProcess(request[].reqId) + else: + ctx[].registeredRequests[][reqIdCs](cast[pointer](request), ctx) + + # One try over warn-loop + handler so a shutdown-drain cancel still reaches the response-and-free below. + let res = + try: + await awaitWithStaleWarnings(retFut, request, ctx.staleWarnInterval, reqId) + except CatchableError as e: + Result[seq[byte], string].err( + "Error in processRequest for " & reqId & ": " & e.msg + ) + + try: + handleRes(res, request) + except Exception as e: + error "Unexpected exception in handleRes", error = e.msg + +when singleThreaded: + proc sendRequestToFFIThread*( + ctx: ptr FFIContext, ffiRequest: ptr FFIThreadRequest + ): Result[void, string] = + ## Single-threaded transport: nothing drains reqQueueBank, so run the + ## handler on the caller's chronos loop instead of enqueuing it. + ## + ## Fire-and-forget rather than `waitFor`: handlers await the network, and + ## blocking here would starve the browser's event loop and deadlock. The + ## host drives progress with `ffi_poll()`; `processRequest` still fires the + ## caller's callback and frees the request through `handleRes`, exactly as + ## the threaded path does. + if ctx.lifecycle.load() != CtxLifecycle.Active: + deleteRequest(ffiRequest) + return err("FFI context is not accepting requests (being recycled)") + asyncSpawn processRequest(ffiRequest, ctx) + poll() # advance the handler to its first await + return ok() + + proc ffiPoll*() {.exportc: "ffi_poll", cdecl.} = + ## One chronos iteration. The browser host calls this from its event loop so + ## handlers progress without blocking JS. One call == one iteration, so the + ## host must pump hard while a request is in flight -- see + ## docs/wasm-edge-node.md. + poll() + +proc freeLib[T](ctx: ptr FFIContext[T]) {.gcsafe.} = + ## Releases the library object the ctor stored in ctx.myLib. Only owned libs + ## (createShared'd by a ctor) are freed; the worker's stack fallback is not. + # A reused slot skips initContextResources, so the recycle path clears this. + ctx.libReady.store(false) + if not ctx.myLibOwned or ctx.myLib.isNil(): + ctx.myLib = nil + return + when not defined(gcRefc): + try: + {.cast(gcsafe).}: + `=destroy`(ctx.myLib[]) + except Exception as e: + error "destroying the library on recycle raised; freeing it anyway", error = e.msg + else: + when T is ref: + if ctx.myLibRefd: + GC_unref(ctx.myLib[]) + ctx.myLibRefd = false + freeShared(ctx.myLib) + ctx.myLib = nil + ctx.myLibOwned = false + +const RecycledReason = + "FFI context was recycled before this request ran; the caller is gone" + +proc rejectQueuedRequests[T](ctx: ptr FFIContext[T]) = + ## Fails every queued request instead of dispatching it. A request that a + ## destroyed context left behind still carries that host's `userData`, which + ## the host has freed; running it would answer a dead callback, and running it + ## after the slot is reused would run it against the library of the next owner. + var request = ctx.reqQueueBank.mergeQueues() + while not request.isNil(): + let nextRequest = request[].next # read before handleRes frees it + try: + handleRes(Result[seq[byte], string].err(RecycledReason), request) + except Exception as e: + error "rejecting a queued request raised", error = e.msg + request = nextRequest + +proc recycleContext[T]( + ctx: ptr FFIContext[T], ongoing: ptr seq[Future[void]] +) {.async.} = + ## Drain in-flight handlers, free the lib, clear listeners and release the + ## slot — all WITHOUT stopping the worker/event threads, so the next + ## createFFIContext reuses them (no fd churn). Then fire recycleDoneSignal. + ongoing[].keepItIf(not it.finished()) + var drained = ongoing[].len == 0 + if not drained: + drained = await allFutures(ongoing[]).withTimeout(RecycleTimeout) + if not drained: + for fut in ongoing[]: + fut.cancelSoon() + drained = await allFutures(ongoing[]).withTimeout(RecycleTimeout) + + freeLib(ctx) + clearListeners(ctx[].eventRegistry) + rejectQueuedRequests(ctx) + ongoing[].setLen(0) + + # Fire before the release: a thread that claims the freed slot first would + # otherwise take this fire as the answer to its own recycle. + let fireRes = ctx.recycleDoneSignal.fireSync() + if fireRes.isErr(): + error "failed to fire recycleDoneSignal", err = fireRes.error + ctx.releaseClaim() + +var ffiEventQueueSignalPtr {.threadvar.}: ThreadSignalPtr + # Stashed so the hook has no closure env. + +proc ffiNotifyEventEnqueuedHook() {.gcsafe, raises: [].} = + if not ffiEventQueueSignalPtr.isNil(): + let res = ffiEventQueueSignalPtr.fireSync() + if res.isErr(): + error "failed to fire eventQueueSignal after enqueue", err = res.error + +proc proveAlive(ctx: ptr FFIContext) = + ## Advance the heartbeat the event thread polls; only movement matters, not value. + ctx.ffiHeartbeat.atomicInc() + +proc ffiThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} = + ffiCurrentEventRegistry = addr ctx[].eventRegistry + ffiCurrentEventQueue = addr ctx[].eventQueue + ffiCurrentEventQueueStuck = addr ctx[].eventQueueStuck + ffiEventQueueSignalPtr = ctx.eventQueueSignal + ffiCurrentNotifyEventEnqueued = ffiNotifyEventEnqueuedHook + onFFIThread = true + + logging.setupLog(logging.LogLevel.DEBUG, logging.LogFormat.TEXT) + + defer: + onFFIThread = false + # Free handle refs on the thread that allocated them (refc heap is thread-local). + ctx[].handles.releaseAll() + # Let the event thread stop draining and exit; wake it so it notices now. + ctx.ffiThreadExited.store(true) + ctx.eventQueueSignal.fireSync().isOkOr: + error "failed to wake event thread on FFI thread exit", err = error + # Unblocks destroyFFIContext's bounded wait. + let fireRes = ctx.threadExitSignal.fireSync() + if fireRes.isErr(): + error "failed to fire threadExitSignal on FFI thread exit", err = fireRes.error + + let ffiRun = proc(ctx: ptr FFIContext[T]) {.async.} = + var ffiReqHandler: T # main library object (Waku, LibP2P, SDS, …) + + # Tracked so shutdown can drain them; abandoning a future leaks its request. + var pending: seq[Future[void]] = @[] + + proc cleanFinishedRequests() = + var i = 0 + while i < pending.len: + if not pending[i].finished(): + inc i + continue + pending.del(i) + + proc processQueue() = + ## Drain fully: one wake can stand for many submits. + while true: + var request = ctx.reqQueueBank.mergeQueues() + if request.isNil(): + break + while not request.isNil(): + let nextRequest = request[].next # read before processRequest frees it + # Tick per dispatch so a backlog can't flatline the heartbeat mid-drain. + ctx.proveAlive() + if ctx.myLib.isNil(): + # Must stay inside the closure: keeps `ffiReqHandler` alive across awaits. + ctx.myLib = addr ffiReqHandler + + pending.add processRequest(request, ctx) + request = nextRequest + + while ctx.running.load(): + ctx.proveAlive() + + # Recycle requested by the ffiDtor: drain + free lib + release the slot, + # keeping this thread alive for the next createFFIContext to reuse. + var expected = CtxLifecycle.RecyclePending + if ctx.lifecycle.compareExchange(expected, CtxLifecycle.Recycling): + await recycleContext(ctx, addr pending) + continue + + # A submit that read `Active` just before the recycle can still land here. + # Fail it rather than run it against the library of the next owner. + if ctx.lifecycle.load() != CtxLifecycle.Active: + rejectQueuedRequests(ctx) + discard await ctx.reqSignal.wait().withTimeout(chronos.milliseconds(100)) + continue + + cleanFinishedRequests() + + # Block until a submit signals us, or at most 100ms. + discard await ctx.reqSignal.wait().withTimeout(chronos.milliseconds(100)) + processQueue() + + # Drain once more for requests enqueued just before `running` flipped. + processQueue() + cleanFinishedRequests() + if pending.len > 0: + try: + await allFutures(pending) + except CatchableError as e: + error "draining pending FFI requests on shutdown raised", error = e.msg + + # Run the library's async {.ffiDtor.} shutdown before join if one exists and a request populated `myLib`; exceptions logged, never propagated. + let teardown = ffiTeardownHook[T]() + if not teardown.isNil() and not ctx.myLib.isNil(): + try: + await teardown(ctx.myLib) + except CatchableError as e: + error "library teardown raised on shutdown", error = e.msg + + waitFor ffiRun(ctx) diff --git a/wasm-deps/ffi/ffi/ffi_thread_request.nim b/wasm-deps/ffi/ffi/ffi_thread_request.nim index 93c8b0cf1..ea85ad37a 100644 --- a/wasm-deps/ffi/ffi/ffi_thread_request.nim +++ b/wasm-deps/ffi/ffi/ffi_thread_request.nim @@ -1,64 +1,237 @@ -## This file contains the base message request type that will be handled. -## The requests are created by the main thread and processed by -## the FFI Thread. +## Request blob passed main→FFI thread. Uses libc malloc/free (not Nim +## allocShared) so a producer thread exiting before the FFI thread frees can't +## dangle into reclaimed per-thread ORC TLS. -import std/[json, macros], results, tables +import system/ansi_c +import results import chronos -import ./ffi_config -when not singleThreaded: - import chronos/threadsync # ThreadSignalPtr requires threads enabled -import ./ffi_types, ./internal/ffi_macro, ./alloc +import ./ffi_types, ./alloc, ./cbor_serial + +const EmptyErrorMarker = "unknown error" + ## RET_ERR fallback message; keeps the callback msg ptr non-nil. + +const MaxScalarArgs* = 8 + ## Inline scalar fast-path capacity; more params can't use it (compile-time checked). type FFIThreadRequest* = object - callback: FFICallBack - userData: pointer - reqId*: cstring - reqContent*: pointer + callback*: FFICallBack + userData*: pointer + reqId*: cstring ## Req type name used to look up the handler. + data*: ptr UncheckedArray[byte] + ## Owned request payload: CBOR-encoded, or a packed `_CWire` struct on the + ## `abi = c` path. Nil on the scalar fast path. + dataLen*: int + rawReply*: bool + ## CBOR-free request (scalar fast path or `abi = c`): the reply is raw bytes, + ## so a 0-length one is a real empty string, not a CBOR "no value". + scalarArgs*: array[MaxScalarArgs, uint64] + ## Inlined scalar args (no per-call c_malloc); a plain array keeps + ## `deleteRequest` unaliased. + next*: ptr FFIThreadRequest + ## Intrusive queue link; request doubles as its own node so enqueue needs no + ## ORC-heap alloc. + responded*: bool + ## De-dupes the callback across timeout/completion; both on FFI thread, no race. + +func ffiPackScalar*[T](x: T): uint64 = + ## Bit-cast one scalar into a uint64 request slot. Reverse with `ffiUnpackScalar`. + when T is SomeFloat: + cast[uint64](float64(x)) + elif T is bool: + uint64(ord(x)) + elif T is SomeSignedInt: + cast[uint64](int64(x)) + else: + uint64(x) + +func ffiUnpackScalar*[T](u: uint64, _: typedesc[T]): T = + ## Inverse of `ffiPackScalar`. + when T is SomeFloat: + T(cast[float64](u)) + elif T is bool: + u != 0'u64 + elif T is SomeSignedInt: + T(cast[int64](u)) + else: + T(u) + +proc allocBaseRequest( + callback: FFICallBack, userData: pointer, reqId: cstring +): ptr FFIThreadRequest = + ## c_malloc the envelope and set routing fields; payload set by a helper below. + var ret = cast[ptr FFIThreadRequest](c_malloc(csize_t(sizeof(FFIThreadRequest)))) + ret[].callback = callback + ret[].userData = userData + ret[].reqId = reqId.alloc() + ret[].data = nil + ret[].dataLen = 0 + ret[].rawReply = false + ret[].next = nil + ret[].responded = false + return ret + +proc copySharedPayload(req: ptr FFIThreadRequest, data: ptr byte, dataLen: int) = + ## c_malloc a fresh buffer and copy `dataLen` bytes in; empty payload is a no-op. + if dataLen > 0 and not data.isNil(): + req[].data = cast[ptr UncheckedArray[byte]](c_malloc(csize_t(dataLen))) + copyMem(req[].data, data, dataLen) + req[].dataLen = dataLen + +proc adoptOwnedSharedPayload( + req: ptr FFIThreadRequest, data: ptr UncheckedArray[byte], dataLen: int +) = + ## Embed an already-c_malloc'd buffer without copying; frees a zero-length + ## non-nil buffer so it doesn't leak. + if dataLen > 0 and not data.isNil(): + req[].data = data + req[].dataLen = dataLen + elif not data.isNil(): + c_free(data) + +proc initFromPtr*( + T: typedesc[FFIThreadRequest], + callback: FFICallBack, + userData: pointer, + reqId: cstring, + data: ptr byte, + dataLen: int, +): ptr type T = + ## Copies raw ptr+len into a fresh buffer owned by the returned request. + var ret = allocBaseRequest(callback, userData, reqId) + copySharedPayload(ret, data, dataLen) + return ret proc init*( T: typedesc[FFIThreadRequest], callback: FFICallBack, userData: pointer, reqId: cstring, - reqContent: pointer, + data: openArray[byte], ): ptr type T = - var ret = createShared(FFIThreadRequest) - ret[].callback = callback - ret[].userData = userData - ret[].reqId = reqId.alloc() - ret[].reqContent = reqContent + ## Like `initFromPtr` but from a Nim openArray. + let dataPtr = + if data.len > 0: + cast[ptr byte](unsafeAddr data[0]) + else: + nil + initFromPtr(T, callback, userData, reqId, dataPtr, data.len) + +proc initFromOwnedShared*( + T: typedesc[FFIThreadRequest], + callback: FFICallBack, + userData: pointer, + reqId: cstring, + data: ptr UncheckedArray[byte], + dataLen: int, + rawReply: bool = false, +): ptr type T = + ## Adopts an already-c_malloc'd buffer (no copy); `deleteRequest` c_frees it. + ## Pass `(nil, 0)` for an empty payload. Set `rawReply` when the handler answers + ## with raw (non-CBOR) bytes, so an empty reply reads as a real empty value. + var ret = allocBaseRequest(callback, userData, reqId) + adoptOwnedSharedPayload(ret, data, dataLen) + ret[].rawReply = rawReply return ret -proc deleteRequest(request: ptr FFIThreadRequest) = - deallocShared(request[].reqId) - deallocShared(request) +proc initScalar*( + T: typedesc[FFIThreadRequest], + callback: FFICallBack, + userData: pointer, + reqId: cstring, + args: varargs[uint64], +): ptr type T = + ## Scalar-fast-path request: packed args ride inline, no payload c_malloc. + doAssert args.len <= MaxScalarArgs, + "initScalar: " & $args.len & " scalar args exceed MaxScalarArgs (" & $MaxScalarArgs & + ")" + var ret = allocBaseRequest(callback, userData, reqId) + ret[].rawReply = true + for i in 0 ..< args.len: + ret[].scalarArgs[i] = args[i] + ret -proc handleRes*[T: string | void]( - res: Result[T, string], request: ptr FFIThreadRequest -) = - ## Handles the Result responses, which can either be Result[string, string] or - ## Result[void, string]. +func ffiRawRetBytes*[T](x: T): seq[byte] = + ## CBOR-free handler result as raw bytes: string/cstring ride as UTF-8, other + ## scalars as the 8-byte native image of `ffiPackScalar(x)`. + when T is string: + var b = newSeq[byte](x.len) + if x.len > 0: + copyMem(addr b[0], unsafeAddr x[0], x.len) + b + elif T is cstring: + let n = x.len + var b = newSeq[byte](n) + if n > 0: + copyMem(addr b[0], cast[pointer](x), n) + b + else: + let u = ffiPackScalar(x) + var b = newSeq[byte](sizeof(uint64)) + copyMem(addr b[0], unsafeAddr u, sizeof(uint64)) + b - defer: - deleteRequest(request) +proc deleteRequest*(request: ptr FFIThreadRequest) = + if not request[].data.isNil: + c_free(request[].data) + if not request[].reqId.isNil: + c_free(cast[pointer](request[].reqId)) + c_free(request) +proc fireCallback*(res: Result[seq[byte], string], request: ptr FFIThreadRequest) = + ## Answers the foreign callback at most once (timeout and completion both call + ## it). Does NOT free the request; `handleRes` does. + if request[].responded: + return + request[].responded = true if res.isErr(): foreignThreadGc: - let msg = "ffi error: handleRes fireSyncRes error: " & $res.error + let msg = if res.error.len > 0: res.error else: EmptyErrorMarker request[].callback( - RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), request[].userData + RET_ERR, unsafeAddr msg[0], cast[csize_t](msg.len), request[].userData ) return foreignThreadGc: - var msg: cstring = "" - when T is string: - msg = res.get().cstring() + let bytes = res.get() + if bytes.len > 0: + request[].callback( + RET_OK, + cast[ptr cchar](unsafeAddr bytes[0]), + cast[csize_t](bytes.len), + request[].userData, + ) + elif request[].rawReply: + # A CBOR-free 0-byte return is a real empty string, not CBOR "no value". + var empty: byte + request[].callback( + RET_OK, cast[ptr cchar](addr empty), 0.csize_t, request[].userData + ) + else: + # Always hand the callback a real buffer; CBOR null marks "no value". + var sentinel = CborNullByte + request[].callback( + RET_OK, cast[ptr cchar](addr sentinel), 1.csize_t, request[].userData + ) + +proc fireStaleWarn*(request: ptr FFIThreadRequest, elapsedMs: int64) = + ## In-flight ping; leaves `responded` unset and may fire many times — the + ## terminal RET_OK/RET_ERR is still owed. + if request[].responded: + return + foreignThreadGc: + let msg = $elapsedMs request[].callback( - RET_OK, unsafeAddr msg[0], cast[csize_t](len(msg)), request[].userData + RET_STALE_WARN, + cast[ptr cchar](unsafeAddr msg[0]), + cast[csize_t](msg.len), + request[].userData, ) - return -proc nilProcess*(reqId: cstring): Future[Result[string, string]] {.async.} = +proc handleRes*(res: Result[seq[byte], string], request: ptr FFIThreadRequest) = + ## Terminal step: delivers the response and frees the request exactly once. + defer: + deleteRequest(request) + fireCallback(res, request) + +proc nilProcess*(reqId: cstring): Future[Result[seq[byte], string]] {.async.} = return err("This request type is not implemented: " & $reqId) - diff --git a/wasm-deps/ffi/ffi/ffi_types.nim b/wasm-deps/ffi/ffi/ffi_types.nim index 76ead50ea..1b1e8a7af 100644 --- a/wasm-deps/ffi/ffi/ffi_types.nim +++ b/wasm-deps/ffi/ffi/ffi_types.nim @@ -1,25 +1,23 @@ import std/tables import chronos -################################################################################ -### Exported types - type FFICallBack* = proc( callerRet: cint, msg: ptr cchar, len: csize_t, userData: pointer ) {.cdecl, gcsafe, raises: [].} + ## Result-delivery callback. `RET_OK`/`RET_ERR` fire once and end the request; + ## `RET_STALE_WARN` may fire repeatedly before them. const RET_OK*: cint = 0 const RET_ERR*: cint = 1 const RET_MISSING_CALLBACK*: cint = 2 +const RET_STALE_WARN*: cint = 3 + ## Non-terminal: request still in flight, fires every `StaleWarnInterval` with + ## `msg` = elapsed ms as decimal ASCII, always followed by a terminal code. -### End of exported types -################################################################################ - -################################################################################ -### FFI utils - -type FFIRequestProc* = - proc(request: pointer, reqHandler: pointer): Future[Result[string, string]] {.async.} +type FFIRequestProc* = proc( + request: pointer, reqHandler: pointer +): Future[Result[seq[byte], string]] {.async.} + ## OK payload is a CBOR-encoded response body; errors are plain UTF-8. template foreignThreadGc*(body: untyped) = when declared(setupForeignThreadGc): @@ -30,10 +28,5 @@ template foreignThreadGc*(body: untyped) = when declared(tearDownForeignThreadGc): tearDownForeignThreadGc() -## Registered requests table populated at compile time and never updated at run time. -## The key represents the request type name as cstring, e.g., "CreateNodeRequest". -## The value is a proc that handles the request asynchronously. +## Compile-time-populated table: request type name (cstring) -> async handler. var registeredRequests*: Table[cstring, FFIRequestProc] - -### End of FFI utils -################################################################################ diff --git a/wasm-deps/ffi/ffi/internal/c_macro_helpers.nim b/wasm-deps/ffi/ffi/internal/c_macro_helpers.nim new file mode 100644 index 000000000..960aeb0ea --- /dev/null +++ b/wasm-deps/ffi/ffi/internal/c_macro_helpers.nim @@ -0,0 +1,950 @@ +## Compile-time helpers for the `abi = c` C-struct ABI: for each `{.ffi: "abi = c".}` +## object T, emits a `T_CWire` companion plus `cwirePack`/`cwireUnpack`/`cwireFree`. +## A `seq` may only be a top-level field (no single-field wire form to nest). + +import std/macros +import ../codegen/meta + +const + cwireItemsSuffix = "_items" + cwireLenSuffix = "_len" + +var emittedCWireTypes {.compileTime.}: seq[string] + +proc isCWireEmitted(typeName: string): bool {.compileTime.} = + # Indexed scan: `for x in seq` over a freshly-mutated compileTime seq goes stale on the Nim 2.2 VM. + for i in 0 ..< emittedCWireTypes.len: + if emittedCWireTypes[i] == typeName: + return true + false + +proc markCWireEmitted(typeName: string) {.compileTime.} = + if not isCWireEmitted(typeName): + emittedCWireTypes.add(typeName) + +proc cwireTypeName*(userTypeName: string): string = + userTypeName & "_CWire" + +proc seqItemsField(obj, field: NimNode): NimNode = + newDotExpr(obj, ident($field & cwireItemsSuffix)) + +proc seqLenField(obj, field: NimNode): NimNode = + newDotExpr(obj, ident($field & cwireLenSuffix)) + +proc isStringType*(t: NimNode): bool = + t.kind == nnkIdent and ($t == "string" or $t == "cstring") + +proc isBracketOf(t: NimNode, heads: openArray[string]): bool = + t.kind == nnkBracketExpr and t.len >= 2 and t[0].kind == nnkIdent and $t[0] in heads + +proc isSeqType(t: NimNode): bool = + isBracketOf(t, ["seq"]) + +proc isOptionType(t: NimNode): bool = + isBracketOf(t, ["Option", "Maybe"]) + +proc isArrayType(t: NimNode): bool = + t.kind == nnkBracketExpr and t.len == 3 and t[0].kind == nnkIdent and $t[0] == "array" + +proc isTupleType(t: NimNode): bool = + t.kind == nnkTupleTy + +proc tupleComponents(t: NimNode): seq[tuple[name: string, typ: NimNode]] = + ## Flatten a named tuple into `(name, type)` pairs, one per name. + var comps: seq[tuple[name: string, typ: NimNode]] = @[] + for defs in t: + if defs.kind != nnkIdentDefs: + error("cwire: only named tuples are supported: " & t.repr) + let typ = defs[^2] + for i in 0 ..< defs.len - 2: + comps.add((name: $defs[i], typ: typ)) + comps + +proc isKnownFFIType(name: string): bool {.compileTime.} = + for typeMeta in ffiTypeRegistry: + if typeMeta.name == name and not typeMeta.isEnum(): + return true + false + +proc isNestedFFIType(t: NimNode): bool = + ## Enums are excluded: they are registered types but have no `_CWire` + ## companion, and treating one as a nested struct would silently drop its value. + t.kind == nnkIdent and isKnownFFIType($t) + +proc rejectEnumOnCWire(t: NimNode) {.compileTime.} = + if t.kind == nnkIdent and isFFIEnumTypeName($t): + error( + "cwire: `abi = c` does not support enum types yet, but " & $t & + " crosses the boundary here; use the CBOR ABI for this proc or type" + ) + +proc cwireNeedsFree(t: NimNode): bool = + ## Whether the wire form of `t` owns allocations `cwireFree` must release. + if isStringType(t) or isNestedFFIType(t) or isOptionType(t) or isSeqType(t): + return true + if isArrayType(t): + return cwireNeedsFree(t[2]) + if isTupleType(t): + for c in tupleComponents(t): + if cwireNeedsFree(c.typ): + return true + return false + false + +proc rejectNestedSeq(t: NimNode) = + error( + "cwire: `seq` has no single-field wire form, so it can't nest inside " & + "another container (use it only as a top-level field): " & t.repr + ) + +proc wireValueType(t: NimNode): NimNode = + ## Single-field wire form of value type `t`; `seq` has none, so it errors here. + rejectEnumOnCWire(t) + if isStringType(t): + return ident("cstring") + if isNestedFFIType(t): + return ident(cwireTypeName($t)) + if isOptionType(t): + return nnkPtrTy.newTree(wireValueType(t[1])) + if isArrayType(t): + return + nnkBracketExpr.newTree(ident("array"), t[1].copyNimTree(), wireValueType(t[2])) + if isTupleType(t): + let wireTup = nnkTupleTy.newTree() + for c in tupleComponents(t): + wireTup.add(newIdentDefs(ident(c.name), wireValueType(c.typ))) + return wireTup + if isSeqType(t): + rejectNestedSeq(t) + t + +proc wireFieldsFor(fieldName: string, fieldType: NimNode): seq[NimNode] = + ## IdentDefs for one field; `seq[T]` splits into `_items` + `_len`. + if isSeqType(fieldType): + let elemWire = wireValueType(fieldType[1]) + let itemsField = newIdentDefs( + ident(fieldName & cwireItemsSuffix), + nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("UncheckedArray"), elemWire)), + newEmptyNode(), + ) + let lenField = + newIdentDefs(ident(fieldName & cwireLenSuffix), ident("int"), newEmptyNode()) + return @[itemsField, lenField] + @[newIdentDefs(ident(fieldName), wireValueType(fieldType), newEmptyNode())] + +proc buildCWireTypeDef( + userTypeName: string, fieldNames: seq[string], fieldTypes: seq[NimNode] +): NimNode = + ## Build the bare `nnkTypeDef` for the wire companion of `userTypeName`. + let wireName = ident(cwireTypeName(userTypeName)) + var fields: seq[NimNode] = @[] + for i in 0 ..< fieldNames.len: + for fd in wireFieldsFor(fieldNames[i], fieldTypes[i]): + fields.add(fd) + let recList = + if fields.len > 0: + newTree(nnkRecList, fields) + else: + newTree( + nnkRecList, newIdentDefs(ident("_placeholder"), ident("uint8"), newEmptyNode()) + ) + let objTy = newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), recList) + newTree(nnkTypeDef, postfix(wireName, "*"), newEmptyNode(), objTy) + +proc emitOptionPack(dstAccess, srcAccess, userType: NimNode): NimNode +proc emitOptionUnpack(dstAccess, srcAccess, userType: NimNode): NimNode +proc emitOptionFree(dstAccess, userType: NimNode): NimNode +proc emitArrayPack(dstAccess, srcAccess, arrType: NimNode): NimNode +proc emitArrayUnpack(dstAccess, srcAccess, arrType: NimNode): NimNode +proc emitArrayFree(dstAccess, arrType: NimNode): NimNode +proc emitTuplePack(dstAccess, srcAccess, tupType: NimNode): NimNode +proc emitTupleUnpack(dstAccess, srcAccess, tupType: NimNode): NimNode +proc emitTupleFree(dstAccess, tupType: NimNode): NimNode + +proc emitElemPack(dstElem, srcElem, elemType: NimNode): NimNode = + ## Pack one value; recurses through nested ffi/Option/array/tuple, POD copied. + if isStringType(elemType): + return newAssignment(dstElem, newCall(ident("cwireAllocStr"), srcElem)) + if isNestedFFIType(elemType): + return newCall(ident("cwirePack"), dstElem, srcElem) + if isOptionType(elemType): + return emitOptionPack(dstElem, srcElem, elemType) + if isArrayType(elemType): + return emitArrayPack(dstElem, srcElem, elemType) + if isTupleType(elemType): + return emitTuplePack(dstElem, srcElem, elemType) + if isSeqType(elemType): + rejectNestedSeq(elemType) + newAssignment(dstElem, srcElem) + +proc emitElemUnpack(dstElem, srcElem, elemType: NimNode): NimNode = + ## Inverse of `emitElemPack`: copy one value back into Nim memory. + if isStringType(elemType): + return newAssignment(dstElem, newCall(ident("$"), srcElem)) + if isNestedFFIType(elemType): + return newAssignment(dstElem, newCall(ident("cwireUnpack"), srcElem)) + if isOptionType(elemType): + return emitOptionUnpack(dstElem, srcElem, elemType) + if isArrayType(elemType): + return emitArrayUnpack(dstElem, srcElem, elemType) + if isTupleType(elemType): + return emitTupleUnpack(dstElem, srcElem, elemType) + if isSeqType(elemType): + rejectNestedSeq(elemType) + newAssignment(dstElem, srcElem) + +proc emitElemFree(elemAccess, elemType: NimNode): NimNode = + ## Free one value, or `nnkEmpty` for POD. + if isStringType(elemType): + return newCall(ident("cwireFreeStr"), elemAccess) + if isNestedFFIType(elemType): + return newCall(ident("cwireFree"), elemAccess) + if isOptionType(elemType): + return emitOptionFree(elemAccess, elemType) + if isArrayType(elemType): + return emitArrayFree(elemAccess, elemType) + if isTupleType(elemType): + return emitTupleFree(elemAccess, elemType) + if isSeqType(elemType): + rejectNestedSeq(elemType) + newEmptyNode() + +proc maybeStmt(n: NimNode): NimNode = + ## `n` as a one-statement list, empty list when `nnkEmpty`. + if n.kind == nnkEmpty: + return newStmtList() + newStmtList(n) + +proc indexLoop(access, idx, body: NimNode): NimNode = + ## `for in low(access) .. high(access): body` (covers non-0-based ranges). + nnkForStmt.newTree( + idx, + nnkInfix.newTree( + ident(".."), newCall(ident("low"), access), newCall(ident("high"), access) + ), + newStmtList(body), + ) + +proc emitArrayPack(dstAccess, srcAccess, arrType: NimNode): NimNode = + ## Pack a fixed `array[N, T]` element-by-element into the inline wire array. + let idx = genSym(nskForVar, "i") + let body = emitElemPack( + nnkBracketExpr.newTree(dstAccess, idx), + nnkBracketExpr.newTree(srcAccess, idx), + arrType[2], + ) + indexLoop(srcAccess, idx, body) + +proc emitArrayUnpack(dstAccess, srcAccess, arrType: NimNode): NimNode = + ## Inverse of `emitArrayPack`. + let idx = genSym(nskForVar, "i") + let body = emitElemUnpack( + nnkBracketExpr.newTree(dstAccess, idx), + nnkBracketExpr.newTree(srcAccess, idx), + arrType[2], + ) + indexLoop(srcAccess, idx, body) + +proc emitArrayFree(dstAccess, arrType: NimNode): NimNode = + ## Free each array element; `nnkEmpty` when the element owns nothing. + if not cwireNeedsFree(arrType[2]): + return newEmptyNode() + let idx = genSym(nskForVar, "i") + let body = emitElemFree(nnkBracketExpr.newTree(dstAccess, idx), arrType[2]) + indexLoop(dstAccess, idx, body) + +proc emitTuplePack(dstAccess, srcAccess, tupType: NimNode): NimNode = + ## Pack each named tuple component into the matching wire component. + let body = newStmtList() + for c in tupleComponents(tupType): + let nm = ident(c.name) + body.add(emitElemPack(newDotExpr(dstAccess, nm), newDotExpr(srcAccess, nm), c.typ)) + body + +proc emitTupleUnpack(dstAccess, srcAccess, tupType: NimNode): NimNode = + ## Inverse of `emitTuplePack`. + let body = newStmtList() + for c in tupleComponents(tupType): + let nm = ident(c.name) + body.add( + emitElemUnpack(newDotExpr(dstAccess, nm), newDotExpr(srcAccess, nm), c.typ) + ) + body + +proc emitTupleFree(dstAccess, tupType: NimNode): NimNode = + ## Free each tuple component that owns allocations. + if not cwireNeedsFree(tupType): + return newEmptyNode() + let body = newStmtList() + for c in tupleComponents(tupType): + body.add(maybeStmt(emitElemFree(newDotExpr(dstAccess, ident(c.name)), c.typ))) + body + +proc emitSeqPack(dstObj, srcAccess, fieldNameIdent, userType: NimNode): NimNode = + ## Pack a seq into an `allocShared` `UncheckedArray`; empty = nil items + 0 len. + let elemType = userType[1] + let wireElem = wireValueType(elemType) + let items = seqItemsField(dstObj, fieldNameIdent) + let count = seqLenField(dstObj, fieldNameIdent) + let bufType = + nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("UncheckedArray"), wireElem)) + let idx = genSym(nskForVar, "i") + let elemPack = emitElemPack( + nnkBracketExpr.newTree(items, idx), nnkBracketExpr.newTree(srcAccess, idx), elemType + ) + let forLoop = nnkForStmt.newTree( + idx, + nnkInfix.newTree( + ident("..<"), newLit(0), newCall(newDotExpr(srcAccess, ident("len"))) + ), + newStmtList(elemPack), + ) + quote: + if `srcAccess`.len() == 0: + `items` = nil + `count` = 0 + else: + `items` = cast[`bufType`](cwireAllocBuf(sizeof(`wireElem`) * `srcAccess`.len())) + `forLoop` + `count` = `srcAccess`.len() + +proc emitOptionPack(dstAccess, srcAccess, userType: NimNode): NimNode = + ## Pack an Option into a `ptr`: some → `cwireAllocBuf` box, none → nil. Payload + ## read into a local once so a composite inner type isn't re-`get()` per element. + let innerType = userType[1] + let wireInner = wireValueType(innerType) + let bufType = nnkPtrTy.newTree(wireInner) + let innerVal = genSym(nskLet, "innerVal") + let elemPack = emitElemPack(nnkBracketExpr.newTree(dstAccess), innerVal, innerType) + quote: + if `srcAccess`.isSome(): + `dstAccess` = cast[`bufType`](cwireAllocBuf(sizeof(`wireInner`))) + let `innerVal` = `srcAccess`.get() + `elemPack` + else: + `dstAccess` = nil + +proc emitPackStmt(dstObj, srcObj, fieldNameIdent, userType: NimNode): seq[NimNode] = + ## Populate `dstObj.` from `srcObj.`. + let srcAccess = newDotExpr(srcObj, fieldNameIdent) + let dstAccess = newDotExpr(dstObj, fieldNameIdent) + if isSeqType(userType): + return @[emitSeqPack(dstObj, srcAccess, fieldNameIdent, userType)] + @[emitElemPack(dstAccess, srcAccess, userType)] + +proc emitSeqUnpack(dstAccess, srcObj, fieldNameIdent, userType: NimNode): NimNode = + ## Rebuild a Nim seq from the `_items`/`_len` wire pair. + let elemType = userType[1] + let items = seqItemsField(srcObj, fieldNameIdent) + let count = seqLenField(srcObj, fieldNameIdent) + let elemVar = genSym(nskVar, "elem") + let idx = genSym(nskForVar, "i") + let elemUnpack = emitElemUnpack(elemVar, nnkBracketExpr.newTree(items, idx), elemType) + quote: + `dstAccess` = @[] + for `idx` in 0 ..< `count`: + var `elemVar`: `elemType` + `elemUnpack` + `dstAccess`.add(`elemVar`) + +proc emitOptionUnpack(dstAccess, srcAccess, userType: NimNode): NimNode = + ## Rebuild an Option from a wire `ptr`: nil → none, else unpack the pointee. + let innerType = userType[1] + let elemVar = genSym(nskVar, "innerVal") + let elemUnpack = emitElemUnpack(elemVar, nnkBracketExpr.newTree(srcAccess), innerType) + quote: + if `srcAccess`.isNil(): + `dstAccess` = none(`innerType`) + else: + var `elemVar`: `innerType` + `elemUnpack` + `dstAccess` = some(`elemVar`) + +proc emitUnpackStmt( + resultObj, srcObj, fieldNameIdent, userType: NimNode +): seq[NimNode] = + ## Fill `resultObj.` from `srcObj.`. + let srcAccess = newDotExpr(srcObj, fieldNameIdent) + let dstAccess = newDotExpr(resultObj, fieldNameIdent) + if isSeqType(userType): + return @[emitSeqUnpack(dstAccess, srcObj, fieldNameIdent, userType)] + @[emitElemUnpack(dstAccess, srcAccess, userType)] + +proc emitSeqFree(dstObj, fieldNameIdent, userType: NimNode): NimNode = + ## Free a seq field: each element (skipped for POD), then the shared buffer. + let elemType = userType[1] + let items = seqItemsField(dstObj, fieldNameIdent) + let count = seqLenField(dstObj, fieldNameIdent) + let idx = genSym(nskForVar, "i") + let elemFree = emitElemFree(nnkBracketExpr.newTree(items, idx), elemType) + let freeLoop = + if elemFree.kind == nnkEmpty: + newStmtList() + else: + newStmtList( + nnkForStmt.newTree( + idx, nnkInfix.newTree(ident("..<"), newLit(0), count), newStmtList(elemFree) + ) + ) + quote: + if not `items`.isNil(): + `freeLoop` + cwireFreeBuf(`items`) + `items` = nil + `count` = 0 + +proc emitOptionFree(dstAccess, userType: NimNode): NimNode = + ## Free an Option field: the pointee (skipped for POD), then the box. + let innerType = userType[1] + let freeInner = maybeStmt(emitElemFree(nnkBracketExpr.newTree(dstAccess), innerType)) + quote: + if not `dstAccess`.isNil(): + `freeInner` + cwireFreeBuf(`dstAccess`) + `dstAccess` = nil + +proc emitFreeStmt(dstObj, fieldNameIdent, userType: NimNode): seq[NimNode] = + ## Release `dstObj.`: free cstrings/arrays/pointers; POD frees nothing. + let dstAccess = newDotExpr(dstObj, fieldNameIdent) + if isSeqType(userType): + return @[emitSeqFree(dstObj, fieldNameIdent, userType)] + let elemFree = emitElemFree(dstAccess, userType) + if elemFree.kind == nnkEmpty: + return @[] + @[elemFree] + +proc buildCWireProcs( + userTypeName: string, fieldNames: seq[string], fieldTypes: seq[NimNode] +): seq[NimNode] = + ## Generate public cwirePack / cwireUnpack / cwireFree procs for `userTypeName`. + let userName = ident(userTypeName) + let wireName = ident(cwireTypeName(userTypeName)) + + let packDst = ident("dst") + let packSrc = ident("src") + var packBody = newStmtList() + for i in 0 ..< fieldNames.len: + let fIdent = ident(fieldNames[i]) + for s in emitPackStmt(packDst, packSrc, fIdent, fieldTypes[i]): + packBody.add(s) + if fieldNames.len == 0: + packBody.add quote do: + discard + let packProc = newProc( + name = postfix(ident("cwirePack"), "*"), + params = @[ + newEmptyNode(), + newIdentDefs(packDst, nnkVarTy.newTree(wireName)), + newIdentDefs(packSrc, userName), + ], + body = packBody, + ) + + let unpSrc = ident("src") + let unpRes = ident("res") + var unpBody = newStmtList() + unpBody.add quote do: + var `unpRes`: `userName` + for i in 0 ..< fieldNames.len: + let fIdent = ident(fieldNames[i]) + for s in emitUnpackStmt(unpRes, unpSrc, fIdent, fieldTypes[i]): + unpBody.add(s) + unpBody.add quote do: + return `unpRes` + let unpProc = newProc( + name = postfix(ident("cwireUnpack"), "*"), + params = @[userName, newIdentDefs(unpSrc, wireName)], + body = unpBody, + ) + + let freeDst = ident("dst") + var freeBody = newStmtList() + for i in 0 ..< fieldNames.len: + let fIdent = ident(fieldNames[i]) + for s in emitFreeStmt(freeDst, fIdent, fieldTypes[i]): + freeBody.add(s) + if freeBody.len == 0: + freeBody.add quote do: + discard + let freeProc = newProc( + name = postfix(ident("cwireFree"), "*"), + params = @[newEmptyNode(), newIdentDefs(freeDst, nnkVarTy.newTree(wireName))], + body = freeBody, + ) + + @[packProc, unpProc, freeProc] + +proc fieldInfoForType( + typeName: string +): tuple[names: seq[string], types: seq[NimNode]] {.compileTime.} = + ## Look up an ffi type's fields from the registry, parsing each recorded type. + for typeMeta in ffiTypeRegistry: + if typeMeta.name != typeName: + continue + var names: seq[string] = @[] + var types: seq[NimNode] = @[] + for f in typeMeta.fields: + names.add(f.name) + types.add(parseExpr(f.typeName)) + return (names, types) + error("fieldInfoForType: ffi type '" & typeName & "' not in registry") + +proc collectNestedFFITypes( + fieldTypes: seq[NimNode], deps: var seq[string] +) {.compileTime.} = + ## Append (deduped) nested ffi type names in `fieldTypes`, recursing through + ## `seq`/`Option`/`array`/`tuple`. + for t in fieldTypes: + if isNestedFFIType(t): + let n = $t + if n notin deps: + deps.add(n) + elif isSeqType(t) or isOptionType(t): + collectNestedFFITypes(@[t[1]], deps) + elif isArrayType(t): + collectNestedFFITypes(@[t[2]], deps) + elif isTupleType(t): + for c in tupleComponents(t): + collectNestedFFITypes(@[c.typ], deps) + +proc ensureCWireFor(typeName: string, sink: NimNode) {.compileTime.} = + ## Idempotent: append `typeName`'s cwire companion + procs to `sink` if not yet + ## emitted. Nested ffi deps are ensured first so the AST is self-contained. + if isCWireEmitted(typeName): + return + let info = fieldInfoForType(typeName) + var deps: seq[string] = @[] + collectNestedFFITypes(info.types, deps) + for dep in deps: + ensureCWireFor(dep, sink) + markCWireEmitted(typeName) + let section = newNimNode(nnkTypeSection) + section.add(buildCWireTypeDef(typeName, info.names, info.types)) + sink.add(section) + for p in buildCWireProcs(typeName, info.names, info.types): + sink.add(p) + +proc flushCWireCompanions*(): NimNode {.compileTime.} = + ## Emit the `_CWire` companion + procs for every registered `abi = c` type. + let sink = newStmtList() + for typeMeta in ffiTypeRegistry: + if typeMeta.abiFormat == ABIFormat.C: + ensureCWireFor(typeMeta.name, sink) + sink + +## abi = c proc dispatch. The foreign surface is CBOR-free (the `_CWire` structs are +## the C ABI) but transport reuses the CBOR request path internally. Emitted at +## `genBindings()` time (after `flushCWireCompanions`) so the companions are in scope. + +type + CAbiKind = enum + cakMethod + cakCtor + cakStatic + + CAbiSpec = object + kind: CAbiKind + exportName: string ## snake_case C symbol, e.g. "echo_shout" + libType: NimNode ## library value type, e.g. `Echo` + envelope: NimNode ## per-proc Req type, e.g. `EchoShoutReq` + paramNames: seq[string] ## envelope field names (the extra params) + paramTypes: seq[NimNode] ## envelope field types + respType: NimNode ## method result T; empty for a ctor + handler: NimNode + ## FFI-thread handler, deferred here so it lands after the `_CWire` + ## companions it packs/unpacks through. + +var cAbiSpecs {.compileTime.}: seq[CAbiSpec] + +proc copyTypes(types: seq[NimNode]): seq[NimNode] {.compileTime.} = + var res: seq[NimNode] = @[] + for t in types: + res.add(t.copyNimTree()) + res + +proc registerCAbiProc*( + isStatic: bool, + exportName: string, + libType, envelope: NimNode, + paramNames: seq[string], + paramTypes: seq[NimNode], + respType, handler: NimNode, +) {.compileTime.} = + ## Record an `abi = c` method (or `{.ffiStatic.}` proc) for `flushCAbiDispatch`. + ## Nodes are `copyNimTree` frozen: reusing the Req section's originals (bound to + ## `nnkSym`) would ICE. + cAbiSpecs.add( + CAbiSpec( + kind: if isStatic: cakStatic else: cakMethod, + exportName: exportName, + libType: libType.copyNimTree(), + envelope: envelope.copyNimTree(), + paramNames: paramNames, + paramTypes: copyTypes(paramTypes), + respType: respType.copyNimTree(), + handler: handler.copyNimTree(), + ) + ) + +proc registerCAbiCtor*( + exportName: string, + libType, envelope: NimNode, + paramNames: seq[string], + paramTypes: seq[NimNode], + handler: NimNode, +) {.compileTime.} = + ## Record an `abi = c` ctor for `flushCAbiDispatch`; see `registerCAbiProc` + ## for why nodes are `copyNimTree` frozen. + cAbiSpecs.add( + CAbiSpec( + kind: cakCtor, + exportName: exportName, + libType: libType.copyNimTree(), + envelope: envelope.copyNimTree(), + paramNames: paramNames, + paramTypes: copyTypes(paramTypes), + respType: newEmptyNode(), + handler: handler.copyNimTree(), + ) + ) + +proc cdeclReplyPragma(): NimNode = + nnkPragma.newTree( + ident("cdecl"), + ident("gcsafe"), + nnkExprColonExpr.newTree(ident("raises"), nnkBracket.newTree()), + ) + +proc cAbiCbType(replyType: NimNode): NimNode = + ## The caller's typed reply callback proc type. + let fp = nnkFormalParams.newTree( + newEmptyNode(), + newIdentDefs(ident("err"), ident("cint")), + newIdentDefs(ident("reply"), replyType), + newIdentDefs(ident("errMsg"), ident("cstring")), + newIdentDefs(ident("ud"), ident("pointer")), + ) + nnkProcTy.newTree(fp, cdeclReplyPragma()) + +proc boxTypeDef(boxName, cbType: NimNode): NimNode = + ## Box object holding the caller's callback + user data across the thread hop. + let recList = nnkRecList.newTree( + newIdentDefs(ident("fn"), cbType), newIdentDefs(ident("ud"), ident("pointer")) + ) + let objTy = nnkObjectTy.newTree(newEmptyNode(), newEmptyNode(), recList) + nnkTypeSection.newTree(nnkTypeDef.newTree(boxName, newEmptyNode(), objTy)) + +proc replyTrampProc(trampName, body: NimNode): NimNode = + ## `FFICallBack`-shaped proc: runs on the FFI thread, converts the reply, frees the box. + newProc( + name = trampName, + params = @[ + newEmptyNode(), + newIdentDefs(ident("ret"), ident("cint")), + newIdentDefs(ident("msg"), nnkPtrTy.newTree(ident("cchar"))), + newIdentDefs(ident("len"), ident("csize_t")), + newIdentDefs(ident("ud"), ident("pointer")), + ], + body = body, + pragmas = cdeclReplyPragma(), + ) + +proc objectTrampBody(boxName, respWire: NimNode): NimNode = + ## Reply trampoline for an object return: the payload is already the packed + ## `_CWire` image, so hand its address straight to the caller and release the + ## buffers it owns. `reply` is nil only on error. + quote: + let box = cast[ptr `boxName`](ud) + if box.isNil(): + return + if ret == RET_STALE_WARN: + # Non-terminal progress signal: keep the box, don't read the payload. + return + defer: + freeBox(box) + if box.fn.isNil(): + return + try: + if ret != RET_OK: + var em = newString(int(len)) + if int(len) > 0: + copyMem(addr em[0], msg, int(len)) + box.fn(ret, nil, em.cstring, box.ud) + return + if msg.isNil() or int(len) != sizeof(`respWire`): + box.fn(RET_ERR, nil, "abi = c reply: unexpected payload size".cstring, box.ud) + return + var wire = cast[ptr `respWire`](msg)[] + box.fn(RET_OK, addr wire, "".cstring, box.ud) + cwireFree(wire) + except CatchableError as e: + box.fn(RET_ERR, nil, e.msg.cstring, box.ud) + +proc stringTrampBody(boxName: NimNode): NimNode = + ## Reply trampoline for a `string` return (and the ctor's address string): the + ## payload is raw length-delimited UTF-8, so copy it into a NUL-terminated + ## `cstring`. Whichever of reply/error is unused rides as empty, safe to deref. + quote: + let box = cast[ptr `boxName`](ud) + if box.isNil(): + return + if ret == RET_STALE_WARN: + # Non-terminal progress signal: keep the box, don't read the payload. + return + defer: + freeBox(box) + if box.fn.isNil(): + return + try: + var payload = newString(int(len)) + if int(len) > 0 and not msg.isNil(): + copyMem(addr payload[0], msg, int(len)) + if ret != RET_OK: + box.fn(ret, "".cstring, payload.cstring, box.ud) + return + box.fn(RET_OK, payload.cstring, "".cstring, box.ud) + except CatchableError as e: + box.fn(RET_ERR, "".cstring, e.msg.cstring, box.ud) + +proc ctxBindingGuard( + poolIdent, emptyReply, ctxIdent: NimNode, isStatic: bool +): NimNode {.compileTime.} = + ## Prologue that binds `ctxIdent`: a method validates the ctx it was handed, a + ## static resolves the library's shared one. + # + # Any call can be a host thread's first entry. The body allocates via the GC + # on the calling thread, so register it first; initializeLibrary is idempotent. + # Raw AST: `when declared` of an undeclared symbol inside `quote` ICEs. + let initGuard = nnkWhenStmt.newTree( + nnkElifBranch.newTree( + newCall(ident("declared"), ident("initializeLibrary")), + newStmtList(newCall(ident("initializeLibrary"))), + ) + ) + if not isStatic: + let methodGuard = quote: + if onReply.isNil(): + return RET_MISSING_CALLBACK + if not `poolIdent`.isValidCtx(cast[pointer](`ctxIdent`)): + onReply( + RET_ERR, `emptyReply`, "ctx is not a valid FFI context".cstring, userData + ) + return RET_ERR + methodGuard.insert(0, initGuard) + return methodGuard + let guard = quote: + if onReply.isNil(): + return RET_MISSING_CALLBACK + let `ctxIdent` = `poolIdent`.staticFFIContext().valueOr: + let errStr = "ffiStatic: " & error + onReply(RET_ERR, `emptyReply`, errStr.cstring, userData) + return RET_ERR + guard.insert(0, initGuard) + guard + +proc exportedProc( + spec: CAbiSpec, + boxName, envWire, trampName, poolIdent, cbType: NimNode, + isStatic: bool, +): NimNode = + # `cwireUnpack`/`cwirePack` alloc on the calling thread; `ctxBindingGuard` + # registered it. No teardown: it would free the heap of a host thread still + # calling in. A host thread that exits leaks its heap; accepted. + let envName = spec.envelope + let ctxIdent = ident("ctx") + # String reply: empty non-nil cstring on error; object reply: nil ptr gated by err_code. + let emptyReply = + if isStringType(spec.respType): + newDotExpr(newLit(""), ident("cstring")) + else: + newNilLit() + let body = quote: + var ownedWire: `envWire` + cwirePack(ownedWire, cwireUnpack(req[])) + let ownedCopy = cwireOwnedCopy(ownedWire) + if ownedCopy.isNil(): + cwireFree(ownedWire) + onReply(RET_ERR, `emptyReply`, "out of memory".cstring, userData) + return RET_ERR + let reqBuf = cast[ptr UncheckedArray[byte]](ownedCopy) + let box = cast[ptr `boxName`](allocBox(sizeof(`boxName`))) + box.fn = onReply + box.ud = userData + let typeStr = $`envName` + let reqPtr = FFIThreadRequest.initFromOwnedShared( + `trampName`, box, typeStr.cstring, reqBuf, sizeof(`envWire`), rawReply = true + ) + let sendRes = + try: + ffi_context.sendRequestToFFIThread(`ctxIdent`, reqPtr) + except Exception as e: + Result[void, string].err("sendRequestToFFIThread exception: " & e.msg) + if sendRes.isErr(): + # A rejected send already `deleteRequest`ed the struct copy, which frees only + # the struct itself; `ownedWire` still aliases its field buffers, so free them + # here — on success the FFI thread's unpack does it instead. + cwireFree(ownedWire) + onReply(RET_ERR, `emptyReply`, sendRes.error.cstring, userData) + return RET_ERR + return RET_OK + + let fullBody = ctxBindingGuard(poolIdent, emptyReply, ctxIdent, isStatic) + for stmt in body: + fullBody.add(stmt) + + var params = @[ + ident("cint"), + newIdentDefs(ident("onReply"), cbType), + newIdentDefs(ident("userData"), ident("pointer")), + newIdentDefs(ident("req"), nnkPtrTy.newTree(envWire)), + ] + if not isStatic: + let libFFICtx = + nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), spec.libType)) + params.insert(newIdentDefs(ctxIdent, libFFICtx), 1) + + newProc( + name = ident($envName & "CAbiExport"), + params = params, + body = fullBody, + pragmas = nnkPragma.newTree( + ident("dynlib"), + nnkExprColonExpr.newTree(ident("exportc"), newStrLitNode(spec.exportName)), + ident("cdecl"), + nnkExprColonExpr.newTree(ident("raises"), nnkBracket.newTree()), + ), + ) + +proc exportedCtorProc( + spec: CAbiSpec, boxName, envWire, trampName, poolIdent, cbType: NimNode +): NimNode = + let envName = spec.envelope + # No `foreignThreadGc` (see exportedMethodProc). initGuard is built as raw AST because a `when declared` over an undeclared symbol inside `quote` ICEs. + let initGuard = nnkWhenStmt.newTree( + nnkElifBranch.newTree( + newCall(ident("declared"), ident("initializeLibrary")), + newStmtList(newCall(ident("initializeLibrary"))), + ) + ) + let body = quote: + let ctxRes = `poolIdent`.createFFIContext() + if ctxRes.isErr(): + if not onCreated.isNil(): + onCreated( + RET_ERR, + "".cstring, + ("ffiCtor: failed to create FFIContext: " & $ctxRes.error).cstring, + userData, + ) + return nil + let ctx = ctxRes.get() + var ownedWire: `envWire` + cwirePack(ownedWire, cwireUnpack(req[])) + let ownedCopy = cwireOwnedCopy(ownedWire) + if ownedCopy.isNil(): + cwireFree(ownedWire) + if not onCreated.isNil(): + onCreated(RET_ERR, "".cstring, "out of memory".cstring, userData) + return nil + let reqBuf = cast[ptr UncheckedArray[byte]](ownedCopy) + let box = cast[ptr `boxName`](allocBox(sizeof(`boxName`))) + box.fn = onCreated + box.ud = userData + let typeStr = $`envName` + let reqPtr = FFIThreadRequest.initFromOwnedShared( + `trampName`, box, typeStr.cstring, reqBuf, sizeof(`envWire`), rawReply = true + ) + let sendRes = + try: + ctx.sendRequestToFFIThread(reqPtr) + except Exception as e: + Result[void, string].err("sendRequestToFFIThread exception: " & e.msg) + if sendRes.isErr(): + # See exportedMethodProc: the rejected send freed the struct copy, not the + # field buffers `ownedWire` still aliases. + cwireFree(ownedWire) + if not onCreated.isNil(): + onCreated(RET_ERR, "".cstring, sendRes.error.cstring, userData) + return nil + return cast[pointer](ctx) + body.insert(0, initGuard) + newProc( + name = ident($envName & "CAbiExport"), + params = @[ + ident("pointer"), + newIdentDefs(ident("req"), nnkPtrTy.newTree(envWire)), + newIdentDefs(ident("onCreated"), cbType), + newIdentDefs(ident("userData"), ident("pointer")), + ], + body = body, + pragmas = nnkPragma.newTree( + ident("dynlib"), + nnkExprColonExpr.newTree(ident("exportc"), newStrLitNode(spec.exportName)), + ident("cdecl"), + nnkExprColonExpr.newTree(ident("raises"), nnkBracket.newTree()), + ), + ) + +proc ensureCWireForFields( + sink: NimNode, typeName: string, names: seq[string], types: seq[NimNode] +) {.compileTime.} = + ## Emit the `_CWire` companion + procs for a synthetic per-proc Req envelope + ## (not a user `{.ffi.}` type, so not in `ffiTypeRegistry`). + if isCWireEmitted(typeName): + return + var deps: seq[string] = @[] + collectNestedFFITypes(types, deps) + for dep in deps: + ensureCWireFor(dep, sink) + markCWireEmitted(typeName) + let section = newNimNode(nnkTypeSection) + section.add(buildCWireTypeDef(typeName, names, types)) + sink.add(section) + for p in buildCWireProcs(typeName, names, types): + sink.add(p) + +proc flushCAbiDispatch*(): NimNode {.compileTime.} = + ## Emit the exported wrappers + reply trampolines for every registered + ## `abi = c` proc. Runs after `flushCWireCompanions`. + let sink = newStmtList() + for spec in cAbiSpecs: + let envName = spec.envelope + ensureCWireForFields(sink, $envName, spec.paramNames, spec.paramTypes) + sink.add(spec.handler) + let envWire = ident(cwireTypeName($envName)) + let boxName = ident($envName & "CBox") + let trampName = ident($envName & "CReply") + let poolIdent = ident($spec.libType & "FFIPool") + case spec.kind + of cakCtor: + let cbType = cAbiCbType(ident("cstring")) + sink.add(boxTypeDef(boxName, cbType)) + sink.add(replyTrampProc(trampName, stringTrampBody(boxName))) + sink.add(exportedCtorProc(spec, boxName, envWire, trampName, poolIdent, cbType)) + of cakMethod, cakStatic: + let isStatic = spec.kind == cakStatic + let rt = spec.respType + if isStringType(rt): + let cbType = cAbiCbType(ident("cstring")) + sink.add(boxTypeDef(boxName, cbType)) + sink.add(replyTrampProc(trampName, stringTrampBody(boxName))) + sink.add( + exportedProc(spec, boxName, envWire, trampName, poolIdent, cbType, isStatic) + ) + # `isKnownFFIType`, not just `nnkIdent`: a bare `int` is an ident too, and + # would otherwise reach for a `int_CWire` companion that is never emitted. + elif rt.kind == nnkIdent and isKnownFFIType($rt): + let respWire = ident(cwireTypeName($rt)) + let cbType = cAbiCbType(nnkPtrTy.newTree(respWire)) + sink.add(boxTypeDef(boxName, cbType)) + sink.add(replyTrampProc(trampName, objectTrampBody(boxName, respWire))) + sink.add( + exportedProc(spec, boxName, envWire, trampName, poolIdent, cbType, isStatic) + ) + else: + error( + "abi = c: unsupported response type for proc '" & spec.exportName & "': " & + rt.repr & " — reply with a `string` or an `{.ffi.}` object type. " & + "A scalar return is wired only for an all-scalar `{.ffi.}` method." + ) + sink diff --git a/wasm-deps/ffi/ffi/internal/c_wire.nim b/wasm-deps/ffi/ffi/internal/c_wire.nim new file mode 100644 index 000000000..bfadc78db --- /dev/null +++ b/wasm-deps/ffi/ffi/internal/c_wire.nim @@ -0,0 +1,44 @@ +## Memory helpers for the macro-generated `*_CWire` types — the flat C-ABI mirror +## of a Nim object, where strings and seq/Option payloads live in separate buffers +## the struct only points at. These procs allocate and free those buffers, and copy +## the struct across the hop to the FFI thread. + +import ../alloc + +proc cwireAllocBuf*(size: int): pointer = + ## Buffer for a wire seq/Option payload. libc `malloc` rather than `allocShared` + ## so one thread can allocate and a different thread can free (see ../alloc). + alloc.allocBox(size) + +proc cwireFreeBuf*(p: pointer) = + ## Frees a `cwireAllocBuf` buffer; does nothing if `p` is nil. + alloc.freeBox(p) + +proc cwireAllocStr*(s: string): cstring {.inline.} = + ## NUL-terminated copy of `s` for a wire string field; free with `cwireFreeStr`. + alloc.alloc(s) + +proc cwireFreeStr*(s: var cstring) {.inline.} = + ## Frees a wire string field and nils it, so freeing twice is harmless. + if s.isNil(): + return + alloc.dealloc(s) + s = nil + +func cwireStructBytes*[W](wire: W): seq[byte] = + ## The struct's raw bytes, to hand a reply back over the FFI-thread hop. Copies + ## the pointers, not what they point at, so `wire`'s buffers must stay alive + ## until the receiver `cwireFree`s them. + var b = newSeq[byte](sizeof(W)) + copyMem(addr b[0], unsafeAddr wire, sizeof(W)) + b + +proc cwireOwnedCopy*[W](wire: W): ptr W = + ## The same shallow copy, into `malloc` memory the FFI thread adopts and frees; + ## nil if the allocation fails. `copyMem` because assigning into raw `malloc` + ## bytes would run ORC's copy hooks over uninitialised memory. + let p = cast[ptr W](alloc.allocBox(sizeof(W))) + if p.isNil(): + return nil + copyMem(p, unsafeAddr wire, sizeof(W)) + return p diff --git a/wasm-deps/ffi/ffi/internal/ffi_codegen_common.nim b/wasm-deps/ffi/ffi/internal/ffi_codegen_common.nim new file mode 100644 index 000000000..0dd924261 --- /dev/null +++ b/wasm-deps/ffi/ffi/internal/ffi_codegen_common.nim @@ -0,0 +1,29 @@ +## Compile-time pieces that more than one `{.ffi.}` codegen path shares. + +import std/macros + +func unwrapPostfix*(n: NimNode): NimNode = + ## Strips the `*` of an exported name, so a name node reads the same whether + ## the writer exported it or not. + return + if n.kind == nnkPostfix: + n[1] + else: + n + +func procIdent*(prc: NimNode): NimNode = + return unwrapPostfix(prc[0]) + +proc buildLibReadyGuard*( + ctxHandlerName, libTypeName: NimNode +): NimNode {.compileTime.} = + ## Rejects a request that reaches the FFI thread before the ctor stores a + ## library. The guard applies only to a `ref` type. For an `object` type the + ## fallback is a usable zero value, but for a `ref` type it is nil. The guard + ## runs in the handler, behind the ctor in the queue. Thus a host can send a + ## call before it waits for the create callback. + quote: + when `libTypeName` is ref: + if not `ctxHandlerName`[].libReady.load(): + return + err("library is not initialized: the constructor failed or has not run yet") diff --git a/wasm-deps/ffi/ffi/internal/ffi_export.nim b/wasm-deps/ffi/ffi/internal/ffi_export.nim new file mode 100644 index 000000000..fa7567e09 --- /dev/null +++ b/wasm-deps/ffi/ffi/internal/ffi_export.nim @@ -0,0 +1,169 @@ +## Simple synchronous C export for a nim-ffi library. +## +## `{.ffi.}` and `{.ffiCtor.}` give the async path. That path uses a context +## handle and encodes the data with CBOR. It fits a library that keeps state +## across many calls. `{.ffiExport.}` covers the other common case: a few simple +## lifecycle entry points. The host loads them with `dlopen` and `dlsym`, then +## calls them synchronously. There is no context, no callback and no CBOR. The +## return value of the function crosses the ABI directly. +## +## Write native Nim types. `ffiExport` maps them to the C ABI: +## int / int64 -> C long long +## int32 / bool -> C int +## uint / uint64 -> C unsigned long long +## float -> C double +## string -> C const char* (valid until the same thread calls again) +## (no return) -> C void +## A return type with no mapping is a compile error. +## The `const char*` buffer belongs to the calling thread. Copy the bytes before +## that thread calls another `{.ffiExport.}` proc that returns a string. +## `ffiExport` also injects the `initializeLibrary()` call of the library. The Nim +## runtime therefore starts on the first call, and the host never calls NimMain. +## The wrapper catches every exception of the body, prints it and returns the +## zero value, because an exception must not cross the C ABI. +## +## declareLibraryBase("myLib") # emits initializeLibrary() +## proc my_start(): int {.ffiExport.} = 0 # -> long long 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`. A proc with +## `{.ffiExport.}` takes no arguments. For a call with arguments, use `{.ffi.}`. + +import std/macros +import ./ffi_route +import ./ffi_codegen_common + +const passthroughCTypes = [ + "cint", "cuint", "clong", "culong", "clonglong", "culonglong", "cfloat", "cdouble", + "cstring", "pointer", +] + +proc cReturnType(t: NimNode, exportName: string): NimNode = + ## Maps the native Nim return type to the C ABI type that crosses the boundary. + ## A type with no mapping is an error: emitting the Nim type as-is would export + ## a symbol whose ABI no host can call. + if t.kind == nnkEmpty: + return t + if t.kind == nnkIdent: + case $t + of "int", "int64": + # `int` is pointer-wide, so `cint` would truncate it on a 64-bit host. + return ident("clonglong") + of "int8", "int16", "int32", "bool": + return ident("cint") + of "uint", "uint64": + return ident("culonglong") + of "uint8", "uint16", "uint32": + return ident("cuint") + of "float", "float64": + return ident("cdouble") + of "float32": + return ident("cfloat") + of "string": + return ident("cstring") + else: + if $t in passthroughCTypes: + return t + error( + "`.ffiExport.` proc " & exportName & " returns " & t.repr & + ", which has no C ABI mapping. Return a scalar, a bool, a string, a C type, " & + "or nothing. For a richer return type, use `{.ffi.}`." + ) + +proc withoutFFIPragmas(pragmas: NimNode): NimNode = + ## Drops only the pragma that routed the proc here, so a `raises` or `gcsafe` + ## the writer asked for still applies to the body. + if pragmas.kind != nnkPragma: + return newEmptyNode() + var kept = nnkPragma.newTree() + for p in pragmas: + let name = + if p.kind in {nnkExprColonExpr, nnkCall}: + p[0] + else: + p + if name.kind == nnkIdent and $name in ["ffi", "ffiExport"]: + continue + kept.add(p) + return + if kept.len == 0: + newEmptyNode() + else: + kept + +proc buildFFIExportProc*(prc: NimNode): NimNode {.compileTime.} = + ## Emits the synchronous C export. `{.ffi.}` and `{.ffiExport.}` share it. + prc.expectKind({nnkProcDef, nnkFuncDef}) + let exportName = $procIdent(prc) + let params = prc.params + let nativeRet = params[0] + let cRet = cReturnType(nativeRet, exportName) + + # The user body becomes a private impl proc that the exported wrapper calls. + let implName = genSym(nskProc, exportName & "Impl") + var impl = copyNimTree(prc) + impl[0] = implName + impl[4] = withoutFFIPragmas(prc[4]) + + let wrapName = ident(exportName) + let boot = quote: + when declared(initializeLibrary): + initializeLibrary() + + # A Nim exception that unwinds through a cdecl frame into the host is + # undefined behaviour, so every wrapper catches and reports instead. + let raiseNote = newLit("error: " & exportName & " raised: ") + + var res = newStmtList(impl) + + if nativeRet.kind == nnkIdent and $nativeRet == "string": + # One buffer per calling thread: a process-wide buffer would let one thread + # free the bytes another thread is still reading. + let buf = genSym(nskVar, exportName & "Buf") + res.add quote do: + var `buf` {.threadvar.}: pointer + proc `wrapName`(): cstring {.exportc: `exportName`, cdecl, dynlib, raises: [].} = + `boot` + var s = "" + try: + s = `implName`() + except CatchableError as e: + echo `raiseNote`, e.msg + if not `buf`.isNil(): + deallocShared(`buf`) + `buf` = allocShared(s.len + 1) + if s.len > 0: + copyMem(`buf`, unsafeAddr s[0], s.len) + cast[ptr UncheckedArray[char]](`buf`)[s.len] = '\0' + return cast[cstring](`buf`) + + elif nativeRet.kind == nnkEmpty: + res.add quote do: + proc `wrapName`() {.exportc: `exportName`, cdecl, dynlib, raises: [].} = + `boot` + try: + `implName`() + except CatchableError as e: + echo `raiseNote`, e.msg + + else: + res.add quote do: + proc `wrapName`(): `cRet` {.exportc: `exportName`, cdecl, dynlib, raises: [].} = + `boot` + try: + return `cRet`(`implName`()) + except CatchableError as e: + echo `raiseNote`, e.msg + return `cRet`(0) + + return res + +macro ffiExport*(prc: untyped): untyped = + ## Marks a proc that takes no arguments as a simple synchronous C export. The + ## macro maps the native Nim return type to the C ABI and starts the Nim + ## runtime. `{.ffi.}` reaches the same path from the shape alone. See the + ## module doc. + prc.expectKind({nnkProcDef, nnkFuncDef}) + assertFFIPath(prc, fpExport) + return buildFFIExportProc(prc) diff --git a/wasm-deps/ffi/ffi/internal/ffi_library.nim b/wasm-deps/ffi/ffi/internal/ffi_library.nim index a9387f769..27c8fd4e6 100644 --- a/wasm-deps/ffi/ffi/internal/ffi_library.nim +++ b/wasm-deps/ffi/ffi/internal/ffi_library.nim @@ -1,9 +1,55 @@ -import std/[macros, atomics], strformat, chronicles, chronos +import + std/[macros, atomics, sysatomics, compilesettings], strformat, chronicles, chronos +import strutils +import ../codegen/meta + +func nimMainPrefixOnCmdLine(cmdLine: string): tuple[found: bool, value: string] = + ## Last `--nimMainPrefix:X` on the command line (style-insensitive match, `:` + ## or `=`); absence isn't proof it was never set (config.nims may not surface). + var found = false + var value = "" + for tok in cmdLine.splitWhitespace(): + let body = tok.strip(trailing = false, chars = {'-'}) + let sep = body.find({':', '='}) + if sep < 0: + continue + if body[0 ..< sep].toLowerAscii().replace("_", "") == "nimmainprefix": + found = true + value = body[sep + 1 .. ^1] + (found, value) + +proc validateNimMainPrefix(libraryName: string) {.compileTime.} = + ## The init symbol is importc'd as `lib{libraryName}NimMain`, so the build must + ## pass `--nimMainPrefix:lib{libraryName}`; a mismatch errors, absence only + ## hints (config.nims may set it) and only under `--app:lib`. + let expectedPrefix = "lib" & libraryName + let (prefixFound, prefixValue) = + nimMainPrefixOnCmdLine(querySetting(SingleValueSetting.commandLine)) + if prefixFound and prefixValue != expectedPrefix: + error( + "declareLibrary(\"" & libraryName & + "\"): the Nim runtime init symbol is importc'd as " & expectedPrefix & + "NimMain, so the build needs --nimMainPrefix:" & expectedPrefix & + ", but the command line passes --nimMainPrefix:" & prefixValue & + ". Change the flag to --nimMainPrefix:" & expectedPrefix & + " (it must be \"lib\" followed by the declareLibrary name)." + ) + elif not prefixFound and compileOption("app", "lib"): + hint( + "declareLibrary(\"" & libraryName & "\"): pass --nimMainPrefix:" & expectedPrefix & + " so the Nim runtime init symbol " & expectedPrefix & + "NimMain resolves; without it the build may fail with an undefined-symbol" & + " link error (ignore this hint if the prefix is set in config.nims)." + ) + +macro declareLibraryBase*(libraryName: static[string]): untyped = + currentLibName = libraryName + + validateNimMainPrefix(libraryName) -macro declareLibrary*(libraryName: static[string]): untyped = var res = newStmtList() - ## Generate {.pragma: exported, exportc, cdecl, raises: [].} + # {.pragma: exported, exportc, cdecl, raises: [].} res.add nnkPragma.newTree( nnkExprColonExpr.newTree(ident"pragma", ident"exported"), ident"exportc", @@ -11,7 +57,7 @@ macro declareLibrary*(libraryName: static[string]): untyped = nnkExprColonExpr.newTree(ident"raises", nnkBracket.newTree()), ) - ## Generate {.pragma: callback, cdecl, raises: [], gcsafe.} + # {.pragma: callback, cdecl, raises: [], gcsafe.} res.add nnkPragma.newTree( nnkExprColonExpr.newTree(ident"pragma", ident"callback"), ident"cdecl", @@ -19,22 +65,26 @@ macro declareLibrary*(libraryName: static[string]): untyped = ident"gcsafe", ) - ## Generate {.passc: "-fPIC".} + # {.passc: "-fPIC".} res.add nnkPragma.newTree(nnkExprColonExpr.newTree(ident"passc", newLit("-fPIC"))) - when defined(linux) and not defined(emscripten): - # NB: under emscripten (--os:linux) a `-Wl,-soname` makes emcc build a wasm - # SIDE module, which breaks EXPORTED_FUNCTIONS/malloc. The wasm/edge build is - # a MAIN module, so skip the soname there. - ## Generates {.passl: "-Wl,-soname,libwaku.so".} (considering libraryName=="waku", for example) - let soName = fmt"-Wl,-soname,lib{libraryName}.so" - res.add( - newNimNode(nnkPragma).add( - nnkExprColonExpr.newTree(ident"passl", newStrLitNode(soName)) + # soname / install_name only apply to a shared library and break an executable link (fatally on macOS), so emit them only under `--app:lib`. + if compileOption("app", "lib"): + when defined(linux): + let soName = fmt"-Wl,-soname,lib{libraryName}.so" + res.add( + newNimNode(nnkPragma).add( + nnkExprColonExpr.newTree(ident"passl", newStrLitNode(soName)) + ) ) - ) - - ## proc lib{libraryName}NimMain() {.importc.} + elif defined(macosx): + let installName = fmt"-install_name @rpath/lib{libraryName}.dylib" + res.add( + newNimNode(nnkPragma).add( + nnkExprColonExpr.newTree(ident"passl", newStrLitNode(installName)) + ) + ) + # proc lib{libraryName}NimMain() {.importc.} let libNimMainName = ident(fmt"lib{libraryName}NimMain") let importcPragma = nnkPragma.newTree(ident"importc") let procDef = newProc( @@ -45,14 +95,14 @@ macro declareLibrary*(libraryName: static[string]): untyped = ) res.add(procDef) - # Create: var initialized: Atomic[bool] - let atomicType = nnkBracketExpr.newTree(ident("Atomic"), ident("bool")) + # initState: 0=not started, 1=in progress, 2=done. Atomic (not a bool) so a racing caller can't skip past the gate mid-init (else Windows WSAStartup fails). + let atomicType = nnkBracketExpr.newTree(ident("Atomic"), ident("int")) let varStmt = nnkVarSection.newTree( - nnkIdentDefs.newTree(ident("initialized"), atomicType, newEmptyNode()) + nnkIdentDefs.newTree(ident("initState"), atomicType, newEmptyNode()) ) res.add(varStmt) - ## Android chronicles redirection + # Android chronicles redirection let chroniclesBlock = quote: when defined(android) and compiles(defaultChroniclesStream.outputs[0].writer): defaultChroniclesStream.outputs[0].writer = proc( @@ -66,12 +116,16 @@ macro declareLibrary*(libraryName: static[string]): untyped = let initializeLibraryProc = quote: proc `procName`*() {.exported.} = - if not initialized.exchange(true): - ## Every Nim library needs to call `NimMain` once exactly, - ## to initialize the Nim runtime. - ## Being `` the value given in the optional - ## compilation flag --nimMainPrefix:yourprefix + ## Calls `NimMain` exactly once to init the Nim runtime. Concurrent + ## callers must block until it returns (its chronos globalInit runs + ## WSAStartup on Windows; racing past yields "WSAStartup failed" later). + var expected: int = 0 + if initState.compareExchange(expected, 1): `nimMainName`() + initState.store(2) + else: + while initState.load() != 2: + cpuRelax() when declared(setupForeignThreadGc): setupForeignThreadGc() when declared(nimGC_setStackBottom): @@ -82,3 +136,107 @@ macro declareLibrary*(libraryName: static[string]): untyped = res.add(initializeLibraryProc) return res + +macro declareLibrary*( + libraryName: static[string], + libType: untyped, + defaultABIFormat: static[string] = "cbor", +): untyped = + ## Declares a library and emits the C-exported event ABI (`_add_event_listener` / + ## `_remove_event_listener`) on its `FFIContext`. `defaultABIFormat` (`"cbor"`/`"c"`) + ## is inherited unless an annotation overrides via `"abi = ..."`. + currentLibType = $libType # so handle-receiver `.ffi.` procs can resolve the pool + + let (abiOk, abiFmt) = parseABIFormatName(defaultABIFormat) + if not abiOk: + error( + "declareLibrary: unknown defaultABIFormat '" & defaultABIFormat & + "'; valid values are \"c\" and \"cbor\"" + ) + currentDefaultABIFormat = abiFmt + libraryDeclared = true + + var stmts = newStmtList() + + stmts.add(newCall(ident("declareLibraryBase"), newStrLitNode(libraryName))) + + # The pool the generated wrappers validate against. + let poolIdent = ident($libType & "FFIPool") + stmts.add quote do: + when not declared(`poolIdent`): + var `poolIdent`*: FFIContextPool[`libType`] + + let ctxType = nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libType)) + let cdeclExportPragma = newTree( + nnkPragma, + ident("dynlib"), + ident("exportc"), + ident("cdecl"), + newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)), + ) + + # {libraryName}_add_event_listener + let addName = libraryName & "_add_event_listener" + let addErr = "error: invalid context in " & addName + let addBody = quote: + # This code runs on the foreign caller thread. That thread can differ from + # the thread of an earlier entry point. If the GC of the thread is not + # ready, the first Nim allocation ($eventName, the registry Table and seq) + # faults. Therefore initialize the GC here. + when declared(initializeLibrary): + initializeLibrary() + var ret: uint64 = 0 + if isNil(ctx): + echo `addErr` + return ret + let evtName = + if eventName.isNil(): + "" + else: + $eventName + ret = addEventListener(ctx[].eventRegistry, evtName, callback, userData) + return ret + + stmts.add( + newProc( + name = ident(addName), + params = @[ + ident("uint64"), + newIdentDefs(ident("ctx"), ctxType), + newIdentDefs(ident("eventName"), ident("cstring")), + newIdentDefs(ident("callback"), ident("FFICallBack")), + newIdentDefs(ident("userData"), ident("pointer")), + ], + body = addBody, + pragmas = cdeclExportPragma, + ) + ) + + # Param is `listenerId`, not `id`: `id` collides with chronos's `futures.id` template under quote injection and the captured symbol wins. + let removeName = libraryName & "_remove_event_listener" + let removeErr = "error: invalid context in " & removeName + let removeBody = quote: + when declared(initializeLibrary): + initializeLibrary() + var ret: cint = 1 + if isNil(ctx): + echo `removeErr` + return ret + if removeEventListener(ctx[].eventRegistry, listenerId): + ret = 0 + return ret + + stmts.add( + newProc( + name = ident(removeName), + params = @[ + ident("cint"), + newIdentDefs(ident("ctx"), ctxType), + newIdentDefs(ident("listenerId"), ident("uint64")), + ], + body = removeBody, + pragmas = cdeclExportPragma, + ) + ) + + return stmts diff --git a/wasm-deps/ffi/ffi/internal/ffi_macro.nim b/wasm-deps/ffi/ffi/internal/ffi_macro.nim index 95e6377d5..d30980ced 100644 --- a/wasm-deps/ffi/ffi/internal/ffi_macro.nim +++ b/wasm-deps/ffi/ffi/internal/ffi_macro.nim @@ -1,174 +1,411 @@ -import std/[macros, tables] +import std/[macros, options, tables, strutils] +from std/os import `/`, relativePath +from std/compilesettings import querySetting, SingleValueSetting import chronos import ../ffi_types +import ../ffi_thread_request +import ../codegen/[meta, string_helpers] +import ./c_macro_helpers +import ./ffi_scalar +import ./ffi_route +import ./ffi_export +import ./ffi_codegen_common +when defined(ffiGenBindings): + import ../codegen/rust + import ../codegen/cpp + import ../codegen/c + import ../codegen/cddl -proc extractFieldsFromLambda(body: NimNode): seq[NimNode] = - ## Extracts the fields (params) from the given lambda body, when using the registerReqFFI macro. - ## e.g., for: - ## registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): - ## proc( - ## configJson: cstring, appCallbacks: AppCallbacks - ## ): Future[Result[string, string]] {.async.} = - ## ... - ## The extracted fields will be: - ## - configJson: cstring - ## - appCallbacks: AppCallbacks - ## +proc requireLibraryDeclared(where: string) {.compileTime.} = + ## Enforce that `declareLibrary(...)` ran before this annotation. + if not libraryDeclared: + error( + where & + ": declareLibrary(name, LibType[, defaultABIFormat]) must be called before any FFI annotation" + ) - var procNode = body - if procNode.kind == nnkStmtList and procNode.len == 1: - procNode = procNode[0] - if procNode.kind != nnkLambda and procNode.kind != nnkProcDef: - error "registerReqFFI expects a lambda proc, found: " & $procNode.kind +proc resolveEventWireName( + leading: seq[NimNode], userProcName: NimNode +): tuple[wireName: string, abiSpecStart: int] {.compileTime.} = + ## A leading string that isn't an `"abi = ..."` spec is the explicit wire name; + ## otherwise derive from the proc. Returns name and index where ABI specs begin. + if leading.len > 0 and leading[0].kind in {nnkStrLit, nnkRStrLit, nnkTripleStrLit} and + ($leading[0]).len > 0 and not parseAbiSpec($leading[0]).ok: + ($leading[0], 1) + else: + (camelToSnakeCase($userProcName), 0) - let params = procNode[3] # parameters list - result = @[] - for p in params[1 .. ^1]: # skip return type - result.add newIdentDefs(p[0], p[1]) +proc requireBeforeGenBindings(where: string) {.compileTime.} = + ## Enforce this annotation expands before `genBindings()`; anything registered + ## afterwards never reaches the generator. + if genBindingsEmitted: + error( + where & + " appears after genBindings(); genBindings() must be the LAST FFI call in the compilation root, after every {.ffi.}/{.ffiCtor.}/{.ffiDtor.}/{.ffiEvent.} annotation" + ) - when defined(ffiDumpMacros): - echo result.repr +proc resolveABIFormat(abiSpecs: seq[NimNode]): ABIFormat {.compileTime.} = + ## Resolve ABI from optional `"abi = ..."` specs (last wins), else lib default. + var fmt = currentDefaultABIFormat + for override in abiSpecs: + if override.kind notin {nnkStrLit, nnkRStrLit, nnkTripleStrLit}: + error( + "FFI ABI override must be a string literal like \"abi = c\", got: " & + override.repr + ) + let parsed = parseAbiSpec($override) + if not parsed.ok: + error(parsed.err) + fmt = parsed.fmt + fmt -proc buildRequestType(reqTypeName: NimNode, body: NimNode): NimNode = - ## Builds: - ## type * = object - ## : - ## ... - ## e.g.: - ## type CreateNodeRequest* = object - ## configJson: cstring - ## appCallbacks: AppCallbacks - ## +proc resolveFFISpecs(specs: seq[NimNode]): ABIFormat {.compileTime.} = + ## Resolve `"abi = ..."` specs (last wins), else the library-default ABI. + var abi = currentDefaultABIFormat + for override in specs: + if override.kind notin {nnkStrLit, nnkRStrLit, nnkTripleStrLit}: + error( + "FFI override must be a string literal like \"abi = c\", got: " & override.repr + ) + case overrideKey($override) + of "abi": + let parsed = parseAbiSpec($override) + if not parsed.ok: + error(parsed.err) + abi = parsed.fmt + else: + error("unknown FFI override '" & $override & "'; expected `abi = ...`") + abi - var procNode = body - if procNode.kind == nnkStmtList and procNode.len == 1: - procNode = procNode[0] - if procNode.kind != nnkLambda and procNode.kind != nnkProcDef: - error "registerReqFFI expects a lambda proc, found: " & $procNode.kind +proc gateABIFormat(fmt: ABIFormat, where: string) {.compileTime.} = + ## Abort if the selected ABI's codegen isn't wired yet, failing loudly. + if not abiCodegenImplemented(fmt): + error( + where & + ": ABI format is recognized but not yet implemented (only 'cbor' currently generates working bindings): " & + $fmt + ) - let params = procNode[3] # formal params of the lambda +proc gateFFITypeABIFormat(fmt: ABIFormat, where: string) {.compileTime.} = + ## Type annotations only register metadata; both ABIs are valid. + case fmt + of ABIFormat.Cbor, ABIFormat.C: discard + +proc isPtr(typ: NimNode): bool = + ## True iff `typ` is a `ptr T` type expression. + typ.kind == nnkPtrTy + +proc rejectRawPtrType(typ: NimNode, where: string) = + ## Reject `pointer`/`ptr T` at macro time: no unvalidatable raw address may + ## cross the FFI boundary (only the framework-managed ctx handle may). `object` + ## and `ref T` are fine — they flow as value copies through cbor_serialization. + if typ.kind == nnkPtrTy: + error( + where & ": raw `ptr T` is not allowed across the FFI boundary " & + "(only the ctx handle, managed by the framework, may be a pointer)" + ) + if typ.kind == nnkIdent and $typ == "pointer": + error( + where & ": raw `pointer` is not allowed across the FFI boundary " & + "(only the ctx handle, managed by the framework, may be a pointer)" + ) + +proc enumWireName(rhs: NimNode, fieldName: string): string {.compileTime.} = + ## What `$value` yields: the associated string if the enum declares one + ## (`cRed = "red"` or `cRed = (3, "red")`), else the symbol name. + case rhs.kind + of nnkStrLit, nnkRStrLit, nnkTripleStrLit: + $rhs + of nnkTupleConstr, nnkPar: + if rhs.len == 2 and rhs[1].kind in {nnkStrLit, nnkRStrLit, nnkTripleStrLit}: + $rhs[1] + else: + fieldName + else: + fieldName + +proc enumValueMetas( + enumTy: NimNode, typeName: string +): seq[FFIEnumValueMeta] {.compileTime.} = + ## Walks an `nnkEnumTy`, resolving each value's wire name and ordinal. + var values: seq[FFIEnumValueMeta] = @[] + var nextOrd = 0 + for child in enumTy: + if child.kind == nnkEmpty: + continue + var name: string + var wire: string + var ordinal = nextOrd + case child.kind + of nnkIdent, nnkSym: + name = $child + wire = name + of nnkEnumFieldDef: + name = $child[0] + wire = enumWireName(child[1], name) + let explicitOrd = + if child[1].kind == nnkIntLit: + some(int(child[1].intVal)) + elif child[1].kind in {nnkTupleConstr, nnkPar} and child[1].len == 2 and + child[1][0].kind == nnkIntLit: + some(int(child[1][0].intVal)) + else: + none(int) + if explicitOrd.isSome(): + ordinal = explicitOrd.get() + else: + error("`.ffi.` enum " & typeName & ": unsupported enum value " & child.repr) + values.add(FFIEnumValueMeta(name: name, wire: wire, ord: ordinal)) + nextOrd = ordinal + 1 + values + +proc registerFFIEnumInfo( + typeDef: NimNode, typeNameStr: string, abiFormat: ABIFormat +) {.compileTime.} = + ## Registers an `{.ffi.}` enum. Only the CBOR wire carries enums; `abi = c` + ## has no representation for them yet, so reject it at the annotation. + if abiFormat == ABIFormat.C: + error( + "`.ffi.` enum " & typeNameStr & + ": `abi = c` does not support enum types yet; use the CBOR ABI for this type" + ) + ffiTypeRegistry.add( + FFITypeMeta( + name: typeNameStr, + abiFormat: abiFormat, + enumValues: enumValueMetas(typeDef[2], typeNameStr), + ) + ) + ffiEnumTypeNames.add(typeNameStr) + +proc registerFFITypeInfo( + typeDef: NimNode, abiFormat: ABIFormat +): NimNode {.compileTime.} = + ## Registers the type in ffiTypeRegistry and returns the clean typeDef. + let typeName = + if typeDef[0].kind == nnkPostfix: + typeDef[0][1] + else: + typeDef[0] + let typeNameStr = $typeName + + if typeDef[2].kind == nnkEnumTy: + registerFFIEnumInfo(typeDef, typeNameStr, abiFormat) + return typeDef + + var fieldMetas: seq[FFIFieldMeta] = @[] + let objTy = typeDef[2] + if objTy.kind == nnkObjectTy and objTy.len >= 3: + let recList = objTy[2] + if recList.kind == nnkRecList: + for identDef in recList: + if identDef.kind == nnkIdentDefs: + let fieldType = identDef[^2] + for i in 0 ..< identDef.len - 2: + rejectRawPtrType( + fieldType, "{.ffi.} type " & typeNameStr & "." & $identDef[i] + ) + let fieldTypeName = + if fieldType.kind == nnkIdent: + $fieldType + else: + fieldType.repr + for i in 0 ..< identDef.len - 2: + fieldMetas.add(FFIFieldMeta(name: $identDef[i], typeName: fieldTypeName)) + + ffiTypeRegistry.add( + FFITypeMeta(name: typeNameStr, fields: fieldMetas, abiFormat: abiFormat) + ) + return typeDef + +func extractDocComment(prc: NimNode): string {.compileTime.} = + ## The proc's leading `##`, or "". Nim drops comments outside a proc body, so + ## types and fields are unreachable from here. + let body = prc[^1] + if body.kind != nnkStmtList or body.len == 0: + return "" + if body[0].kind != nnkCommentStmt: + return "" + return body[0].strVal + +proc nimTypeNameRepr(typ: NimNode): string = + ## Stringifies a parameter or field type for the registry. + case typ.kind + of nnkIdent: + $typ + of nnkPtrTy: + "ptr " & nimTypeNameRepr(typ[0]) + else: + typ.repr + +proc isHandleType(typ: NimNode): bool = + ## True iff `typ` is an `{.ffiHandle.}` type — its wire form is `uint64`. + typ.kind == nnkIdent and isFFIHandleTypeName($typ) + +proc storageType(typ: NimNode): NimNode = + ## In-Req-struct storage type: `cstring`->`string`, handle->`uint64`, else as-is. + if typ.kind == nnkIdent and $typ == "cstring": + return ident("string") + if isHandleType(typ): + return ident("uint64") + typ + +proc unpackReqField*(fieldIdent, userType, decodedIdent: NimNode): NimNode = + ## Emits AST unpacking one field of a CBOR-decoded Req into a local of the + ## user's original type. `cstring` (stored as `string`) is cast back on unpack, + ## safe because `decodedIdent` outlives the cstring use in the generated body. + let storedAsString = userType.kind == nnkIdent and $userType == "cstring" + if not storedAsString: + return newLetStmt(fieldIdent, newDotExpr(decodedIdent, fieldIdent)) + + let fieldAccess = newDotExpr(decodedIdent, fieldIdent) + let castExpr = newDotExpr(fieldAccess, ident("cstring")) + return + nnkLetSection.newTree(nnkIdentDefs.newTree(fieldIdent, ident("cstring"), castExpr)) + +proc unpackHandleField*( + fieldIdent, userType, ctxIdent, decodedIdent: NimNode +): NimNode = + ## Reconstitutes a handle param from its wire `uint64` via the ctx registry. + let errPrefix = "ffiHandle for parameter '" & $fieldIdent & "': " + quote: + let `fieldIdent` = block: + let ffiH = `ctxIdent`[].handles.lookup(`decodedIdent`.`fieldIdent`, $`userType`).valueOr: + return err(`errPrefix` & error) + cast[`userType`](ffiH) + +proc cExportedParams(ctxType: NimNode, withCtx = true): seq[NimNode] = + ## C-exported wrapper param list (cint; ctx, callback, userData, reqCbor, + ## reqCborLen). A `{.ffiStatic.}` wrapper drops the leading `ctx`. + var params: seq[NimNode] = @[] + params.add(ident("cint")) + if withCtx: + params.add(newIdentDefs(ident("ctx"), ctxType)) + params.add(newIdentDefs(ident("callback"), ident("FFICallBack"))) + params.add(newIdentDefs(ident("userData"), ident("pointer"))) + params.add(newIdentDefs(ident("reqCbor"), nnkPtrTy.newTree(ident("byte")))) + params.add(newIdentDefs(ident("reqCborLen"), ident("csize_t"))) + return params + +proc buildReqTypeFromFields( + reqTypeName: NimNode, paramNames: seq[string], paramTypes: seq[NimNode] +): NimNode = + ## Builds the exported per-proc Req `type Foo* = object` from parallel name/type + ## lists. `cstring` fields become `string`; an empty param list gets a single + ## `_placeholder: uint8` field since Nim rejects an empty object body here. var fields: seq[NimNode] = @[] - for p in params[1 .. ^1]: # skip return type at index 0 - let name = p[0] - let typ = p[1] - # Field must be nnkIdentDefs(name, type, defaultExpr) - fields.add newTree(nnkIdentDefs, name, typ, newEmptyNode()) + for i in 0 ..< paramNames.len: + let storedType = storageType(paramTypes[i]) + fields.add newTree(nnkIdentDefs, ident(paramNames[i]), storedType, newEmptyNode()) - # Wrap fields in a rec list - let recList = newTree(nnkRecList, fields) + let recList = + if fields.len > 0: + newTree(nnkRecList, fields) + else: + newTree( + nnkRecList, + newTree(nnkIdentDefs, ident("_placeholder"), ident("uint8"), newEmptyNode()), + ) - # object type node: object [of?] [] [pragma?] recList let objTy = newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), recList) - # Export the type (CreateNodeRequest*) let typeName = if reqTypeName.kind == nnkPostfix: reqTypeName else: postfix(reqTypeName, "*") - result = + return newNimNode(nnkTypeSection).add(newTree(nnkTypeDef, typeName, newEmptyNode(), objTy)) +proc buildRequestType(reqTypeName: NimNode, body: NimNode): NimNode = + ## Builds the per-proc Req object type from a registerReqFFI lambda body, + ## mirroring its param names and types (`cstring` -> `string`). + var procNode = body + if procNode.kind == nnkStmtList and procNode.len == 1: + procNode = procNode[0] + if procNode.kind != nnkLambda and procNode.kind != nnkProcDef: + error "registerReqFFI expects a lambda proc, found: " & $procNode.kind + + let params = procNode[3] + var paramNames: seq[string] = @[] + var paramTypes: seq[NimNode] = @[] + for p in params[1 .. ^1]: + paramNames.add($p[0]) + paramTypes.add(p[1]) + + let typeSection = buildReqTypeFromFields(reqTypeName, paramNames, paramTypes) + when defined(ffiDumpMacros): - echo result.repr - -proc buildFfiNewReqProc(reqTypeName, body: NimNode): NimNode = - ## Builds the ffiNewProc in charge of creating the FFIThreadRequest in shared memory. - ## Then, a pointer to this request will be sent to the FFI thread for processing. - ## e.g.: - ## proc ffiNewReq*(T: typedesc[CreateNodeRequest]; callback: FFICallBack; - ## userData: pointer; configJson: cstring; - ## appCallbacks: AppCallbacks): ptr FFIThreadRequest = - ## var reqObj = createShared(T) - ## reqObj[].configJson = configJson.alloc() - ## reqObj[].appCallbacks = appCallbacks - ## let typeStr`gensym2866 = $T - ## var ret`gensym2866 = FFIThreadRequest.init(callback, userData, - ## typeStr`gensym2866.cstring, reqObj) - ## return ret`gensym2866 - ## - ## This should be invoked by the ffi consumer thread (generally, main thread.) - ## Notice that the shared memory allocated by the main thread is freed by the FFI thread - ## after processing the request. + echo typeSection.repr + return typeSection +proc buildFFINewReqProc(reqTypeName, body: NimNode): NimNode = + ## Builds ffiNewReq: packs the user's typed params into a Req, CBOR-encodes it, + ## and constructs the FFIThreadRequest that owns the buffer. var formalParams = newSeq[NimNode]() var procNode: NimNode if body.kind == nnkStmtList and body.len == 1: - procNode = body[0] # unwrap single statement + procNode = body[0] else: procNode = body if procNode.kind != nnkLambda and procNode.kind != nnkProcDef: error "registerReqFFI expects a lambda definition. Found: " & $procNode.kind - # T: typedesc[CreateNodeRequest] - let typedescParam = newIdentDefs( - ident("T"), # param name - nnkBracketExpr.newTree(ident("typedesc"), reqTypeName), # typedesc[T] - ) + let typedescParam = + newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName)) formalParams.add(typedescParam) - - # Other fixed FFI params formalParams.add(newIdentDefs(ident("callback"), ident("FFICallBack"))) formalParams.add(newIdentDefs(ident("userData"), ident("pointer"))) - # Add original lambda params + # Handle params travel as their uint64 id; others keep the user's type. let procParams = procNode[3] for p in procParams[1 .. ^1]: + if isHandleType(p[1]): + formalParams.add(newIdentDefs(p[0], ident("uint64"))) + continue formalParams.add(p) - # Build `ptr FFIThreadRequest` let retType = newNimNode(nnkPtrTy) retType.add(ident("FFIThreadRequest")) formalParams = @[retType] & formalParams - # Build body let reqObjIdent = ident("reqObj") var newBody = newStmtList() newBody.add( quote do: - var `reqObjIdent` = createShared(T) + var `reqObjIdent`: T ) for p in procParams[1 .. ^1]: - let fieldNameIdent = ident($p[0]) - let fieldTypeNode = p[1] - - # Extract type name as string - var typeStr: string - if fieldTypeNode.kind == nnkIdent: - typeStr = $fieldTypeNode - elif fieldTypeNode.kind == nnkBracketExpr: - typeStr = $fieldTypeNode[0] # e.g., `ptr` in `ptr[Waku]` - else: - typeStr = "" # fallback - - # Apply .alloc() only to cstrings - if typeStr == "cstring": + let fieldName = ident($p[0]) + let userType = p[1] + let storeAsString = userType.kind == nnkIdent and $userType == "cstring" + if storeAsString: newBody.add( quote do: - `reqObjIdent`[].`fieldNameIdent` = `fieldNameIdent`.alloc() + `reqObjIdent`.`fieldName` = $`fieldName` ) else: newBody.add( quote do: - `reqObjIdent`[].`fieldNameIdent` = `fieldNameIdent` + `reqObjIdent`.`fieldName` = `fieldName` ) - # FFIThreadRequest.init using fnv1aHash32 + let reqNameLit = newLit($unwrapPostfix(reqTypeName)) newBody.add( quote do: - let typeStr = $T - var ret = - FFIThreadRequest.init(callback, userData, typeStr.cstring, `reqObjIdent`) - return ret + # Encode into shared memory, avoiding a second seq[byte] copy. + let (sharedData, sharedLen) = cborEncodeShared(`reqObjIdent`) + return FFIThreadRequest.initFromOwnedShared( + callback, userData, cstring(`reqNameLit`), sharedData, sharedLen + ) ) - # Build the proc node - result = newProc( + let newReqProc = newProc( name = postfix(ident("ffiNewReq"), "*"), params = formalParams, body = newBody, @@ -176,46 +413,40 @@ proc buildFfiNewReqProc(reqTypeName, body: NimNode): NimNode = ) when defined(ffiDumpMacros): - echo result.repr + echo newReqProc.repr + return newReqProc -proc buildFfiDeleteReqProc(reqTypeName: NimNode, fields: seq[NimNode]): NimNode = - ## Generates: - ## proc ffiDeleteReq(self: ptr ) = - ## deallocShared(self[].) - ## deallocShared(self) +proc reqDecodePreamble( + reqTypeName, reqIdent, decodedIdent: NimNode, abi: ABIFormat +): NimNode = + ## Materialise the typed Req from the request payload. `abi = c` unpacks the + ## packed `_CWire` struct the caller thread handed over and frees it here (the + ## unpack deep-copies into Nim memory); the envelope buffer itself goes with + ## `deleteRequest`. Otherwise the payload is CBOR. + if abi != ABIFormat.C: + return quote: + let `reqIdent`: ptr FFIThreadRequest = cast[ptr FFIThreadRequest](request) + let `decodedIdent` = cborDecodePtr( + cast[ptr UncheckedArray[byte]](`reqIdent`[].data), + `reqIdent`[].dataLen, + `reqTypeName`, + ).valueOr: + return err("CBOR decode failed for " & $T & ": " & $error) - # Build the body - var body = newStmtList() - for f in fields: - if $f[1] == "cstring": # only dealloc cstring fields - body.add newCall( - ident("deallocShared"), - newDotExpr(newTree(nnkDerefExpr, ident("self")), ident($f[0])), - ) - - # Always free the whole object at the end - body.add newCall(ident("deallocShared"), ident("self")) - - # Build the parameter: (self: ptr ) - let selfParam = newIdentDefs(ident("self"), newTree(nnkPtrTy, reqTypeName)) - - # Build the proc definition - result = newProc( - name = postfix(ident("ffiDeleteReq"), "*"), - params = @[newEmptyNode()] & @[selfParam], # ✅ properly wrapped in a sequence - body = body, - ) - - when defined(ffiDumpMacros): - echo result.repr - -proc buildProcessFFIRequestProc(reqTypeName, reqHandler, body: NimNode): NimNode = - ## Builds, f.e.: - ## proc processFFIRequest(T: typedesc[CreateNodeRequest]; - ## configJson: cstring; - ## appCallbacks: AppCallbacks; - ## ctx: ptr FFIContext[Waku]) ... + let wireType = ident(cwireTypeName($reqTypeName)) + let wirePtr = genSym(nskLet, "wireReq") + return quote: + let `reqIdent`: ptr FFIThreadRequest = cast[ptr FFIThreadRequest](request) + if `reqIdent`[].data.isNil() or `reqIdent`[].dataLen != sizeof(`wireType`): + return err("abi = c: unexpected request payload size for " & $T) + let `wirePtr` = cast[ptr `wireType`](`reqIdent`[].data) + let `decodedIdent` = cwireUnpack(`wirePtr`[]) + cwireFree(`wirePtr`[]) +proc buildProcessFFIRequestProc( + reqTypeName, reqHandler, body: NimNode, abi: ABIFormat +): NimNode = + ## FFI-thread processor: materialises the Req, unpacks fields, runs user body. if reqHandler.kind != nnkExprColonExpr: error( "Second argument must be a typed parameter, e.g., waku: ptr Waku. Found: " & @@ -235,15 +466,13 @@ proc buildProcessFFIRequestProc(reqTypeName, reqHandler, body: NimNode): NimNode let typedescParam = newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName)) - # Build formal params: (returnType, request: pointer, waku: ptr Waku) let procParams = procNode[3] var formalParams: seq[NimNode] = @[] - formalParams.add(procParams[0]) # return type + formalParams.add(procParams[0]) formalParams.add(typedescParam) formalParams.add(newIdentDefs(ident("request"), ident("pointer"))) - formalParams.add(newIdentDefs(reqHandler[0], rhs)) # e.g. waku: ptr Waku + formalParams.add(newIdentDefs(reqHandler[0], rhs)) - # Inject cast/unpack/defer into the body let bodyNode = if procNode.body.kind == nnkStmtList: procNode.body @@ -251,24 +480,20 @@ proc buildProcessFFIRequestProc(reqTypeName, reqHandler, body: NimNode): NimNode newStmtList(procNode.body) let newBody = newStmtList() - let reqIdent = ident("req") + let reqIdent = genSym(nskLet, "ffiReq") + let decodedIdent = genSym(nskLet, "decoded") - newBody.add quote do: - let `reqIdent`: ptr `reqTypeName` = cast[ptr `reqTypeName`](request) - defer: - ffiDeleteReq(`reqIdent`) + newBody.add reqDecodePreamble(reqTypeName, reqIdent, decodedIdent, abi) - # automatically unpack fields into locals for p in procParams[1 ..^ 1]: - let fieldName = p[0] # Ident + if isHandleType(p[1]): + newBody.add unpackHandleField(p[0], p[1], reqHandler[0], decodedIdent) + continue + newBody.add unpackReqField(p[0], p[1], decodedIdent) - newBody.add quote do: - let `fieldName` = `reqIdent`[].`fieldName` - - # Append user's lambda body newBody.add(bodyNode) - result = newProc( + let processProc = newProc( name = postfix(ident("processFFIRequest"), "*"), params = formalParams, body = newBody, @@ -281,125 +506,122 @@ proc buildProcessFFIRequestProc(reqTypeName, reqHandler, body: NimNode): NimNode ) when defined(ffiDumpMacros): - echo result.repr + echo processProc.repr + return processProc -proc addNewRequestToRegistry(reqTypeName, reqHandler: NimNode): NimNode = - ## Adds a new request to the registeredRequests table. - ## The key is a representation of the request, e.g. "CreateNodeReq". - ## The value is a proc definition in charge of handling the request from FFI thread. +proc replyEncode( + typedResIdent, handlerCtxIdent, respType: NimNode, abi: ABIFormat +): NimNode = + ## Lower the handler's typed value into the `seq[byte]` reply payload. `abi = c` + ## rides raw — a `string` as its own UTF-8, an object as the native image of its + ## packed `_CWire`, whose buffers the reply trampoline frees. + if abi == ABIFormat.C: + if isStringType(respType): + return quote: + return ok(ffiRawRetBytes(`typedResIdent`.value)) + let wireType = ident(cwireTypeName($respType)) + let wireIdent = genSym(nskVar, "replyWire") + return quote: + var `wireIdent`: `wireType` + cwirePack(`wireIdent`, `typedResIdent`.value) + return ok(cwireStructBytes(`wireIdent`)) - # Build: request[].reqContent - let reqContent = - newDotExpr(newTree(nnkDerefExpr, ident("request")), ident("reqContent")) + return quote: + # A `seq[byte]` result goes on the wire as CBOR, the same as every other + # `abi = cbor` return. The C, C++ and Rust decoders expect CBOR. They reject + # raw bytes with the error "value encoded in non-canonical form". + when typeof(`typedResIdent`.value) is void: + return ok(newSeq[byte]()) + elif typeof(`typedResIdent`.value) is FFIHandleRoot: + return ok( + encodeHandle( + `handlerCtxIdent`[].handles.register( + `typedResIdent`.value, $typeof(`typedResIdent`.value) + ) + ) + ) + else: + return ok(cborEncode(`typedResIdent`.value)) - # Build Future[Result[string, string]] return type +proc addNewRequestToRegistry( + reqTypeName, reqHandler, respType: NimNode, abi: ABIFormat +): NimNode = + ## Dispatcher the FFI thread calls: runs processFFIRequest and lowers the typed + ## T value into the seq[byte] payload. let returnType = nnkBracketExpr.newTree( ident("Future"), - nnkBracketExpr.newTree(ident("Result"), ident("string"), ident("string")), + nnkBracketExpr.newTree( + ident("Result"), + nnkBracketExpr.newTree(ident("seq"), ident("byte")), + ident("string"), + ), ) - # Extract the type from reqHandler (generic: ptr Waku, ptr Foo, ptr Bar, etc.) let rhsType = if reqHandler.kind == nnkExprColonExpr: - reqHandler[1] # Use the explicit type + reqHandler[1] else: error "Second argument must be a typed parameter, e.g. waku: ptr Waku" - # Build: cast[ptr Waku](reqHandler) or cast[ptr Foo](reqHandler) dynamically - let castedHandler = newTree( - nnkCast, - rhsType, # The type, e.g. ptr Waku - ident("reqHandler"), # The expression to cast - ) + let handlerCtxIdent = genSym(nskLet, "handlerCtx") let callExpr = newCall( - newDotExpr(reqTypeName, ident("processFFIRequest")), ident("request"), castedHandler + newDotExpr(reqTypeName, ident("processFFIRequest")), + ident("request"), + handlerCtxIdent, ) + let typedResIdent = genSym(nskLet, "typedRes") + var newBody = newStmtList() - newBody.add( - quote do: - return await `callExpr` - ) + newBody.add quote do: + let `handlerCtxIdent` = cast[`rhsType`](reqHandler) + let `typedResIdent` = await `callExpr` + if `typedResIdent`.isErr: + return err(`typedResIdent`.error) + + newBody.add replyEncode(typedResIdent, handlerCtxIdent, respType, abi) - # Build: - # proc(request: pointer, reqHandler: pointer): - # Future[Result[string, string]] {.async.} = - # CreateNodeRequest.processFFIRequest(request, reqHandler) let asyncProc = newProc( - name = newEmptyNode(), # anonymous proc - params = - @[ - returnType, - newIdentDefs(ident("request"), ident("pointer")), - newIdentDefs(ident("reqHandler"), ident("pointer")), - ], + name = newEmptyNode(), + params = @[ + returnType, + newIdentDefs(ident("request"), ident("pointer")), + newIdentDefs(ident("reqHandler"), ident("pointer")), + ], body = newBody, pragmas = nnkPragma.newTree(ident("async")), ) - let reqTypeNameStr = $reqTypeName - let key = newLit($reqTypeName) - # Generate: registeredRequests["CreateNodeRequest"] = - result = + let regAssign = newAssignment(newTree(nnkBracketExpr, ident("registeredRequests"), key), asyncProc) when defined(ffiDumpMacros): - echo result.repr + echo regAssign.repr + return regAssign macro registerReqFFI*(reqTypeName, reqHandler, body: untyped): untyped = - ## Registers a request that will be handled by the FFI/working thread. - ## The request should be sent from the ffi consumer thread. - ## - ## e.g.: - ## In this example, we register a CreateNodeRequest that will be handled by a proc that contains - ## the provided lambda body and parameters, by the FFI/working thread. - ## - ## The lambda passed to this macro must: - ## - only have no-GC'ed types. - ## - Return Future[Result[string, string]] and be annotated with {.async.} - ## And notice that the returned values will be sent back to the ffi consumer thread. - ## - ## registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): - ## proc( - ## configJson: cstring, appCallbacks: AppCallbacks - ## ): Future[Result[string, string]] {.async.} = - ## ctx.myLib[] = (await createWaku(configJson, cast[AppCallbacks](appCallbacks))).valueOr: - ## return err($error) - ## return ok("") - ## - ## On the other hand, the created FFI request should be dispatched from the ffi consumer thread - ## (generally, the main thread) following something like: - ## - ## ffi.sendRequestToFFIThread( - ## ctx, CreateNodeRequest.ffiNewReq(callback, userData, configJson, appCallbacks) - ## ).isOkOr: - ## ... - ## ... - ## - - # Extract lambda params to generate fields - let fields = extractFieldsFromLambda(body) - + ## Registers a request handled by the FFI/working thread. The lambda takes only + ## no-GC'ed params (cstring travels as `string`) and must return + ## Future[Result[string, string]] {.async.}. let typeDef = buildRequestType(reqTypeName, body) - let ffiNewReqProc = buildFfiNewReqProc(reqTypeName, body) - let processProc = buildProcessFFIRequestProc(reqTypeName, reqHandler, body) - let addNewReqToReg = addNewRequestToRegistry(reqTypeName, reqHandler) - let deleteProc = buildFfiDeleteReqProc(reqTypeName, fields) - result = newStmtList(typeDef, ffiNewReqProc, deleteProc, processProc, addNewReqToReg) + let ffiNewReqProc = buildFFINewReqProc(reqTypeName, body) + let processProc = + buildProcessFFIRequestProc(reqTypeName, reqHandler, body, ABIFormat.Cbor) + let addNewReqToReg = + addNewRequestToRegistry(reqTypeName, reqHandler, newEmptyNode(), ABIFormat.Cbor) + let stmts = newStmtList(typeDef, ffiNewReqProc, processProc, addNewReqToReg) when defined(ffiDumpMacros): - echo result.repr + echo stmts.repr + return stmts macro processReq*( reqType, ctx, callback, userData: untyped, args: varargs[untyped] ): untyped = - ## Expands T.processReq(ctx, callback, userData, a, b, ...) - ## e.g.: - ## waku_dial_peerReq.processReq(ctx, callback, userData, peerMultiAddr, protocol, timeoutMs) - ## - + ## Expands T.processReq(ctx, callback, userData, args...) into a + ## sendRequestToFFIThread call, reporting errors via `callback`. var callArgs = @[reqType, callback, userData] for a in args: callArgs.add a @@ -410,7 +632,7 @@ macro processReq*( newDotExpr(ident("ffi_context"), ident("sendRequestToFFIThread")), ctx, newReqCall ) - result = quote: + let blockExpr = quote: block: let res = `sendCall` if res.isErr(): @@ -420,72 +642,41 @@ macro processReq*( return RET_OK when defined(ffiDumpMacros): - echo result.repr + echo blockExpr.repr + return blockExpr -macro ffi*(prc: untyped): untyped = - ## Defines an FFI-exported proc that registers a request handler to be executed - ## asynchronously in the FFI thread. - ## - ## {.ffi.} implicitly implies: ...Return[Future[Result[string, string]] {.async.} - ## - ## When using {.ffi.}, the first three parameters must be: - ## - ctx: ptr FFIContext[T] <-- T is the type that handles the FFI requests - ## - callback: FFICallBack - ## - userData: pointer - ## Then, additional parameters may be defined as needed, after these first three, always - ## considering that only no-GC'ed (or C-like) types are allowed. - ## - ## e.g.: - ## proc waku_version( - ## ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer - ## ) {.ffi.} = - ## return ok(WakuNodeVersionString) - ## - ## e.g2.: - ## proc waku_start( - ## ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer - ## ) {.ffi.} = - ## (await startWaku(ctx[].myLib)).isOkOr: - ## error "START_NODE failed", error = error - ## return err("failed to start: " & $error) - ## return ok("") - ## - ## e.g3.: - ## proc waku_peer_exchange_request( - ## ctx: ptr FFIContext[Waku], - ## callback: FFICallBack, - ## userData: pointer, - ## numPeers: uint64, - ## ) {.ffi.} = - ## let numValidPeers = (await performPeerExchangeRequestTo(numPeers, ctx.myLib[])).valueOr: - ## error "waku_peer_exchange_request failed", error = error - ## return err("failed peer exchange: " & $error) - ## return ok($numValidPeers) - ## - ## In these examples, notice that ctx.myLib is of type "ptr Waku", being Waku main library type. - ## +macro ffiRaw*(args: varargs[untyped]): untyped = + ## Raw/legacy FFI proc: first three params (ctx, callback, userData) are explicit, + ## extra no-GC'ed params travel as one CBOR blob, return is implied + ## Future[Result[string, string]] {.async.}. Override abi via `{.ffiRaw: "abi = c".}`. + requireBeforeGenBindings("`.ffiRaw.`") + requireLibraryDeclared("`.ffiRaw.`") + let prc = args[^1] + let rawAbiFormat = resolveFFISpecs(args[0 ..^ 2]) + gateABIFormat(rawAbiFormat, "`.ffiRaw.` proc") let procName = prc[0] let formalParams = prc[3] let bodyNode = prc[^1] if formalParams.len < 2: - error("`.ffi.` procs require at least 1 parameter") + error("`.ffiRaw.` procs require at least 1 parameter") let firstParam = formalParams[1] let paramIdent = firstParam[0] let paramType = firstParam[1] + let libTypeName = paramType[0][1] + let poolIdent = ident($libTypeName & "FFIPool") + let reqName = ident($procName & "Req") let returnType = ident("cint") - # Build parameter list (skip return type) var newParams = newSeq[NimNode]() newParams.add(returnType) for i in 1 ..< formalParams.len: newParams.add(newIdentDefs(formalParams[i][0], formalParams[i][1])) - # Build Future[Result[string, string]] return type let futReturnType = quote: Future[Result[string, string]] @@ -495,55 +686,1305 @@ macro ffi*(prc: untyped): untyped = for i in 4 ..< formalParams.len: userParams.add(newIdentDefs(formalParams[i][0], formalParams[i][1])) - # Build argument list for processReq var argsList = newSeq[NimNode]() for i in 1 ..< formalParams.len: argsList.add(formalParams[i][0]) - # 1. Build the dot expression. e.g.: waku_is_onlineReq.processReq let dotExpr = newTree(nnkDotExpr, reqName, ident"processReq") - # 2. Build the call node with dotExpr as callee let callNode = newTree(nnkCall, dotExpr) for arg in argsList: callNode.add(arg) - # Proc body let ffiBody = newStmtList( quote do: initializeLibrary() - if not isNil(ctx): - ctx[].userData = userData + if not `poolIdent`.isValidCtx(cast[pointer](ctx)): + return RET_ERR + ctx[].userData = userData if isNil(callback): return RET_MISSING_CALLBACK ) ffiBody.add(callNode) - # Under emscripten, `dynlib` makes Nim emit `emcc -shared` (a wasm SIDE module), - # which breaks EXPORTED_FUNCTIONS/malloc. The wasm/edge build is a MAIN module, - # so export with plain `exportc` there. - let exportPragmas = - when defined(emscripten): - newTree(nnkPragma, ident "exportc", ident "cdecl") - else: - newTree(nnkPragma, ident "dynlib", ident "exportc", ident "cdecl") - let ffiProc = - newProc(name = procName, params = newParams, body = ffiBody, pragmas = exportPragmas) + let ffiProc = newProc( + name = procName, + params = newParams, + body = ffiBody, + pragmas = newTree(nnkPragma, ident "dynlib", ident "exportc", ident "cdecl"), + ) var anonymousProcNode = newProc( - name = newEmptyNode(), # anonymous proc + name = newEmptyNode(), params = userParams, body = newStmtList(bodyNode), pragmas = newTree(nnkPragma, ident"async"), ) - # registerReqFFI wrapper let registerReq = quote: registerReqFFI(`reqName`, `paramIdent`: `paramType`): `anonymousProcNode` - result = newStmtList(registerReq, ffiProc) + let stmts = newStmtList(registerReq, ffiProc) when defined(ffiDumpMacros): - echo result.repr + echo stmts.repr + return stmts + +macro ffiHandle*(args: varargs[untyped]): untyped = + ## Marks a `ref object` as an opaque FFI handle: it rides as a `uint64` id while + ## the live object stays in the per-ctx registry. An `"abi = ..."` spec is + ## accepted but only validated (a handle is abi-agnostic). + requireBeforeGenBindings("`.ffiHandle.`") + requireLibraryDeclared("`.ffiHandle.`") + let prc = args[^1] + discard resolveABIFormat(args[0 ..^ 2]) + if prc.kind != nnkTypeDef: + error("`.ffiHandle.` must be applied to a type definition") + + var clean = prc.copyNimTree() + if clean[0].kind == nnkPragmaExpr: + clean[0] = clean[0][0] + + let typeName = + if clean[0].kind == nnkPostfix: + clean[0][1] + else: + clean[0] + + let refTy = clean[2] + if refTy.kind != nnkRefTy or refTy[0].kind != nnkObjectTy: + error("`.ffiHandle.` type " & $typeName & " must be a `ref object`") + let objTy = refTy[0] + if objTy[1].kind != nnkEmpty: + error("`.ffiHandle.` type " & $typeName & " must not already inherit a base") + # Inherit the registry's storable base so handle refs share one static type. + objTy[1] = nnkOfInherit.newTree(ident("FFIHandleRoot")) + + ffiHandleTypeNames.add($typeName) + + when defined(ffiDumpMacros): + echo clean.repr + return clean + +proc registerFFIConst(nameNode: NimNode): NimNode {.compileTime.} = + ## Emits the type guard plus the `static:` block that records the const's + ## evaluated value; `$typeof` runs after the const is defined, so computed + ## expressions (`3 * 7`) land in the registry as their result. + let nameStr = newLit($nameNode) + let unsupported = newLit( + "`.ffiConst.` " & $nameNode & + ": only integer, float, bool and string consts can cross the FFI boundary" + ) + # bindSym: the emitted code lands in the user's module, which doesn't import meta. + let registry = bindSym("ffiConstRegistry") + let metaType = bindSym("FFIConstMeta") + quote: + when not (`nameNode` is (SomeInteger | SomeFloat | bool | string)): + {.error: `unsupported`.} + static: + `registry`.add( + `metaType`(name: `nameStr`, typeName: $typeof(`nameNode`), value: $(`nameNode`)) + ) + +macro ffiConst*(args: varargs[untyped]): untyped = + ## Exposes a Nim `const` to the generated bindings as a native constant + ## (`static const` in C/C++, `pub const` in Rust). An `"abi = ..."` spec is + ## accepted but only validated — a constant never rides the wire. + requireBeforeGenBindings("`.ffiConst.`") + requireLibraryDeclared("`.ffiConst.`") + let section = args[^1] + discard resolveABIFormat(args[0 ..^ 2]) + if section.kind != nnkConstSection: + error("`.ffiConst.` must be applied to a `const` definition") + + # Nim splits the section so only the annotated defs reach this macro. + var stmts = newStmtList(section.copyNimTree()) + for def in section: + let nameNode = + if def[0].kind == nnkPostfix: + def[0][1] + else: + def[0] + stmts.add(registerFFIConst(nameNode)) + + when defined(ffiDumpMacros): + echo stmts.repr + return stmts + +proc buildFFIProc( + prc: NimNode, abiFormat: ABIFormat, isStatic: bool +): NimNode {.compileTime.} = + ## Shared body of `{.ffi.}` and `{.ffiStatic.}`. A static has no library receiver: + ## its wire params start at param 1 and its C wrapper binds the static context. + let where = if isStatic: "`.ffiStatic.`" else: "`.ffi.`" + + let procName = prc[0] + let formalParams = prc[3] + let bodyNode = prc[^1] + + if not isStatic and formalParams.len < 2: + error("`.ffi.` procs require at least 1 parameter (the library type)") + + var recvName, recvType: NimNode = newEmptyNode() + var firstIsHandle = false + if not isStatic: + let firstParam = formalParams[1] + recvName = firstParam[0] + recvType = firstParam[1] + firstIsHandle = isHandleType(recvType) + if (firstIsHandle or isStatic) and currentLibType.len == 0: + let why = + if isStatic: " takes no library param" else: " has an {.ffiHandle.} receiver" + error( + where & " proc " & $procName & why & " but no library is declared; " & + "call declareLibrary(name, LibType) first" + ) + # Neither carries a library type, so fall back to the declared one. + let libTypeName = + if firstIsHandle or isStatic: + ident(currentLibType) + else: + recvType + + let retTypeNode = formalParams[0] + if retTypeNode.kind == nnkEmpty: + error( + where & " proc must have an explicit return type Future[Result[RetType, string]]" + ) + if retTypeNode.kind != nnkBracketExpr or $retTypeNode[0] != "Future": + error( + where & " return type must be Future[Result[RetType, string]], got: " & + retTypeNode.repr + ) + let resultInner = retTypeNode[1] + if resultInner.kind != nnkBracketExpr or $resultInner[0] != "Result": + error( + where & " return type must be Future[Result[RetType, string]], got: " & + retTypeNode.repr + ) + + let resultRetType = resultInner[1] + rejectRawPtrType(resultRetType, where & " proc " & $procName & " return type") + # An {.ffiHandle.} lives in one ctx's registry, which a static proc cannot reach. + if isStatic and isHandleType(resultRetType): + error( + where & " proc " & $procName & " returns the {.ffiHandle.} type " & $resultRetType & + "; a handle belongs to a context. Make it an `{.ffi.}` method instead." + ) + + # A handle receiver rides the wire; a value-type lib receiver binds to ctx.myLib. + var extraParamNames: seq[string] = @[] + var extraParamTypes: seq[NimNode] = @[] + let wireStart = if isStatic or firstIsHandle: 1 else: 2 + for i in wireStart ..< formalParams.len: + let p = formalParams[i] + for j in 0 ..< p.len - 2: + rejectRawPtrType(p[^2], where & " proc " & $procName & " parameter " & $p[j]) + if isStatic and isHandleType(p[^2]): + error( + where & " proc " & $procName & " takes the {.ffiHandle.} parameter " & $p[j] & + ": " & $p[^2] & "; a handle belongs to a context. " & + "Make it an `{.ffi.}` method instead." + ) + extraParamNames.add($p[j]) + extraParamTypes.add(p[^2]) + + let procNameStr = block: + let raw = $procName + if raw.endsWith("*"): + raw[0 ..^ 2] + else: + raw + let cExportName = camelToSnakeCase(procNameStr) + let camelName = snakeToPascalCase(procNameStr) + + let reqTypeName = ident(camelName & "Req") + + var userProcName = procName + if procName.kind == nnkPostfix: + userProcName = procName[1] + # Nim proc and C wrapper share the user's name (resolved by overload); the wrapper's `{.exportc.}` keeps the foreign ABI symbol. + let cExportProcName = userProcName + + let ctxType = + nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)) + + proc wireParamMeta(pname: string, ptype: NimNode): FFIParamMeta = + let isPointer = isPtr(ptype) + let handle = isHandleType(ptype) + let tn = + if isPointer: + nimTypeNameRepr(ptype[0]) + else: + nimTypeNameRepr(ptype) + FFIParamMeta(name: pname, typeName: tn, isPtr: isPointer, isHandle: handle) + + var wireParamMetas: seq[FFIParamMeta] = @[] + for i in 0 ..< extraParamNames.len: + wireParamMetas.add(wireParamMeta(extraParamNames[i], extraParamTypes[i])) + + let retTypeInner = resultInner[1] + let retIsPtr = isPtr(retTypeInner) + let retIsHandle = isHandleType(retTypeInner) + let retTn = + if retIsPtr: + nimTypeNameRepr(retTypeInner[0]) + else: + nimTypeNameRepr(retTypeInner) + + # Built once, registered by whichever path runs; reused for the check below. + let procMeta = FFIProcMeta( + procName: cExportName, + libName: currentLibName, + kind: if isStatic: FFIKind.STATIC else: FFIKind.FFI, + libTypeName: $libTypeName, + extraParams: wireParamMetas, + returnTypeName: retTn, + returnIsPtr: retIsPtr, + returnIsHandle: retIsHandle, + abiFormat: abiFormat, + doc: extractDocComment(prc), + ) + + # CBOR-free scalar fast path: only `abi = c` with all-scalar params/return that fit the inline slots; non-scalar `abi = c` rides the `_CWire` C-dispatch. + let scalarEligible = + abiFormat == ABIFormat.C and isScalarOnly(procMeta) and + extraParamNames.len <= MaxScalarArgs + + let poolIdent = ident($libTypeName & "FFIPool") + + proc buildCtxGuard(): NimNode = + ## Nil-checks callback and validates `ctx`, replying `RET_ERR` before build. + quote: + if callback.isNil: + return RET_MISSING_CALLBACK + if not `poolIdent`.isValidCtx(cast[pointer](ctx)): + let errStr = "ctx is not a valid FFI context" + callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData) + return RET_ERR + + proc buildStaticCtxGuard(): NimNode = + ## Binds the library's static context; a static call may be the host's first + ## entry, hence `initializeLibrary`. + # `ctxIdent` is substituted so the send below sees it (`quote` gensyms). + let ctxIdent = ident("ctx") + quote: + initializeLibrary() + if callback.isNil(): + return RET_MISSING_CALLBACK + let `ctxIdent` = `poolIdent`.staticFFIContext().valueOr: + let errStr = "ffiStatic: " & error + callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData) + return RET_ERR + + proc buildSendAndReply(reqPtrIdent: NimNode): NimNode = + ## Hands `reqPtrIdent` to the FFI thread and maps the outcome to a C return code. + let sendResIdent = genSym(nskLet, "sendRes") + quote: + let `sendResIdent` = + try: + ffi_context.sendRequestToFFIThread(ctx, `reqPtrIdent`) + except Exception as exc: + Result[void, string].err("sendRequestToFFIThread exception: " & exc.msg) + if `sendResIdent`.isErr(): + let errStr = "error in sendRequestToFFIThread: " & `sendResIdent`.error + callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData) + return RET_ERR + return RET_OK + + proc buildCExportProc(params: seq[NimNode], body: NimNode): NimNode = + ## The dynlib/exportc/cdecl C-ABI wrapper both wire paths emit. + newProc( + name = postfix(cExportProcName, "*"), + params = params, + body = body, + pragmas = newTree( + nnkPragma, + ident("dynlib"), + newTree(nnkExprColonExpr, ident("exportc"), newStrLitNode(cExportName)), + ident("cdecl"), + newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)), + ), + ) + + proc buildAsyncHelperProc(): NimNode = + ## Reproduces the user's exact signature so it stays callable from Nim. + var helperParams = newSeq[NimNode]() + helperParams.add(retTypeNode) + let helperStart = if isStatic: 1 else: 2 + if not isStatic: + helperParams.add(newIdentDefs(recvName, recvType)) + for i in helperStart ..< formalParams.len: + let p = formalParams[i] + for j in 0 ..< p.len - 2: + helperParams.add(newIdentDefs(p[j], p[^2])) + newProc( + name = postfix(userProcName, "*"), + params = helperParams, + body = newStmtList(bodyNode), + pragmas = newTree(nnkPragma, ident("async")), + ) + + proc asyncPath(): NimNode = + ## Emits the C-exported wrapper and registers the FFI-thread handler. + let helperProc = buildAsyncHelperProc() + + # registerReqFFI lambda: typed params, returns user's typed Result. + let ctxHandlerName = ident("ffiCtxHandler") + let ptrFFICtx = + nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)) + + var lambdaParams = newSeq[NimNode]() + lambdaParams.add(retTypeNode) + for i in 0 ..< extraParamNames.len: + lambdaParams.add(newIdentDefs(ident(extraParamNames[i]), extraParamTypes[i])) + + let helperCall = newTree(nnkCall, userProcName) + let bindsLib = not firstIsHandle and not isStatic + if bindsLib: + let ctxMyLib = newDotExpr(newTree(nnkDerefExpr, ctxHandlerName), ident("myLib")) + helperCall.add(newTree(nnkDerefExpr, ctxMyLib)) + for name in extraParamNames: + helperCall.add(ident(name)) + + let lambdaBody = newStmtList() + if bindsLib: + lambdaBody.add(buildLibReadyGuard(ctxHandlerName, libTypeName)) + let retValIdent = ident("retVal") + lambdaBody.add quote do: + let `retValIdent` = (await `helperCall`).valueOr: + return err($error) + return ok(`retValIdent`) + + let lambdaNode = newProc( + name = newEmptyNode(), + params = lambdaParams, + body = lambdaBody, + pragmas = newTree(nnkPragma, ident("async")), + ) + + let registerReq = quote: + registerReqFFI(`reqTypeName`, `ctxHandlerName`: `ptrFFICtx`): + `lambdaNode` + + # C-exported wrapper: (ctx, callback, userData, reqCbor, reqCborLen). + let exportedParams = cExportedParams(ctxType, withCtx = not isStatic) + + let ffiBody = newStmtList() + # Flattened: the guard's `let ctx` must be a sibling of the send to be in scope. + let guard = + if isStatic: + buildStaticCtxGuard() + else: + buildCtxGuard() + for stmt in guard: + ffiBody.add(stmt) + + let reqPtrIdent = genSym(nskLet, "reqPtr") + let reqNameLit = newLit($unwrapPostfix(reqTypeName)) + ffiBody.add quote do: + let `reqPtrIdent` = FFIThreadRequest.initFromPtr( + callback, userData, cstring(`reqNameLit`), reqCbor, int(reqCborLen) + ) + ffiBody.add buildSendAndReply(reqPtrIdent) + + let ffiProc = buildCExportProc(exportedParams, ffiBody) + + ffiProcRegistry.add(procMeta) + + if abiFormat == ABIFormat.C: + # The handler unpacks through the `_CWire` companions, which only exist once every `{.ffi.}` type has been seen, so it (with the wrapper + reply trampoline) is emitted at genBindings() time (flushCAbiDispatch). The Req type stays here for the companion to name. The CBOR `ffiProc`/`ffiNewReq` aren't emitted at all. + let handlerParam = nnkExprColonExpr.newTree(ctxHandlerName, ptrFFICtx) + let handler = newStmtList( + buildProcessFFIRequestProc(reqTypeName, handlerParam, lambdaNode, ABIFormat.C), + addNewRequestToRegistry(reqTypeName, handlerParam, resultRetType, ABIFormat.C), + ) + registerCAbiProc( + isStatic, cExportName, libTypeName, reqTypeName, extraParamNames, + extraParamTypes, resultRetType, handler, + ) + return newStmtList(helperProc, buildRequestType(reqTypeName, lambdaNode)) + + return newStmtList(helperProc, registerReq, ffiProc) + + proc scalarPath(): NimNode = + ## Scalar fast path lives in `ffi_scalar`; here we only build the shared + ## dispatch pieces and hand them over. + let reqPtrIdent = genSym(nskLet, "reqPtr") + buildScalarPath( + helperProc = buildAsyncHelperProc(), + ctxGuard = buildCtxGuard(), + reqPtrIdent = reqPtrIdent, + sendAndReply = buildSendAndReply(reqPtrIdent), + userProcName = userProcName, + cExportProcName = cExportProcName, + cExportName = cExportName, + ctxType = ctxType, + camelName = camelName, + extraParamNames = extraParamNames, + extraParamTypes = extraParamTypes, + procMeta = procMeta, + ) + + let stmts = + if scalarEligible: + scalarPath() + else: + asyncPath() + + when defined(ffiDumpMacros): + echo stmts.repr + return stmts + +proc buildFFIDtorProc(prc: NimNode, abiFormat: ABIFormat): NimNode {.compileTime.} +proc buildFFIEventProc(prc: NimNode, leading: seq[NimNode]): NimNode {.compileTime.} + +macro ffi*(args: varargs[untyped]): untyped = + ## Simplified FFI macro for a type or a proc. A type registers for binding + ## generation. For a proc, `routeFFIProc` reads the signature and picks the + ## path: a context method, a static call, a synchronous export, a destructor, + ## or an event. See `ffi/internal/ffi_route.nim` for the rules. + requireBeforeGenBindings("`.ffi.`") + # Annotated node is the last vararg; leading args are `"abi = ..."` specs. + let prc = args[^1] + let leading = args[0 ..^ 2] + + # A value type stands alone (no library required); its `c` companion is emitted later by `genBindings()`, since a type-pragma macro can only return a TypeDef. + if prc.kind == nnkTypeDef: + let typeABIFormat = resolveFFISpecs(leading) + gateFFITypeABIFormat(typeABIFormat, "`.ffi.` type") + var cleanTypeDef = prc.copyNimTree() + if cleanTypeDef[0].kind == nnkPragmaExpr: + cleanTypeDef[0] = cleanTypeDef[0][0] + return registerFFITypeInfo(cleanTypeDef, typeABIFormat) + + if prc.kind notin {nnkProcDef, nnkFuncDef}: + error("`.ffi.` must be applied to a type or a proc definition") + requireLibraryDeclared("`.ffi.`") + + proc gatedABIFormat(what: string): ABIFormat = + let abiFormat = resolveFFISpecs(leading) + gateABIFormat(abiFormat, what) + return abiFormat + + case routeFFIProc(prc) + of fpEvent: + # An event may lead with a wire-name literal, which the ABI parser rejects, + # so it resolves its own specs. + return buildFFIEventProc(prc, leading) + of fpExport: + # The export crosses the ABI with its own return value, so no ABI applies. + if leading.len > 0: + error( + "`.ffi.` proc " & $procIdent(prc) & + " is a synchronous export and takes no `abi = ...` spec" + ) + return buildFFIExportProc(prc) + of fpDtor: + return buildFFIDtorProc(prc, gatedABIFormat("`.ffi.` destructor")) + of fpStatic: + return buildFFIProc(prc, gatedABIFormat("`.ffi.` static proc"), isStatic = true) + of fpMethod: + return buildFFIProc(prc, resolveFFISpecs(leading), isStatic = false) + +macro ffiStatic*(args: varargs[untyped]): untyped = + ## Context-independent `{.ffi.}`: no library receiver, and no `ctx` in the C + ## wrapper, so a host calls it without constructing the library. `{.ffi.}` + ## reaches the same path from the shape alone. + requireBeforeGenBindings("`.ffiStatic.`") + requireLibraryDeclared("`.ffiStatic.`") + let prc = args[^1] + let abiFormat = resolveFFISpecs(args[0 ..^ 2]) + gateABIFormat(abiFormat, "`.ffiStatic.` proc") + if prc.kind notin {nnkProcDef, nnkFuncDef}: + error("`.ffiStatic.` must be applied to a proc definition") + assertFFIPath(prc, fpStatic) + return buildFFIProc(prc, abiFormat, isStatic = true) + +proc buildCtorRequestType( + reqTypeName: NimNode, paramNames: seq[string], paramTypes: seq[NimNode] +): NimNode = + ## Builds the ctor's Req object using the user's actual Nim types. + var fields: seq[NimNode] = @[] + for i in 0 ..< paramNames.len: + let fieldName = ident(paramNames[i]) + let storedType = storageType(paramTypes[i]) + fields.add newTree(nnkIdentDefs, fieldName, storedType, newEmptyNode()) + + let recList = + if fields.len > 0: + newTree(nnkRecList, fields) + else: + newTree( + nnkRecList, + newTree(nnkIdentDefs, ident("_placeholder"), ident("uint8"), newEmptyNode()), + ) + + let objTy = newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), recList) + let typeName = postfix(reqTypeName, "*") + let typeSection = + newNimNode(nnkTypeSection).add(newTree(nnkTypeDef, typeName, newEmptyNode(), objTy)) + + when defined(ffiDumpMacros): + echo typeSection.repr + return typeSection + +proc buildCtorFFINewReqProc(reqTypeName: NimNode, paramNames: seq[string]): NimNode = + ## Wraps a CBOR byte buffer into an FFIThreadRequest for the ctor request type. + + var formalParams = newSeq[NimNode]() + + let typedescParam = + newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName)) + formalParams.add(typedescParam) + formalParams.add(newIdentDefs(ident("callback"), ident("FFICallBack"))) + formalParams.add(newIdentDefs(ident("userData"), ident("pointer"))) + formalParams.add(newIdentDefs(ident("reqCbor"), nnkPtrTy.newTree(ident("byte")))) + formalParams.add(newIdentDefs(ident("reqCborLen"), ident("csize_t"))) + + let retType = newTree(nnkPtrTy, ident("FFIThreadRequest")) + formalParams = @[retType] & formalParams + + let reqNameLit = newLit($unwrapPostfix(reqTypeName)) + var newBody = newStmtList() + newBody.add quote do: + return FFIThreadRequest.initFromPtr( + callback, userData, cstring(`reqNameLit`), reqCbor, int(reqCborLen) + ) + + let newReqProc = newProc( + name = postfix(ident("ffiNewReq"), "*"), + params = formalParams, + body = newBody, + pragmas = newEmptyNode(), + ) + + when defined(ffiDumpMacros): + echo newReqProc.repr + return newReqProc + +proc buildCtorBodyProc( + helperName: NimNode, + paramNames: seq[string], + paramTypes: seq[NimNode], + libTypeName: NimNode, + userBody: NimNode, +): NimNode = + let innerRetType = nnkBracketExpr.newTree( + ident("Future"), + nnkBracketExpr.newTree(ident("Result"), libTypeName, ident("string")), + ) + var innerParams = newSeq[NimNode]() + innerParams.add(innerRetType) + for i in 0 ..< paramNames.len: + innerParams.add(newIdentDefs(ident(paramNames[i]), paramTypes[i])) + + let bodyProc = newProc( + name = postfix(helperName, "*"), + params = innerParams, + body = newStmtList(userBody), + pragmas = newTree(nnkPragma, ident("async")), + ) + + when defined(ffiDumpMacros): + echo bodyProc.repr + return bodyProc + +proc buildCtorProcessFFIRequestProc( + reqTypeName: NimNode, + helperName: NimNode, + paramNames: seq[string], + paramTypes: seq[NimNode], + libTypeName: NimNode, + abi: ABIFormat, +): NimNode = + ## Materialises the Req, runs the user body, stores the library value in ctx.myLib. + let returnType = nnkBracketExpr.newTree( + ident("Future"), + nnkBracketExpr.newTree(ident("Result"), ident("string"), ident("string")), + ) + + let ctxType = + nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)) + + let typedescParam = + newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName)) + + var formalParams: seq[NimNode] = @[] + formalParams.add(returnType) + formalParams.add(typedescParam) + formalParams.add(newIdentDefs(ident("request"), ident("pointer"))) + formalParams.add(newIdentDefs(ident("ctx"), ctxType)) + + let newBody = newStmtList() + let reqIdent = ident("req") + let ctxIdent = ident("ctx") + let decodedIdent = ident("decoded") + + newBody.add reqDecodePreamble(reqTypeName, reqIdent, decodedIdent, abi) + + for i in 0 ..< paramNames.len: + newBody.add unpackReqField(ident(paramNames[i]), paramTypes[i], decodedIdent) + + let helperCallNode = newTree(nnkCall, helperName) + for name in paramNames: + helperCallNode.add(ident(name)) + + let libValIdent = ident("libVal") + newBody.add quote do: + let `libValIdent` = (await `helperCallNode`).valueOr: + return err($error) + + let myLibIdent = newDotExpr(newTree(nnkDerefExpr, ctxIdent), ident("myLib")) + let myLibOwnedIdent = newDotExpr(newTree(nnkDerefExpr, ctxIdent), ident("myLibOwned")) + let myLibRefdIdent = newDotExpr(newTree(nnkDerefExpr, ctxIdent), ident("myLibRefd")) + let libReadyIdent = newDotExpr(newTree(nnkDerefExpr, ctxIdent), ident("libReady")) + newBody.add quote do: + `myLibIdent` = createShared(`libTypeName`) + `myLibIdent`[] = `libValIdent` + `myLibOwnedIdent` = true + # Root the ref lib under refc: it lives only via this ptr in non-GC + # createShared memory, invisible to the cycle collector. freeLib unroots it. + when defined(gcRefc): + when `libTypeName` is ref: + GC_ref(`myLibIdent`[]) + `myLibRefdIdent` = true + # Set the flag after the store, so an observer never sees the fallback. + `libReadyIdent`.store(true) + + newBody.add quote do: + return ok($cast[uint](`ctxIdent`)) + + let processProc = newProc( + name = postfix(ident("processFFIRequest"), "*"), + params = formalParams, + body = newBody, + procType = nnkProcDef, + pragmas = newTree(nnkPragma, ident("async")), + ) + + when defined(ffiDumpMacros): + echo processProc.repr + return processProc + +proc addCtorRequestToRegistry( + reqTypeName, libTypeName: NimNode, abi: ABIFormat +): NimNode = + ## Wraps the ctor processFFIRequest result in a seq[byte] dispatcher; the ctor + ## returns the ctx address as a decimal string — raw UTF-8 under `abi = c`, + ## CBOR-encoded otherwise. + let ctxType = + nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)) + + let returnType = nnkBracketExpr.newTree( + ident("Future"), + nnkBracketExpr.newTree( + ident("Result"), + nnkBracketExpr.newTree(ident("seq"), ident("byte")), + ident("string"), + ), + ) + + let callExpr = newCall( + newDotExpr(reqTypeName, ident("processFFIRequest")), + ident("request"), + newTree(nnkCast, ctxType, ident("reqHandler")), + ) + + let resIdent = genSym(nskLet, "ctorRes") + let encodeRet = + if abi == ABIFormat.C: + quote: + return ok(ffiRawRetBytes(`resIdent`.value)) + else: + quote: + return ok(cborEncode(`resIdent`.value)) + + var newBody = newStmtList() + newBody.add quote do: + let `resIdent` = await `callExpr` + if `resIdent`.isErr: + return err(`resIdent`.error) + + newBody.add encodeRet + + let asyncProc = newProc( + name = newEmptyNode(), + params = @[ + returnType, + newIdentDefs(ident("request"), ident("pointer")), + newIdentDefs(ident("reqHandler"), ident("pointer")), + ], + body = newBody, + pragmas = nnkPragma.newTree(ident("async")), + ) + + let key = newLit($reqTypeName) + let regAssign = + newAssignment(newTree(nnkBracketExpr, ident("registeredRequests"), key), asyncProc) + + when defined(ffiDumpMacros): + echo regAssign.repr + return regAssign + +macro ffiCtor*(args: varargs[untyped]): untyped = + ## C-exported constructor: creates an FFIContext and fills ctx.myLib async on the + ## FFI thread. Takes Nim params (one CBOR blob), no ctx/callback/userData. Wrapper + ## returns the ctx pointer sync (NULL on failure); callback fires with its address. + requireBeforeGenBindings("`.ffiCtor.`") + requireLibraryDeclared("`.ffiCtor.`") + let prc = args[^1] + let abiFormat = resolveFFISpecs(args[0 ..^ 2]) + gateABIFormat(abiFormat, "`.ffiCtor.` proc") + + let procName = prc[0] + let formalParams = prc[3] + let bodyNode = prc[^1] + + let retTypeNode = formalParams[0] + if retTypeNode.kind == nnkEmpty: + error( + "ffiCtor: proc must have an explicit return type Future[Result[LibType, string]]" + ) + if retTypeNode.kind != nnkBracketExpr or $retTypeNode[0] != "Future": + error( + "ffiCtor: return type must be Future[Result[LibType, string]], got: " & + retTypeNode.repr + ) + let resultInner = retTypeNode[1] + if resultInner.kind != nnkBracketExpr or $resultInner[0] != "Result": + error( + "ffiCtor: return type must be Future[Result[LibType, string]], got: " & + retTypeNode.repr + ) + let libTypeName = resultInner[1] + + var paramNames: seq[string] = @[] + var paramTypes: seq[NimNode] = @[] + for i in 1 ..< formalParams.len: + let p = formalParams[i] + for j in 0 ..< p.len - 2: + rejectRawPtrType(p[^2], "`.ffiCtor.` proc " & $procName & " parameter " & $p[j]) + paramNames.add($p[j]) + paramTypes.add(p[^2]) + + let procNameStr = $procName + let cleanName = + if procNameStr.endsWith("*"): + procNameStr[0 ..^ 2] + else: + procNameStr + let cExportName = camelToSnakeCase(cleanName) + let reqTypeNameStr = snakeToPascalCase(cleanName) & "CtorReq" + let reqTypeName = ident(reqTypeNameStr) + + let typeDef = buildCtorRequestType(reqTypeName, paramNames, paramTypes) + let ffiNewReqProc = buildCtorFFINewReqProc(reqTypeName, paramNames) + var userProcName = procName + if procName.kind == nnkPostfix: + userProcName = procName[1] + # Nim ctor and C wrapper share the user's name as overloads; the wrapper's `{.exportc.}` keeps the ABI symbol. + let cExportProcName = userProcName + let helperProc = + buildCtorBodyProc(userProcName, paramNames, paramTypes, libTypeName, bodyNode) + let processProc = buildCtorProcessFFIRequestProc( + reqTypeName, userProcName, paramNames, paramTypes, libTypeName, abiFormat + ) + let addToReg = addCtorRequestToRegistry(reqTypeName, libTypeName, abiFormat) + + # C-exported proc: (reqCbor, reqCborLen, callback, userData) -> pointer + var exportedParams = newSeq[NimNode]() + exportedParams.add(ident("pointer")) + exportedParams.add(newIdentDefs(ident("reqCbor"), nnkPtrTy.newTree(ident("byte")))) + exportedParams.add(newIdentDefs(ident("reqCborLen"), ident("csize_t"))) + exportedParams.add(newIdentDefs(ident("callback"), ident("FFICallBack"))) + exportedParams.add(newIdentDefs(ident("userData"), ident("pointer"))) + + let ffiBody = newStmtList() + + ffiBody.add quote do: + when declared(initializeLibrary): + initializeLibrary() + + let ctxSym = genSym(nskLet, "ctx") + let poolIdent = ident($libTypeName & "FFIPool") + + ffiBody.add quote do: + let `ctxSym` = `poolIdent`.createFFIContext().valueOr: + if not callback.isNil: + let errStr = "ffiCtor: failed to create FFIContext: " & $error + callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData) + return nil + + # Early validation: decode the CBOR payload to verify it parses cleanly. + ffiBody.add quote do: + block: + let validateRes = cborDecodePtr( + cast[ptr UncheckedArray[byte]](reqCbor), int(reqCborLen), `reqTypeName` + ) + if validateRes.isErr(): + if not callback.isNil: + let errStr = "ffiCtor: failed to decode request: " & $validateRes.error + callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData) + return nil + + let newReqCall = newCall( + ident("ffiNewReq"), + reqTypeName, + ident("callback"), + ident("userData"), + ident("reqCbor"), + ident("reqCborLen"), + ) + + let sendCall = + newCall(newDotExpr(ctxSym, ident("sendRequestToFFIThread")), newReqCall) + + let sendResIdent = genSym(nskLet, "sendRes") + ffiBody.add quote do: + let `sendResIdent` = + try: + `sendCall` + except Exception as exc: + Result[void, string].err("sendRequestToFFIThread exception: " & exc.msg) + if `sendResIdent`.isErr(): + if not callback.isNil: + let errStr = "ffiCtor: failed to send request: " & $`sendResIdent`.error + callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData) + return nil + + ffiBody.add quote do: + return cast[pointer](`ctxSym`) + + let ffiProc = newProc( + name = postfix(cExportProcName, "*"), + params = exportedParams, + body = ffiBody, + pragmas = newTree( + nnkPragma, + ident("dynlib"), + newTree(nnkExprColonExpr, ident("exportc"), newStrLitNode(cExportName)), + ident("cdecl"), + newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)), + ), + ) + + block: + var ctorExtraParams: seq[FFIParamMeta] = @[] + for i in 0 ..< paramNames.len: + let ptype = paramTypes[i] + let isPointer = isPtr(ptype) + let tn = + if isPointer: + nimTypeNameRepr(ptype[0]) + else: + nimTypeNameRepr(ptype) + ctorExtraParams.add( + FFIParamMeta(name: paramNames[i], typeName: tn, isPtr: isPointer) + ) + ffiProcRegistry.add( + FFIProcMeta( + procName: cExportName, + libName: currentLibName, + kind: FFIKind.CTOR, + libTypeName: $libTypeName, + extraParams: ctorExtraParams, + returnTypeName: $libTypeName, + returnIsPtr: false, + abiFormat: abiFormat, + doc: extractDocComment(prc), + ) + ) + + let poolDecl = quote: + when not declared(`poolIdent`): + var `poolIdent`: FFIContextPool[`libTypeName`] + + let stmts = + if abiFormat == ABIFormat.C: + # The `abi = c` handler + wrapper are emitted at genBindings() time (the handler unpacks through the `_CWire` companions); the CBOR `ffiProc`/`ffiNewReq` aren't emitted at all. + registerCAbiCtor( + cExportName, + libTypeName, + reqTypeName, + paramNames, + paramTypes, + newStmtList(processProc, addToReg), + ) + newStmtList(typeDef, helperProc, poolDecl) + else: + newStmtList( + typeDef, ffiNewReqProc, helperProc, processProc, addToReg, poolDecl, ffiProc + ) + + when defined(ffiDumpMacros): + echo stmts.repr + return stmts + +proc buildFFIDtorProc(prc: NimNode, abiFormat: ABIFormat): NimNode {.compileTime.} = + ## Emits the C-exported FFIContext destructor. `{.ffi.}` and `{.ffiDtor.}` + ## share it. + let procName = prc[0] + let formalParams = prc[3] + let bodyNode = prc[^1] + + if formalParams.len < 2: + error("ffiDtor: proc must have exactly one parameter (w: LibType)") + + let libParamName = formalParams[1][0] + let libTypeName = formalParams[1][1] + + # A dtor is sync (no return) or async (`Future[void]`); reject anything else. + let retTypeNode = formalParams[0] + let retIsFutureVoid = + retTypeNode.kind == nnkBracketExpr and $retTypeNode[0] == "Future" and + retTypeNode.len == 2 and $retTypeNode[1] == "void" + if retTypeNode.kind != nnkEmpty and not retIsFutureVoid: + error( + "ffiDtor: proc must return nothing (sync) or Future[void] (async), got: " & + retTypeNode.repr + ) + + let procNameStr = block: + let raw = $procName + if raw.endsWith("*"): + raw[0 ..^ 2] + else: + raw + let cExportName = camelToSnakeCase(procNameStr) + # The dtor only emits a C wrapper and uses the user's name directly (no Nim-facing helper to overload against). + var cExportProcName = procName + if procName.kind == nnkPostfix: + cExportProcName = procName[1] + + let destroyResIdent = genSym(nskLet, "destroyRes") + + let ffiBody = newStmtList() + + ffiBody.add quote do: + when declared(initializeLibrary): + initializeLibrary() + + ffiBody.add quote do: + if ctx.isNil or cast[ptr FFIContext[`libTypeName`]](ctx)[].myLib.isNil: + return RET_ERR + + let isNoop = + bodyNode.kind == nnkEmpty or ( + bodyNode.kind == nnkStmtList and bodyNode.len == 1 and + bodyNode[0].kind == nnkDiscardStmt + ) + + # Lift the body into an async `ffiTeardownHook` the FFI thread awaits at shutdown; the C wrapper no longer runs the body. + let teardownImplName = genSym(nskProc, "ffiTeardownImpl") + let teardownRegistration = + if isNoop: + newEmptyNode() + else: + quote: + proc `teardownImplName`(lib: ptr `libTypeName`): Future[void] {.async.} = + let `libParamName` = lib[] + `bodyNode` + + ffiTeardownHook[`libTypeName`]() = `teardownImplName` + + let poolIdent = ident($libTypeName & "FFIPool") + ffiBody.add quote do: + let `destroyResIdent` = + `poolIdent`.recycleFFIContext(cast[ptr FFIContext[`libTypeName`]](ctx)) + if `destroyResIdent`.isErr(): + return RET_ERR + + ffiBody.add quote do: + return RET_OK + + let ffiProc = newProc( + name = postfix(cExportProcName, "*"), + params = @[ident("cint"), newIdentDefs(ident("ctx"), ident("pointer"))], + body = ffiBody, + pragmas = newTree( + nnkPragma, + ident("dynlib"), + newTree(nnkExprColonExpr, ident("exportc"), newStrLitNode(cExportName)), + ident("cdecl"), + newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)), + ), + ) + + ffiProcRegistry.add( + FFIProcMeta( + procName: cExportName, + libName: currentLibName, + kind: FFIKind.DTOR, + libTypeName: $libTypeName, + extraParams: @[], + returnTypeName: "", + returnIsPtr: false, + abiFormat: abiFormat, + doc: extractDocComment(prc), + ) + ) + + let poolDecl = quote: + when not declared(`poolIdent`): + var `poolIdent`: FFIContextPool[`libTypeName`] + + let stmts = newStmtList(teardownRegistration, poolDecl, ffiProc) + + when defined(ffiDumpMacros): + echo stmts.repr + return stmts + +macro ffiDtor*(args: varargs[untyped]): untyped = + ## C-exported FFIContext destructor. Sync (no return) or async (`Future[void]`); + ## a non-empty body becomes an async `ffiTeardownHook` the FFI thread awaits at + ## shutdown, so teardown runs on the worker thread. RET_ERR on null/invalid ctx. + ## `{.ffi.}` reaches the same path from the shape alone. + requireBeforeGenBindings("`.ffiDtor.`") + requireLibraryDeclared("`.ffiDtor.`") + let prc = args[^1] + let abiFormat = resolveABIFormat(args[0 ..^ 2]) + gateABIFormat(abiFormat, "`.ffiDtor.` proc") + assertFFIPath(prc, fpDtor) + return buildFFIDtorProc(prc, abiFormat) + +proc buildFFIEventProc(prc: NimNode, leading: seq[NimNode]): NimNode {.compileTime.} = + ## Emits the event dispatcher. `{.ffi.}` and `{.ffiEvent.}` share it. + ## One parameter rides the wire directly (a scalar, or an existing `{.ffi.}` + ## object). Two or more are bundled into a synthesised, registered envelope + ## object named `Payload` whose fields are the parameters, + ## so the foreign side still decodes one typed value. + let procName = prc[0] + var userProcName = procName + if procName.kind == nnkPostfix: + userProcName = procName[1] + + let (wireName, abiSpecStart) = resolveEventWireName(leading, userProcName) + let abiFormat = resolveABIFormat(leading[abiSpecStart ..^ 1]) + gateABIFormat(abiFormat, "`.ffiEvent.` proc") + if abiFormat == ABIFormat.C: + error( + "`.ffiEvent.` proc: the `c` ABI does not yet support events; declare the " & + "event with `abi = cbor` (events still ride CBOR internally)" + ) + + let formalParams = prc[3] + + if formalParams.len < 2: + error("ffiEvent requires at least one parameter") + + # Flatten the parameter list (a grouped `a, b: T` expands to one entry each). + var paramNames: seq[NimNode] = @[] + var paramTypes: seq[NimNode] = @[] + for i in 1 ..< formalParams.len: + let p = formalParams[i] + for j in 0 ..< p.len - 2: + rejectRawPtrType( + p[^2], "`.ffiEvent.` proc " & $userProcName & " parameter " & $p[j] + ) + paramNames.add(p[j]) + paramTypes.add(p[^2]) + + let wireNameLit = newStrLitNode(wireName) + let resultStmts = newStmtList() + + var payloadTypeNameStr: string + var dispatchPayload: NimNode + + if paramNames.len == 1: + let payloadTypeNode = paramTypes[0] + payloadTypeNameStr = + if payloadTypeNode.kind == nnkIdent: + $payloadTypeNode + else: + payloadTypeNode.repr + dispatchPayload = paramNames[0] + else: + # Synthesise + register an envelope object, then dispatch an instance built + # from the parameters. + let payloadType = ident(snakeToPascalCase(wireName) & "Payload") + payloadTypeNameStr = $payloadType + + var paramNameStrs: seq[string] = @[] + for n in paramNames: + paramNameStrs.add($n) + let typeSection = buildCtorRequestType(payloadType, paramNameStrs, paramTypes) + discard registerFFITypeInfo(typeSection[0], abiFormat) + resultStmts.add(typeSection) + + let envelope = nnkObjConstr.newTree(payloadType) + for i in 0 ..< paramNames.len: + # `cstring` rides as `string` in the envelope (per storageType). + let value = + if paramTypes[i].kind == nnkIdent and $paramTypes[i] == "cstring": + newCall(ident("$"), paramNames[i]) + else: + paramNames[i] + envelope.add(nnkExprColonExpr.newTree(paramNames[i], value)) + dispatchPayload = envelope + + let dispatchBody = + newStmtList(newCall(ident("dispatchFFIEventCbor"), wireNameLit, dispatchPayload)) + + var newParams = newSeq[NimNode]() + newParams.add(formalParams[0]) + for i in 1 ..< formalParams.len: + newParams.add(formalParams[i]) + + let pragmas = + if prc.len >= 5 and prc[4].kind != nnkEmpty: + prc[4] + else: + newEmptyNode() + + let generated = newProc( + name = procName, + params = newParams, + body = dispatchBody, + procType = prc.kind, + pragmas = pragmas, + ) + resultStmts.add(generated) + + ffiEventRegistry.add( + FFIEventMeta( + wireName: wireName, + nimProcName: $userProcName, + libName: currentLibName, + payloadTypeName: payloadTypeNameStr, + abiFormat: abiFormat, + doc: extractDocComment(prc), + ) + ) + + when defined(ffiDumpMacros): + echo resultStmts.repr + return resultStmts + +macro ffiEvent*(args: varargs[untyped]): untyped = + ## Declares a library-initiated event: the empty-bodied proc is filled with a + ## `dispatchFFIEventCbor` call. Wire name defaults to `camelToSnakeCase` of the + ## proc name (a string literal overrides it) and is the cross-binding source of truth. + ## `{.ffi.}` reaches the same path from the shape alone. + requireBeforeGenBindings("`.ffiEvent.`") + requireLibraryDeclared("`.ffiEvent.`") + if args.len < 1: + error("ffiEvent must be applied to a proc declaration") + + let prc = args[^1] + if prc.kind notin {nnkProcDef, nnkFuncDef}: + error("ffiEvent must be applied to a proc declaration") + assertFFIPath(prc, fpEvent) + return buildFFIEventProc(prc, args[0 ..^ 2]) + +proc reportScalarFastPathDrops(procs: seq[FFIProcMeta]) {.compileTime.} = + ## Fail loudly on scalar-fast-path procs a target can't bind, unless + ## `-d:ffiAllowScalarSkip` downgrades it to a hint. + var skipped: seq[string] = @[] + for p in procs: + if p.scalarFastPath: + skipped.add(p.procName) + if skipped.len == 0: + return + if ffiAllowScalarSkip: + for name in skipped: + hint( + "genBindings: omitting scalar-fast-path proc '" & name & + "' from the bindings (-d:ffiAllowScalarSkip)" + ) + return + error( + """genBindings: this target has no foreign-binding codegen for scalar-fast-path +`abi = c` procs, so these would be silently omitted from the generated bindings: +$1 +They are emitted only into the `abi = c` C header (an `abi = c` library generated +with -d:targetLang=c). +Fix by one of: + - make the library `abi = c` (declareLibrary(..., "c")) and generate C bindings, or + - switch the proc to `abi = cbor`, or + - add a non-scalar param (e.g. a struct or handle) so it takes the CBOR wire shape, or + - pass -d:ffiAllowScalarSkip to accept the omission.""" % + [skipped.join(", ")] + ) + +proc bindingsOutputDir(lang, explicit: string): string {.compileTime.} = + ## Output dir for `lang`; defaults to `_bindings/` next to the compiled + ## source, or an explicit -d:ffiOutputDir override. + if explicit.len > 0: + explicit + else: + return querySetting(SingleValueSetting.projectPath) / (lang & "_bindings") + +proc bindingsSrcPath(outDir, explicit: string): string {.compileTime.} = + ## Nim source path embedded in build files, relative to `outDir`; defaults to + ## the compiled file, or an explicit -d:ffiSrcPath override. + if explicit.len > 0: + explicit + else: + relativePath(querySetting(SingleValueSetting.projectFull), outDir) + +when defined(ffiGenBindings): + proc emitBindingsFor( + lang: string, genProcs: seq[FFIProcMeta], libName, outDir, srcRel: string + ) {.compileTime.} = + ## Route one language token to its generator; unknown tokens error. + case lang + of "rust": + generateRustCrate( + genProcs, ffiTypeRegistry, libName, outDir, srcRel, ffiEventRegistry, + ffiConstRegistry, + ) + of "cpp", "c++": + generateCppBindings( + genProcs, ffiTypeRegistry, libName, outDir, srcRel, ffiEventRegistry, + ffiConstRegistry, + ) + of "c": + generateCBindings( + genProcs, ffiTypeRegistry, libName, outDir, srcRel, ffiEventRegistry, + ffiConstRegistry, + ) + of "cddl": + generateCddlBindings(genProcs, ffiTypeRegistry, libName, outDir, srcRel) + else: + error( + "genBindings: unknown targetLang '" & lang & + "'. Use 'rust', 'cpp', 'c', or 'cddl'." + ) + +macro genBindings*( + outputDir: static[string] = ffiOutputDir, nimSrcRelPath: static[string] = ffiSrcPath +): untyped = + ## Emits binding files from the compile-time FFI registries. MUST be called AFTER + ## every {.ffi.}/{.ffiCtor.}/{.ffiDtor.} annotation, so place it at the compilation + ## root's bottom. -d:targetLang picks languages; emission needs -d:ffiGenBindings. + genBindingsEmitted = true + + when defined(ffiGenBindings): + let libName = deriveLibName(ffiProcRegistry) + for rawLang in targetLang.split(','): + let lang = string_helpers.toLower(rawLang.strip()) + if lang.len == 0: + continue + # The `abi = c` C header is the only output with scalar-fast-path codegen. + let emitsScalars = lang == "c" and currentDefaultABIFormat == ABIFormat.C + let genProcs = + if emitsScalars: + ffiProcRegistry + else: + bindableProcs(ffiProcRegistry) + if not emitsScalars: + reportScalarFastPathDrops(ffiProcRegistry) + let outDir = bindingsOutputDir(lang, outputDir) + emitBindingsFor( + lang, genProcs, libName, outDir, bindingsSrcPath(outDir, nimSrcRelPath) + ) + + let emitted = flushCWireCompanions() + for node in flushCAbiDispatch(): + emitted.add(node) + when defined(ffiDumpMacros): + echo emitted.repr + emitted diff --git a/wasm-deps/ffi/ffi/internal/ffi_route.nim b/wasm-deps/ffi/ffi/internal/ffi_route.nim new file mode 100644 index 000000000..fe733863d --- /dev/null +++ b/wasm-deps/ffi/ffi/internal/ffi_route.nim @@ -0,0 +1,93 @@ +## Picks the FFI path of a proc from the shape of its signature. +## +## `{.ffi.}`, `{.ffiStatic.}`, `{.ffiExport.}`, `{.ffiDtor.}` and `{.ffiEvent.}` +## own five disjoint shapes, so one router serves all five. Each shape that the +## router claims fails to compile under any other pragma today, so the router +## only turns a compile error into the meaning the writer intended. +## +## `{.ffiCtor.}` stays explicit, because its shape is not free. A ctor differs +## from a static call by one token: the type inside `Result`. A static call that +## returns the library type builds today and exports a working C symbol. A +## router would silently give it the ctor ABI instead. + +import std/macros +import ../codegen/meta +import ./ffi_codegen_common + +type FFIPath* = enum + fpMethod ## A library or handle receiver, and an async result. + fpStatic ## No receiver, and an async result. + fpExport ## No arguments, and a synchronous result. + fpDtor ## A library receiver, and no result. + fpEvent ## A payload parameter, and no result. + +func pathPragma*(path: FFIPath): string = + case path + of fpMethod: "`.ffi.`" + of fpStatic: "`.ffiStatic.`" + of fpExport: "`.ffiExport.`" + of fpDtor: "`.ffiDtor.`" + of fpEvent: "`.ffiEvent.`" + +func pathShape*(path: FFIPath): string = + case path + of fpMethod: + "the first parameter is the library type or an {.ffiHandle.} type, and the " & + "return type is Future[Result[T, string]]" + of fpStatic: + "there is no library receiver, and the return type is Future[Result[T, string]]" + of fpExport: + "there are no parameters, and the return type is a plain Nim type" + of fpDtor: + "there is one library parameter, and the return type is nothing or Future[void]" + of fpEvent: + "there is a payload parameter that is not the library type, and there is no result" + +func isFuture(t: NimNode): bool = + return + t.kind == nnkBracketExpr and t.len == 2 and t[0].kind == nnkIdent and + $t[0] == "Future" + +func isFutureVoid(t: NimNode): bool = + return isFuture(t) and t[1].kind == nnkIdent and $t[1] == "void" + +proc isLibReceiver(t: NimNode): bool {.compileTime.} = + ## The receiver is the type that `declareLibrary` recorded, or a handle type. + if t.kind != nnkIdent: + return false + return ($t == currentLibType and currentLibType.len > 0) or isFFIHandleTypeName($t) + +proc routeFFIProc*(prc: NimNode): FFIPath {.compileTime.} = + ## Reads the receiver and the return type, then names the path. + let params = prc.params + let ret = params[0] + let hasReceiver = params.len > 1 and isLibReceiver(params[1][1]) + + if hasReceiver: + return if ret.kind == nnkEmpty or isFutureVoid(ret): fpDtor else: fpMethod + if params.len == 1 and not isFuture(ret): + return fpExport + # A static call always returns Future[Result[T, string]], so a payload + # parameter with no result can only be an event. + if params.len > 1 and ret.kind == nnkEmpty: + return fpEvent + return fpStatic + +proc assertFFIPath*(prc: NimNode, want: FFIPath) {.compileTime.} = + ## Guards an explicit pragma against a signature that routes elsewhere. + let got = routeFFIProc(prc) + if got == want: + return + let name = $procIdent(prc) + # A receiver is the one mismatch a caller can read straight off the signature. + if want == fpStatic and got == fpMethod: + error( + "`.ffiStatic.` proc " & name & " takes " & prc.params[1][1].repr & + " as its first parameter, which is the library type or an {.ffiHandle.} type. " & + "A receiver belongs to a context. Make it an `{.ffi.}` method instead." + ) + error( + pathPragma(want) & " proc " & name & " has the shape of a " & pathPragma(got) & + " proc. Use " & pathPragma(got) & " here, or make sure that " & pathShape(want) & + "." + ) diff --git a/wasm-deps/ffi/ffi/internal/ffi_scalar.nim b/wasm-deps/ffi/ffi/internal/ffi_scalar.nim new file mode 100644 index 000000000..de10b1220 --- /dev/null +++ b/wasm-deps/ffi/ffi/internal/ffi_scalar.nim @@ -0,0 +1,154 @@ +## CBOR-free scalar fast path for all-scalar `{.ffi: "abi = c".}` methods. + +import std/macros +import ../codegen/meta +import ./ffi_codegen_common + +const scalarPodTypeNames = [ + "int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", + "uint64", "byte", "float", "float32", "float64", "bool", +] + ## Fixed-width POD scalars that survive the async hop by value; `cstring`/ + ## `string` are excluded as params (they alias caller memory read after return). + +func isScalarParamTypeName*(name: string): bool = + name in scalarPodTypeNames + +func isScalarReturnTypeName*(name: string): bool = + ## Unlike params, a `string`/`cstring` return is fine: the bytes ride back raw. + name in scalarPodTypeNames or name == "string" or name == "cstring" + +func isScalarOnly*(p: FFIProcMeta): bool = + ## True iff every wire param and return of `p` is scalar. Handles and raw + ## pointers are excluded. + if p.kind != FFIKind.FFI: + return false + if p.returnIsPtr or p.returnIsHandle: + return false + if not isScalarReturnTypeName(p.returnTypeName): + return false + for ep in p.extraParams: + if ep.isPtr or ep.isHandle or not isScalarParamTypeName(ep.typeName): + return false + true + +func bindableProcs*(procs: seq[FFIProcMeta]): seq[FFIProcMeta] = + ## Procs the CBOR-speaking generators emit for; scalar-fast-path procs are + ## dropped (their inline-scalar export doesn't match the CBOR codegen shape). + ## The `abi = c` C header binds the full registry instead. + var kept: seq[FFIProcMeta] = @[] + for p in procs: + if not p.scalarFastPath: + kept.add(p) + kept + +proc buildScalarPath*( + helperProc, ctxGuard, reqPtrIdent, sendAndReply: NimNode, + userProcName, cExportProcName: NimNode, + cExportName: string, + ctxType: NimNode, + camelName: string, + extraParamNames: seq[string], + extraParamTypes: seq[NimNode], + procMeta: FFIProcMeta, +): NimNode {.compileTime.} = + ## Emits the scalar-fast-path codegen for one `.ffi.` proc; the caller supplies + ## the generic dispatch pieces, this owns the inline pack/unpack/raw-bytes wiring. + let scalarReqKey = camelName & "Req" + + let reqIdent = genSym(nskLet, "ffiReq") + let ctxHandlerName = genSym(nskLet, "ffiCtxHandler") + let handlerBody = newStmtList() + handlerBody.add quote do: + let `reqIdent` = cast[ptr FFIThreadRequest](request) + let `ctxHandlerName` = cast[`ctxType`](reqHandler) + + # ctxType is `ptr FFIContext[LibType]`; the guard needs the library type. + handlerBody.add(buildLibReadyGuard(ctxHandlerName, ctxType[0][1])) + + let helperCall = newTree(nnkCall, userProcName) + let ctxMyLib = newDotExpr(newTree(nnkDerefExpr, ctxHandlerName), ident("myLib")) + helperCall.add(newTree(nnkDerefExpr, ctxMyLib)) + for i in 0 ..< extraParamNames.len: + let argIdent = ident(extraParamNames[i]) + let slot = nnkBracketExpr.newTree( + newDotExpr(newTree(nnkDerefExpr, reqIdent), ident("scalarArgs")), newLit(i) + ) + handlerBody.add( + newLetStmt(argIdent, newCall(ident("ffiUnpackScalar"), slot, extraParamTypes[i])) + ) + helperCall.add(argIdent) + + let retValIdent = genSym(nskLet, "retVal") + handlerBody.add quote do: + let `retValIdent` = (await `helperCall`).valueOr: + return err(error) + return ok(ffiRawRetBytes(`retValIdent`)) + + let seqByteResult = nnkBracketExpr.newTree( + ident("Future"), + nnkBracketExpr.newTree( + ident("Result"), + nnkBracketExpr.newTree(ident("seq"), ident("byte")), + ident("string"), + ), + ) + let handlerProc = newProc( + name = newEmptyNode(), + params = @[ + seqByteResult, + newIdentDefs(ident("request"), ident("pointer")), + newIdentDefs(ident("reqHandler"), ident("pointer")), + ], + body = handlerBody, + pragmas = nnkPragma.newTree(ident("async")), + ) + let registerAssign = newAssignment( + nnkBracketExpr.newTree(ident("registeredRequests"), newLit(scalarReqKey)), + handlerProc, + ) + + var scalarParams = @[ + ident("cint"), + newIdentDefs(ident("ctx"), ctxType), + newIdentDefs(ident("callback"), ident("FFICallBack")), + newIdentDefs(ident("userData"), ident("pointer")), + ] + for i in 0 ..< extraParamNames.len: + scalarParams.add(newIdentDefs(ident(extraParamNames[i]), extraParamTypes[i])) + + let ffiBody = newStmtList() + ffiBody.add ctxGuard + + let initScalarCall = newTree( + nnkCall, + newDotExpr(ident("FFIThreadRequest"), ident("initScalar")), + ident("callback"), + ident("userData"), + newDotExpr(newLit(scalarReqKey), ident("cstring")), + ) + for i in 0 ..< extraParamNames.len: + initScalarCall.add(newCall(ident("ffiPackScalar"), ident(extraParamNames[i]))) + + ffiBody.add newLetStmt(reqPtrIdent, initScalarCall) + ffiBody.add sendAndReply + + let ffiProc = newProc( + name = postfix(cExportProcName, "*"), + params = scalarParams, + body = ffiBody, + pragmas = newTree( + nnkPragma, + ident("dynlib"), + newTree(nnkExprColonExpr, ident("exportc"), newStrLitNode(cExportName)), + ident("cdecl"), + newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)), + ), + ) + + # Registered so metadata stays introspectable; `bindableProcs` drops it later. + var scalarMeta = procMeta + scalarMeta.scalarFastPath = true + ffiProcRegistry.add(scalarMeta) + + newStmtList(helperProc, registerAssign, ffiProc) diff --git a/wasm-deps/ffi/ffi/logging.nim b/wasm-deps/ffi/ffi/logging.nim index b82ec117a..2350e8831 100644 --- a/wasm-deps/ffi/ffi/logging.nim +++ b/wasm-deps/ffi/ffi/logging.nim @@ -1,6 +1,4 @@ -## This code has been copied and addapted from `status-im/nimbu-eth2` project. -## Link: https://github.com/status-im/nimbus-eth2/blob/c585b0a5b1ae4d55af38ad7f4715ad455e791552/beacon_chain/nimbus_binary_common.nim -## This is also copied in logos-messaging-nim repository (2025-12-10) +## Adapted from status-im/nimbus-eth2 nimbus_binary_common.nim. import std/[typetraits, os, strutils, syncio], chronicles, @@ -15,11 +13,8 @@ type LogFormat* = enum TEXT JSON -## Utils - proc stripAnsi(v: string): string = - ## Copied from: https://github.com/status-im/nimbus-eth2/blob/stable/beacon_chain/nimbus_binary_common.nim#L41 - ## Silly chronicles, colors is a compile-time property + ## chronicles colors are a compile-time property, so strip ANSI at runtime. var res = newStringOfCap(v.len) i: int @@ -31,14 +26,14 @@ proc stripAnsi(v: string): string = x = i + 1 found = false - while x < v.len: # look for [..m + while x < v.len: let c2 = v[x] if x == i + 1: if c2 != '[': break else: if c2 in {'0' .. '9'} + {';'}: - discard # keep looking + discard elif c2 == 'm': i = x + 1 found = true @@ -47,7 +42,7 @@ proc stripAnsi(v: string): string = break inc x - if found: # skip adding c + if found: continue res.add c inc i @@ -58,13 +53,11 @@ proc writeAndFlush(f: syncio.File, s: LogOutputStr) = try: f.write(s) f.flushFile() - except CatchableError: + except IOError: logLoggingFailure(cstring(s), getCurrentException()) -## Setup - proc setupLogLevel(level: LogLevel) = - # TODO: Support per topic level configuratio + # TODO: Support per topic level configuration topics_registry.setLogLevel(level) proc setupLogFormat(format: LogFormat, color = true) = @@ -94,12 +87,11 @@ proc setupLogFormat(format: LogFormat, color = true) = .} proc setupLog*(level: LogLevel, format: LogFormat) = - ## Logging setup # Adhere to NO_COLOR initiative: https://no-color.org/ let color = try: not parseBool(os.getEnv("NO_COLOR", "false")) - except CatchableError: + except ValueError: true setupLogLevel(level) diff --git a/wasm-deps/ffi/nimblemeta.json b/wasm-deps/ffi/nimblemeta.json index 7dfc5b79e..0c8977dbc 100644 --- a/wasm-deps/ffi/nimblemeta.json +++ b/wasm-deps/ffi/nimblemeta.json @@ -3,21 +3,66 @@ "metaData": { "url": "https://github.com/logos-messaging/nim-ffi", "downloadMethod": "git", - "vcsRevision": "06111de155253b34e47ed2aaed1d61d08d62cc1b", + "vcsRevision": "53515de17af0ef3e88b2aec9675b8163dddc14ae", "files": [ - "/ffi.nim", - "/ffi/ffi_types.nim", - "/ffi.nimble", - "/ffi/ffi_thread_request.nim", - "/ffi/alloc.nim", - "/ffi/logging.nim", + "/ffi/codegen/templates/c/header_prelude.h.tpl", + "/ffi/codegen/templates/cpp/vendor/tinycbor/LICENSE", "/ffi/internal/ffi_library.nim", + "/ffi/codegen/templates/cpp/vendor/tinycbor/cborerrorstrings.c", + "/ffi/codegen/templates/cpp/CMakeLists.txt.tpl", + "/ffi/internal/ffi_export.nim", + "/ffi/ffi_handles.nim", + "/ffi/ffi_request_queue.nim", + "/ffi/codegen/c_cpp_common.nim", + "/ffi/ffi_types.nim", + "/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser_dup_string.c", + "/ffi/event_thread.nim", + "/ffi/codegen/cpp.nim", + "/ffi/codegen/cddl.nim", + "/ffi/internal/c_macro_helpers.nim", + "/ffi/codegen/templates/cpp/vendor/tinycbor/tinycbor-version.h", + "/ffi/internal/ffi_codegen_common.nim", + "/ffi/codegen/templates/cpp/vendor/tinycbor/cborinternal_p.h", + "/ffi/cbor_serial.nim", + "/ffi/codegen/templates/nim_ffi_lib.cmake", + "/ffi/codegen/templates/cpp/vendor/tinycbor/cborparser.c", + "/ffi/codegen/templates/c/cbor_helpers.h.tpl", + "/ffi.nim", + "/ffi/codegen/templates/cpp/result.hpp.tpl", + "/ffi/alloc.nim", + "/ffi/codegen/rust.nim", + "/ffi/codegen/string_helpers.nim", + "/ffi/ffi_thread_request.nim", + "/ffi/codegen/templates/cpp/vendor/tinycbor/utf8_p.h", + "/ffi/ffi_thread.nim", + "/ffi/codegen/meta.nim", + "/ffi/codegen/consts.nim", + "/ffi/codegen/templates/cpp/sync_call_helper.hpp.tpl", + "/ffi/codegen/types_ir.nim", + "/ffi/codegen/templates/c/CMakeLists_abi.txt.tpl", + "/ffi/codegen/templates/cpp/vendor/tinycbor/cbor.h", + "/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder_close_container_checked.c", + "/ffi/ffi_context.nim", + "/ffi/codegen/templates/cpp/vendor/tinycbor/cborencoder.c", + "/ffi.nimble", + "/ffi/internal/ffi_scalar.nim", + "/ffi/codegen/templates/c/CMakeLists.txt.tpl", + "/ffi/codegen/templates/cpp/cbor_helpers.hpp.tpl", + "/ffi/ffi_events.nim", + "/ffi/codegen/c.nim", "/ffi/internal/ffi_macro.nim", - "/ffi/ffi_context.nim" + "/ffi/codegen/templates/cpp/header_prelude.hpp.tpl", + "/ffi/codegen/templates/cpp/vendor/tinycbor/compilersupport_p.h", + "/ffi/codegen/templates/cpp/context_rule_of_5.hpp.tpl", + "/ffi/ffi_context_pool.nim", + "/ffi/internal/c_wire.nim", + "/ffi/logging.nim", + "/ffi/internal/ffi_route.nim" ], "binaries": [], "specialVersions": [ - "0.1.3" + "0.3.0", + "#53515de17af0ef3e88b2aec9675b8163dddc14ae" ] } } \ No newline at end of file