mirror of
https://github.com/logos-messaging/logos-messaging-nim.git
synced 2026-08-26 07:51:09 +00:00
build(wasm): commit wasm-deps, the browser edge build's own dependencies
These were never tracked, yet nothing in library/edge compiles without them: - edge_builders.nim — a standalone copy of libp2p's SwitchBuilder with QUIC, autotls and ws-transport stripped out. libp2p/builders pulls lsquic and boringssl, neither of which builds for wasm, and Nim resolves that import to the real package regardless of --path overrides, so bypassing it needs a separate module rather than a flag. - the patched ffi + shim headers the emscripten build compiles against. Leaving them untracked meant the browser edge node was one `rm -rf` from being unrecoverable, and that a fresh clone could never reproduce the artifact. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M321nwYww2xHYUsVyXZBxi
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
import
|
||||
brokers/
|
||||
[event_broker, request_broker, multi_request_broker, broker_context, api_library]
|
||||
export event_broker, request_broker, multi_request_broker, broker_context, api_library
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[strutils, concurrency/atomics], chronos
|
||||
|
||||
type BrokerContext* = distinct uint32
|
||||
|
||||
func `==`*(a, b: BrokerContext): bool =
|
||||
uint32(a) == uint32(b)
|
||||
|
||||
func `!=`*(a, b: BrokerContext): bool =
|
||||
uint32(a) != uint32(b)
|
||||
|
||||
func `$`*(bc: BrokerContext): string =
|
||||
toHex(uint32(bc), 8)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Context split — a BrokerContext packs two uint16 halves:
|
||||
# bits [15:0] classCtx — which broker-object/interface scope ("global"
|
||||
# context). 0 = reserved (nil/invalid), 1 = the
|
||||
# default base scope, 2..0xFFFE = allocated,
|
||||
# 0xFFFF = reserved guard.
|
||||
# bits [31:16] instanceCtx — which instance of that scope. 0 = flat /
|
||||
# class-level (no specific instance), 1..0xFFFF =
|
||||
# OOP-owned instances.
|
||||
# Bucket lookup remains keyed by the full uint32; the split is semantic.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
func classCtx*(bc: BrokerContext): uint16 =
|
||||
uint16(uint32(bc) and 0xFFFF'u32)
|
||||
|
||||
func instanceCtx*(bc: BrokerContext): uint16 =
|
||||
uint16((uint32(bc) shr 16) and 0xFFFF'u32)
|
||||
|
||||
func makeBrokerContext*(classCtx, instanceCtx: uint16): BrokerContext =
|
||||
BrokerContext((uint32(instanceCtx) shl 16) or uint32(classCtx))
|
||||
|
||||
const DefaultBrokerContext* = makeBrokerContext(1'u16, 0'u16)
|
||||
## 0x0000_0001 —
|
||||
## the base "global" flat scope (classCtx 1, instance 0). Deliberately not
|
||||
## 0x0 so an unset/nil context is distinguishable from the default.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Thread-global broker context
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Each thread has its own BrokerContext value (threadvar).
|
||||
# Defaults to DefaultBrokerContext until explicitly set via
|
||||
# setThreadBrokerContext or initThreadBrokerContext.
|
||||
#
|
||||
# NOTE: Module-level threadvar assignments only execute on the main thread.
|
||||
# Secondary threads get zero-initialized threadvars, so we use a flag to
|
||||
# lazily initialize on first access.
|
||||
|
||||
var globalBrokerContextLock {.threadvar.}: AsyncLock
|
||||
globalBrokerContextLock = newAsyncLock()
|
||||
var globalBrokerContextValue {.threadvar.}: BrokerContext
|
||||
globalBrokerContextValue = DefaultBrokerContext
|
||||
var globalBrokerContextInitialized {.threadvar.}: bool
|
||||
globalBrokerContextInitialized = true # main thread is initialized
|
||||
|
||||
proc threadGlobalBrokerContext*(): BrokerContext =
|
||||
## Returns the currently active broker context for this thread.
|
||||
##
|
||||
## Defaults to `DefaultBrokerContext` until explicitly set via
|
||||
## `setThreadBrokerContext` or `initThreadBrokerContext`.
|
||||
## Lock-free threadvar read — safe to call from anywhere.
|
||||
if not globalBrokerContextInitialized:
|
||||
globalBrokerContextValue = DefaultBrokerContext
|
||||
globalBrokerContextInitialized = true
|
||||
globalBrokerContextValue
|
||||
|
||||
# Backward-compatible alias
|
||||
template globalBrokerContext*(): BrokerContext =
|
||||
threadGlobalBrokerContext()
|
||||
|
||||
var gClassCtxCounter: Atomic[uint32]
|
||||
|
||||
proc newClassCtx*(): uint16 =
|
||||
## Allocate a fresh, process-unique classCtx (the low-16 "global" scope id).
|
||||
## Shared by flat `NewBrokerContext` and the OOP interface-class registration
|
||||
## so every classCtx is unique. Starts at 2 (0 = nil, 1 = default scope).
|
||||
let id = gClassCtxCounter.fetchAdd(1, moRelaxed) + 2'u32
|
||||
doAssert id < 0xFFFF'u32, "BrokerContext classCtx space exhausted (max 65534)"
|
||||
uint16(id)
|
||||
|
||||
proc NewBrokerContext*(): BrokerContext =
|
||||
## A flat "global" context: a fresh classCtx with instanceCtx 0.
|
||||
makeBrokerContext(newClassCtx(), 0'u16)
|
||||
|
||||
var gInstanceCtxCounter: Atomic[uint32]
|
||||
|
||||
proc newInstanceCtx*(parentCtx: BrokerContext): BrokerContext =
|
||||
## Allocate a sub-instance context that SHARES `parentCtx`'s classCtx (so it
|
||||
## routes to the same library context — same processing/delivery thread and
|
||||
## courier) but carries a fresh, process-unique instanceCtx (high16).
|
||||
##
|
||||
## Used by create-instance FFI requests (reduced-A): a sub-interface instance
|
||||
## lives on the main library's processing thread, so it must share the library
|
||||
## classCtx. `<lib>_call` masks the instanceCtx off to find the courier, then
|
||||
## dispatches against the full sub ctx so the provider keyed by it is hit.
|
||||
## The counter is process-monotonic, so two sub-instances under the same
|
||||
## library never collide on instanceCtx.
|
||||
let id = gInstanceCtxCounter.fetchAdd(1, moRelaxed) + 1'u32
|
||||
doAssert id < 0x1_0000'u32, "BrokerContext instanceCtx space exhausted (max 65535)"
|
||||
makeBrokerContext(classCtx(parentCtx), uint16(id))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sync thread-context binding (usable from {.thread.} init, before event loop)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc setThreadBrokerContext*(ctx: BrokerContext) =
|
||||
## Installs an existing BrokerContext as this thread's global broker context.
|
||||
##
|
||||
## Use when the context was created elsewhere (e.g. on the main thread)
|
||||
## and this thread should adopt it. Readable via `threadGlobalBrokerContext()`.
|
||||
##
|
||||
## This is sync and thread-safe (writes only to this thread's threadvar).
|
||||
globalBrokerContextValue = ctx
|
||||
globalBrokerContextInitialized = true
|
||||
|
||||
proc initThreadBrokerContext*(): BrokerContext =
|
||||
## Generates a new BrokerContext and installs it as this thread's
|
||||
## global broker context. Returns the new context so it can be
|
||||
## propagated to other threads for cross-thread broker access.
|
||||
##
|
||||
## Convenience for: `let ctx = NewBrokerContext(); setThreadBrokerContext(ctx)`
|
||||
let ctx = NewBrokerContext()
|
||||
setThreadBrokerContext(ctx)
|
||||
return ctx
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Async scoped context (backward compat)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
template lockGlobalBrokerContext*(brokerCtx: BrokerContext, body: untyped): untyped =
|
||||
## Runs `body` while holding the global broker context lock with the provided
|
||||
## `brokerCtx` installed as the globally accessible context.
|
||||
##
|
||||
## This template is intended for use from within `chronos` async procs.
|
||||
block:
|
||||
# Lazy init: threadvar is nil on secondary threads (module-level init
|
||||
# only runs on the main thread).
|
||||
if globalBrokerContextLock.isNil():
|
||||
globalBrokerContextLock = newAsyncLock()
|
||||
await noCancel(globalBrokerContextLock.acquire())
|
||||
let previousBrokerCtx = globalBrokerContextValue
|
||||
globalBrokerContextValue = brokerCtx
|
||||
globalBrokerContextInitialized = true
|
||||
try:
|
||||
body
|
||||
finally:
|
||||
globalBrokerContextValue = previousBrokerCtx
|
||||
try:
|
||||
globalBrokerContextLock.release()
|
||||
except AsyncLockError:
|
||||
doAssert false, "globalBrokerContextLock.release(): lock not held"
|
||||
|
||||
template lockNewGlobalBrokerContext*(body: untyped): untyped =
|
||||
## Runs `body` while holding the global broker context lock with a freshly
|
||||
## generated broker context installed as the global accessor.
|
||||
##
|
||||
## The previous global broker context (if any) is restored on exit.
|
||||
lockGlobalBrokerContext(NewBrokerContext()):
|
||||
body
|
||||
|
||||
{.pop.}
|
||||
@@ -0,0 +1,268 @@
|
||||
## BrokerImplement — derived implementation of a BrokerInterface
|
||||
## (doc/HIERARCHICAL_BROKERS_PLAN.md, phase P4).
|
||||
##
|
||||
## type MyServiceImpl = ref object of IMyService
|
||||
## db: Database
|
||||
##
|
||||
## BrokerImplement MyServiceImpl of IMyService:
|
||||
## proc init(db: Database) = ## optional; `self` is the new instance
|
||||
## self.db = db
|
||||
## method getHealth(self: MyServiceImpl): Future[Result[GetHealth, string]] =
|
||||
## ok(GetHealth(...)) ## raw method overrides of the abstract base
|
||||
##
|
||||
## Generates: `MyServiceImpl.new(db = ...)` (allocates an instance brokerCtx and
|
||||
## runs `init`), per-instance provider closures that dispatch each request to
|
||||
## the overriding method (capturing `self`), and `close(self)` which clears
|
||||
## those providers — breaking the instance<->closure cycle (mandatory under
|
||||
## --mm:refc) and freeing the instance ctx for reuse.
|
||||
|
||||
import std/[macros, strutils, atomics]
|
||||
import chronos, results
|
||||
import ./broker_context
|
||||
import ./request_broker, ./event_broker
|
||||
import ./internal/helper/broker_utils
|
||||
|
||||
export chronos, results, broker_context, request_broker, event_broker
|
||||
|
||||
proc canonPragma(async: bool): NimNode {.compileTime.} =
|
||||
## Canonical override pragma matching the BrokerInterface abstract base
|
||||
## (byte-identical async/raises/gcsafe is required for method dispatch).
|
||||
let src =
|
||||
if async:
|
||||
"proc d() {.async: (raises: []), gcsafe.} = discard"
|
||||
else:
|
||||
"proc d() {.gcsafe, raises: [].} = discard"
|
||||
parseStmt(src)[0][4]
|
||||
|
||||
proc isAsyncRet(ret: NimNode): bool {.compileTime.} =
|
||||
ret.kind == nnkBracketExpr and ret.len >= 1 and ret[0].kind == nnkIdent and
|
||||
ret[0].eqIdent("Future")
|
||||
|
||||
proc baseName(n: NimNode): NimNode {.compileTime.} =
|
||||
if n.kind == nnkPostfix:
|
||||
n[1]
|
||||
else:
|
||||
n
|
||||
|
||||
macro BrokerImplement*(args: varargs[untyped]): untyped =
|
||||
## See module docs. Invoked as `BrokerImplement Impl of IFace: <body>`.
|
||||
if args.len < 2:
|
||||
macros.error("BrokerImplement requires `Impl of IFace:` and a body")
|
||||
let body = args[^1]
|
||||
if body.kind != nnkStmtList:
|
||||
macros.error("BrokerImplement body must be a `:` block")
|
||||
let infix = args[0]
|
||||
if infix.kind != nnkInfix or not infix[0].eqIdent("of"):
|
||||
macros.error(
|
||||
"BrokerImplement must be written `BrokerImplement Impl of IFace:`", infix
|
||||
)
|
||||
let implName = infix[1]
|
||||
let implStr = $implName
|
||||
let ifaceStr = $infix[2]
|
||||
|
||||
result = newStmtList()
|
||||
|
||||
var initParams: seq[NimNode] = @[] # extra new() params (after the typedesc)
|
||||
var initBody = newStmtList()
|
||||
# (verb, brokerName, argParams, payloadRepr, async)
|
||||
var methods: seq[(string, string, seq[NimNode], string, bool)] = @[]
|
||||
|
||||
for stmt in body:
|
||||
case stmt.kind
|
||||
of nnkProcDef:
|
||||
if not baseName(stmt[0]).eqIdent("init"):
|
||||
macros.error(
|
||||
"BrokerImplement only allows an `init` proc and `method` overrides", stmt
|
||||
)
|
||||
let p = stmt.params
|
||||
for i in 1 ..< p.len: # skip return type
|
||||
initParams.add(copyNimTree(p[i]))
|
||||
initBody = copyNimTree(stmt.body)
|
||||
of nnkMethodDef:
|
||||
let verb = $baseName(stmt[0])
|
||||
let p = stmt.params
|
||||
let ret = p[0]
|
||||
let async = isAsyncRet(ret)
|
||||
let payload = extractResultOk(ret, async)
|
||||
if payload.isNil:
|
||||
macros.error(
|
||||
"method `" & verb & "` must return " &
|
||||
(if async: "Future[Result[T, string]]" else: "Result[T, string]"),
|
||||
stmt,
|
||||
)
|
||||
# Stamp the canonical override pragma and emit the method verbatim.
|
||||
var m = copyNimTree(stmt)
|
||||
m[4] = canonPragma(async)
|
||||
result.add(m)
|
||||
var margs: seq[NimNode] = @[]
|
||||
for i in 2 ..< p.len: # skip return (0) and self (1)
|
||||
margs.add(copyNimTree(p[i]))
|
||||
methods.add((verb, capitalizeAscii(verb), margs, payload.repr.strip(), async))
|
||||
of nnkEmpty, nnkCommentStmt:
|
||||
discard
|
||||
else:
|
||||
macros.error(
|
||||
"BrokerImplement only allows an `init` proc and `method` overrides", stmt
|
||||
)
|
||||
|
||||
# Compile-time fulfillment check: every request verb declared in the
|
||||
# interface must have a corresponding method override in the implementation.
|
||||
let ifaceVerbs = interfaceRequestVerbs(ifaceStr)
|
||||
for (verb, typeName) in ifaceVerbs:
|
||||
var found = false
|
||||
for m in methods:
|
||||
if m[0] == verb:
|
||||
found = true
|
||||
break
|
||||
if not found:
|
||||
macros.error(
|
||||
"BrokerImplement " & implStr & ": missing method override for '" & verb &
|
||||
"' (request type " & typeName & ") declared in " & ifaceStr
|
||||
)
|
||||
|
||||
# Per-class context allocation state.
|
||||
let classCtxVar = ident(implStr & "BrokerClassCtx")
|
||||
let instCounter = ident(implStr & "BrokerInstCounter")
|
||||
let setupName = ident(implStr & "SetupProviders")
|
||||
result.add(
|
||||
quote do:
|
||||
# classCtx allocated once at module init (immutable -> race-free and
|
||||
# gcsafe to read); per-instance instanceCtx from an atomic counter.
|
||||
let `classCtxVar` = newClassCtx()
|
||||
var `instCounter` {.global.}: Atomic[uint16]
|
||||
)
|
||||
|
||||
# setupProviders — register a per-instance provider closure per request that
|
||||
# dispatches to the overriding method (capturing `self`).
|
||||
var setupSrc = "proc " & $setupName & "(self: " & implStr & ") {.gcsafe.} =\n"
|
||||
if methods.len == 0:
|
||||
setupSrc.add(" discard\n")
|
||||
for (verb, brokerName, margs, payload, async) in methods:
|
||||
var paramDecls = ""
|
||||
var argNames = ""
|
||||
for a in margs:
|
||||
paramDecls.add((if paramDecls.len > 0: ", " else: "") & a.repr.strip())
|
||||
for j in 0 ..< a.len - 2:
|
||||
argNames.add((if argNames.len > 0: ", " else: "") & $baseName(a[j]))
|
||||
let ret =
|
||||
if async:
|
||||
"Future[Result[" & payload & ", string]]"
|
||||
else:
|
||||
"Result[" & payload & ", string]"
|
||||
# Pragma must match the broker's generated provider proc type
|
||||
# (request_broker `makeProcType`): plain `{.async.}` for async,
|
||||
# `{.gcsafe, raises: [CatchableError].}` for sync.
|
||||
let prag = if async: "{.async.}" else: "{.gcsafe, raises: [CatchableError].}"
|
||||
let call = (if async: "await " else: "") & "self." & verb & "(" & argNames & ")"
|
||||
setupSrc.add(
|
||||
" discard " & brokerName & ".setProvider(self.brokerCtx, proc(" & paramDecls &
|
||||
"): " & ret & " " & prag & " =\n " & call & ")\n"
|
||||
)
|
||||
result.add(parseStmt(setupSrc))
|
||||
|
||||
# new() — allocate the instance, its brokerCtx, run init, wire providers.
|
||||
var newFormal = nnkFormalParams.newTree(copyNimTree(implName))
|
||||
newFormal.add(
|
||||
newIdentDefs(
|
||||
ident("T"), nnkBracketExpr.newTree(ident("typedesc"), copyNimTree(implName))
|
||||
)
|
||||
)
|
||||
for p in initParams:
|
||||
newFormal.add(copyNimTree(p))
|
||||
# Build new()'s body as ONE flat scope so `self` is visible to the spliced
|
||||
# init body. `self` is interpolated as an explicit ident (quote would gensym
|
||||
# a literal `let self`, breaking the user's `self.field` references).
|
||||
let selfId = ident("self")
|
||||
var newBody = newStmtList()
|
||||
let pre = quote:
|
||||
let `selfId` = `implName`()
|
||||
`selfId`.brokerCtx =
|
||||
makeBrokerContext(`classCtxVar`, `instCounter`.fetchAdd(1'u16, moRelaxed) + 1'u16)
|
||||
for s in pre:
|
||||
newBody.add(s)
|
||||
for s in initBody:
|
||||
newBody.add(copyNimTree(s))
|
||||
let post = quote:
|
||||
`setupName`(`selfId`)
|
||||
`selfId`
|
||||
for s in post:
|
||||
newBody.add(copyNimTree(s))
|
||||
# A0: new() is gcsafe — the create-instance FFI path constructs sub-instances
|
||||
# in a gcsafe request method body (classCtx is an immutable `let`, instanceCtx
|
||||
# an atomic, setupProviders is gcsafe). Requires the impl's `init` body to be
|
||||
# gcsafe (trivial field writes always are). If a real in-process user needs a
|
||||
# non-gcsafe init, add a separate non-gcsafe constructor rather than relaxing
|
||||
# this.
|
||||
result.add(
|
||||
nnkProcDef.newTree(
|
||||
postfix(ident("new"), "*"),
|
||||
newEmptyNode(),
|
||||
newEmptyNode(),
|
||||
newFormal,
|
||||
nnkPragma.newTree(ident("gcsafe")),
|
||||
newEmptyNode(),
|
||||
newBody,
|
||||
)
|
||||
)
|
||||
|
||||
# bindToContext() — construct an instance that ADOPTS an externally-supplied
|
||||
# brokerCtx (the FFI library context allocated by `<lib>_createContext`)
|
||||
# instead of allocating its own. Lets a BrokerInterface(API) impl serve as the
|
||||
# provider set for registerBrokerLibrary's `setupProviders(ctx)` (runs on the
|
||||
# processing thread → gcsafe). Wires providers keyed by `ctx`.
|
||||
var bindFormal = nnkFormalParams.newTree(copyNimTree(implName))
|
||||
bindFormal.add(
|
||||
newIdentDefs(
|
||||
ident("T"), nnkBracketExpr.newTree(ident("typedesc"), copyNimTree(implName))
|
||||
)
|
||||
)
|
||||
bindFormal.add(newIdentDefs(ident("ctx"), ident("BrokerContext")))
|
||||
for p in initParams:
|
||||
bindFormal.add(copyNimTree(p))
|
||||
var bindBody = newStmtList()
|
||||
let bindPre = quote:
|
||||
let `selfId` = `implName`()
|
||||
`selfId`.brokerCtx = ctx
|
||||
for s in bindPre:
|
||||
bindBody.add(s)
|
||||
for s in initBody:
|
||||
bindBody.add(copyNimTree(s))
|
||||
for s in post:
|
||||
bindBody.add(copyNimTree(s))
|
||||
result.add(
|
||||
nnkProcDef.newTree(
|
||||
postfix(ident("bindToContext"), "*"),
|
||||
newEmptyNode(),
|
||||
newEmptyNode(),
|
||||
bindFormal,
|
||||
nnkPragma.newTree(ident("gcsafe")),
|
||||
newEmptyNode(),
|
||||
bindBody,
|
||||
)
|
||||
)
|
||||
|
||||
# close() — clear this instance's providers (breaks the refc cycle) and free
|
||||
# its ctx. Idempotent.
|
||||
var closeSrc = "proc close*(self: " & implStr & ") =\n"
|
||||
closeSrc.add(" if self.brokerCtx == DefaultBrokerContext: return\n")
|
||||
for (verb, brokerName, margs, payload, async) in methods:
|
||||
closeSrc.add(" " & brokerName & ".clearProvider(self.brokerCtx)\n")
|
||||
# B2: also drop this instance's event listeners. The interface published its
|
||||
# event types via the compile-time registry; guard with `when compiles` so it
|
||||
# works whether the event broker is single-thread / mt / API.
|
||||
for ev in interfaceEvents(ifaceStr):
|
||||
# dropAllListeners clears the listener table synchronously (before its first
|
||||
# await), so discarding the Future from sync close() still removes listeners;
|
||||
# only the in-flight-cancel await is abandoned (matches teardown semantics).
|
||||
closeSrc.add(" when compiles(" & ev & ".dropAllListeners(self.brokerCtx)):\n")
|
||||
closeSrc.add(
|
||||
" when typeof(" & ev & ".dropAllListeners(self.brokerCtx)) is void:\n"
|
||||
)
|
||||
closeSrc.add(" " & ev & ".dropAllListeners(self.brokerCtx)\n")
|
||||
closeSrc.add(" else:\n")
|
||||
closeSrc.add(" discard " & ev & ".dropAllListeners(self.brokerCtx)\n")
|
||||
closeSrc.add(" self.brokerCtx = DefaultBrokerContext\n")
|
||||
result.add(parseStmt(closeSrc))
|
||||
|
||||
when defined(brokerDebug):
|
||||
echo result.repr
|
||||
@@ -0,0 +1,243 @@
|
||||
## BrokerInterface — an abstract, OOP-style facade over a group of Event /
|
||||
## Request brokers (see doc/HIERARCHICAL_BROKERS_PLAN.md, phase P3).
|
||||
##
|
||||
## A `BrokerInterface` block declares the *contract*: the events it can emit
|
||||
## and the requests it answers. It generates:
|
||||
## * a `ref object of RootObj` interface type carrying a hidden `brokerCtx`;
|
||||
## * the underlying Event/Request brokers (re-emitted verbatim, or lowered to
|
||||
## their `(API)` variants when the interface is declared `(API)`);
|
||||
## * one abstract `{.base.}` `method` per request (pure-virtual — raises
|
||||
## until a `BrokerImplement` derived type overrides it);
|
||||
## * a generic instance-scoped event facade (`self.emit` / `self.listen` /
|
||||
## `self.dropListener`) that injects `self.brokerCtx`.
|
||||
##
|
||||
## Invocation forms (note: `BrokerInterface(API) IFace:` does NOT parse in Nim —
|
||||
## the `(API)` binds as a call; use the comma form instead):
|
||||
## BrokerInterface IFace: ## or BrokerInterface(IFace):
|
||||
## EventBroker: ...
|
||||
## RequestBroker: ...
|
||||
## BrokerInterface(API, IFace): ## (API) propagates to every sub-broker
|
||||
## EventBroker: ...
|
||||
## RequestBroker: ...
|
||||
##
|
||||
## Requests inside an interface use the proc-sugar form (a lowercase verb proc);
|
||||
## the verb becomes the abstract method name a `BrokerImplement` overrides.
|
||||
|
||||
import std/[macros, strutils]
|
||||
import chronos, results
|
||||
import ./broker_context
|
||||
import ./request_broker, ./event_broker
|
||||
import ./internal/helper/broker_utils
|
||||
|
||||
export chronos, results, broker_context, request_broker, event_broker
|
||||
|
||||
proc isApiArg(n: NimNode): bool =
|
||||
n.kind == nnkIdent and n.eqIdent("API")
|
||||
|
||||
proc brokerHeadName(stmt: NimNode): string =
|
||||
## The macro name a sub-block invokes (EventBroker / RequestBroker), or "".
|
||||
if stmt.kind notin {nnkCall, nnkCommand}:
|
||||
return ""
|
||||
let head = stmt[0]
|
||||
if head.kind == nnkIdent:
|
||||
return $head
|
||||
""
|
||||
|
||||
proc renderAbstractMethod(
|
||||
ifaceName, verb, payloadRepr: string, argParams: seq[NimNode], async: bool
|
||||
): string =
|
||||
## Render an abstract base method as Nim source (parsed back via parseStmt —
|
||||
## sidesteps fiddly pragma-AST construction for `async: (raises: [])`).
|
||||
var params = "self: " & ifaceName
|
||||
for p in argParams:
|
||||
params.add(", " & p.repr.strip())
|
||||
let ret =
|
||||
if async:
|
||||
"Future[Result[" & payloadRepr & ", string]]"
|
||||
else:
|
||||
"Result[" & payloadRepr & ", string]"
|
||||
let pragma =
|
||||
if async:
|
||||
"{.base, async: (raises: []), gcsafe.}"
|
||||
else:
|
||||
"{.base, gcsafe, raises: [].}"
|
||||
result =
|
||||
"method " & verb & "*(" & params & "): " & ret & " " & pragma & " =\n" &
|
||||
" raiseAssert(\"" & ifaceName & "." & verb & " has no implementation\")\n"
|
||||
|
||||
macro BrokerInterface*(args: varargs[untyped]): untyped =
|
||||
## See module docs. `args` is `[<API>?, <IFaceName>, <body>]` in any order for
|
||||
## the leading idents, with the `:` block as the final argument.
|
||||
if args.len < 2:
|
||||
macros.error("BrokerInterface requires an interface name and a `:` body block")
|
||||
let body = args[^1]
|
||||
if body.kind != nnkStmtList:
|
||||
macros.error("BrokerInterface body must be a `:` block")
|
||||
|
||||
var ifaceName: NimNode = nil
|
||||
var isApi = false
|
||||
for i in 0 ..< args.len - 1:
|
||||
if isApiArg(args[i]):
|
||||
isApi = true
|
||||
elif args[i].kind == nnkIdent:
|
||||
if ifaceName != nil:
|
||||
macros.error(
|
||||
"BrokerInterface: unexpected extra name `" & $args[i] & "`", args[i]
|
||||
)
|
||||
ifaceName = args[i]
|
||||
else:
|
||||
macros.error("BrokerInterface: unexpected argument", args[i])
|
||||
if ifaceName.isNil:
|
||||
macros.error("BrokerInterface requires an interface name", body)
|
||||
|
||||
let ifaceNameStr = $ifaceName
|
||||
result = newStmtList()
|
||||
|
||||
# 1. Interface ref type with the hidden context.
|
||||
result.add(
|
||||
quote do:
|
||||
type `ifaceName`* = ref object of RootObj
|
||||
brokerCtx*: BrokerContext
|
||||
|
||||
)
|
||||
|
||||
# 2. Walk the sub-blocks: re-emit each broker (lowered to `(API)` when the
|
||||
# interface is `(API)`), and generate abstract methods for requests.
|
||||
var eventNames: seq[string] = @[]
|
||||
var requestTypes: seq[string] = @[] # sanitized request broker type names (A1)
|
||||
var requestVerbs: seq[(string, string)] = @[] # (verb, sanitized type name)
|
||||
for stmt in body:
|
||||
let headName = brokerHeadName(stmt)
|
||||
if headName notin ["EventBroker", "RequestBroker"]:
|
||||
macros.error(
|
||||
"BrokerInterface body may only contain `EventBroker:` / `RequestBroker:` blocks",
|
||||
stmt,
|
||||
)
|
||||
let innerBody = stmt[^1]
|
||||
if innerBody.kind != nnkStmtList:
|
||||
macros.error(
|
||||
headName & " inside BrokerInterface must have a `:` body block", stmt
|
||||
)
|
||||
let hasMode = stmt.len == 3 # nnkCall(Head, mode, body)
|
||||
|
||||
# Re-emit the underlying broker.
|
||||
if isApi:
|
||||
if hasMode:
|
||||
macros.error(
|
||||
"BrokerInterface(API): sub-brokers must be plain `" & headName &
|
||||
":` (the API mode is applied automatically)",
|
||||
stmt,
|
||||
)
|
||||
result.add(newCall(ident(headName), ident("API"), copyNimTree(innerBody)))
|
||||
else:
|
||||
result.add(copyNimTree(stmt))
|
||||
|
||||
# Requests → abstract methods.
|
||||
if headName == "RequestBroker":
|
||||
let async = isApi or not (hasMode and stmt[1].eqIdent("sync"))
|
||||
let sg = parseRequestSugar(innerBody, "BrokerInterface RequestBroker", async)
|
||||
let payloadRepr = sg.payloadType.repr.strip()
|
||||
# Record the request broker type name (matches CborRequestEntry.
|
||||
# responseTypeName) so codegen can attribute the flat entry to this iface.
|
||||
requestTypes.add(sanitizeIdentName(sg.typeIdent))
|
||||
requestVerbs.add((sg.verb, sanitizeIdentName(sg.typeIdent)))
|
||||
if not sg.zeroArgProc.isNil:
|
||||
result.add(
|
||||
parseStmt(
|
||||
renderAbstractMethod(ifaceNameStr, sg.verb, payloadRepr, @[], async)
|
||||
)
|
||||
)
|
||||
if not sg.argProc.isNil:
|
||||
result.add(
|
||||
parseStmt(
|
||||
renderAbstractMethod(
|
||||
ifaceNameStr, sg.verb, payloadRepr, sg.argParams, async
|
||||
)
|
||||
)
|
||||
)
|
||||
elif headName == "EventBroker":
|
||||
# Record the event type so BrokerImplement.close() can drop listeners.
|
||||
let evParsed = parseSingleTypeDef(innerBody, "BrokerInterface EventBroker")
|
||||
eventNames.add($evParsed.typeIdent)
|
||||
|
||||
# Publish this interface's event types for BrokerImplement teardown (B2).
|
||||
registerInterfaceEvents(ifaceNameStr, eventNames)
|
||||
|
||||
# Publish this interface's request verbs for BrokerImplement fulfillment check.
|
||||
registerInterfaceVerbs(ifaceNameStr, requestVerbs)
|
||||
|
||||
# A1: publish (API) interfaces to the compile-time registry so
|
||||
# registerBrokerLibrary can designate a main class and partition the per-
|
||||
# interface wrapper surface. Plain (non-API) interfaces are not FFI-exposed.
|
||||
if isApi:
|
||||
registerApiInterface(ifaceNameStr, requestTypes, eventNames)
|
||||
|
||||
# 3. Generic instance-scoped event facade — forwards any event typedesc to
|
||||
# the underlying ctx-based broker API using `self.brokerCtx`.
|
||||
result.add(
|
||||
quote do:
|
||||
template emit*(self: `ifaceName`, t: typedesc, args: varargs[untyped]): untyped =
|
||||
t.emit(self.brokerCtx, args)
|
||||
|
||||
template listen*(self: `ifaceName`, t: typedesc, handler: untyped): untyped =
|
||||
t.listen(self.brokerCtx, handler)
|
||||
|
||||
template dropListener*(self: `ifaceName`, t: typedesc, handle: untyped): untyped =
|
||||
t.dropListener(self.brokerCtx, handle)
|
||||
|
||||
)
|
||||
|
||||
# 4. Factory / dependency-injection. A consumer depends only on the interface
|
||||
# module; an implementer installs a constructor via `provideFactory`
|
||||
# (last wins) and the consumer obtains an instance via `create`. The
|
||||
# factory may close over outer config, or take a typed config at call
|
||||
# time. In-process the factory returns the real impl (direct virtual
|
||||
# dispatch); the cross-runtime proxy variant is wired in P6.
|
||||
# NOTE (P6): factory storage is a process-global here; cross-thread FFI use
|
||||
# will harden it (lock + shared) when registerBrokerLibrary lands.
|
||||
let ifaceNameLit = newLit(ifaceNameStr)
|
||||
let facVar = ident(ifaceNameStr & "BrokerFactory")
|
||||
let facCfgVar = ident(ifaceNameStr & "BrokerFactoryCfg")
|
||||
result.add(
|
||||
quote do:
|
||||
var `facVar` {.global.}:
|
||||
proc(cfg: pointer): Result[`ifaceName`, string] {.raises: [].}
|
||||
var `facCfgVar` {.global.}: string
|
||||
|
||||
proc provideFactory*(
|
||||
_: typedesc[`ifaceName`], f: proc(): Result[`ifaceName`, string]
|
||||
) =
|
||||
`facCfgVar` = ""
|
||||
`facVar` = proc(cfg: pointer): Result[`ifaceName`, string] {.raises: [].} =
|
||||
try:
|
||||
f()
|
||||
except Exception as e:
|
||||
err(`ifaceNameLit` & " factory raised: " & e.msg)
|
||||
|
||||
proc provideFactory*[A](
|
||||
_: typedesc[`ifaceName`], f: proc(cfg: A): Result[`ifaceName`, string]
|
||||
) =
|
||||
`facCfgVar` = $A
|
||||
`facVar` = proc(cfg: pointer): Result[`ifaceName`, string] {.raises: [].} =
|
||||
try:
|
||||
f(cast[ptr A](cfg)[])
|
||||
except Exception as e:
|
||||
err(`ifaceNameLit` & " factory raised: " & e.msg)
|
||||
|
||||
proc create*(_: typedesc[`ifaceName`]): Result[`ifaceName`, string] =
|
||||
if `facVar`.isNil:
|
||||
return err("no factory provided for " & `ifaceNameLit`)
|
||||
`facVar`(nil)
|
||||
|
||||
proc create*[A](_: typedesc[`ifaceName`], cfg: A): Result[`ifaceName`, string] =
|
||||
if `facVar`.isNil:
|
||||
return err("no factory provided for " & `ifaceNameLit`)
|
||||
if `facCfgVar` != $A:
|
||||
return err(
|
||||
`ifaceNameLit` & " factory config type mismatch (got " & $A & ", expected " &
|
||||
`facCfgVar` & ")"
|
||||
)
|
||||
var c = cfg
|
||||
`facVar`(addr c)
|
||||
|
||||
)
|
||||
@@ -0,0 +1,628 @@
|
||||
## EventBroker
|
||||
## -------------------
|
||||
## EventBroker represents a reactive decoupling pattern, that
|
||||
## allows event-driven development without
|
||||
## need for direct dependencies in between emitters and listeners.
|
||||
## Worth considering using it in a single or many emitters to many listeners scenario.
|
||||
##
|
||||
## Generates a standalone, type-safe event broker for the declared type.
|
||||
## The macro exports the value type itself plus a broker companion that manages
|
||||
## listeners via thread-local storage.
|
||||
##
|
||||
## Type definitions:
|
||||
## - Inline `object` / `ref object` definitions are supported.
|
||||
## - Native types, aliases, and externally-defined types are also supported.
|
||||
## In that case, EventBroker will automatically wrap the declared RHS type in
|
||||
## `distinct` unless you already used `distinct`.
|
||||
## This keeps event types unique even when multiple brokers share the same
|
||||
## underlying base type.
|
||||
##
|
||||
## Default vs. context aware use:
|
||||
## Every generated broker is a thread-local global instance. This means EventBroker
|
||||
## enables decoupled event exchange threadwise.
|
||||
##
|
||||
## Sometimes we use brokers inside a context (e.g. within a component that has many
|
||||
## modules or subsystems). If you instantiate multiple such components in a single
|
||||
## thread, and each component must have its own listener set for the same EventBroker
|
||||
## type, you can use context-aware EventBroker.
|
||||
##
|
||||
## Context awareness is supported through the `BrokerContext` argument for
|
||||
## `listen`, `emit`, `dropListener`, and `dropAllListeners`.
|
||||
## Listener stores are kept separate per broker context.
|
||||
##
|
||||
## Default broker context is defined as `DefaultBrokerContext`. If you don't need
|
||||
## context awareness, you can keep using the interfaces without the context
|
||||
## argument, which operate on `DefaultBrokerContext`.
|
||||
##
|
||||
## Usage:
|
||||
## Declare your desired event type inside an `EventBroker` macro, add any number of fields.:
|
||||
## ```nim
|
||||
## EventBroker:
|
||||
## type TypeName = object
|
||||
## field1*: FieldType
|
||||
## field2*: AnotherFieldType
|
||||
## ```
|
||||
##
|
||||
## After this, you can register async listeners anywhere in your code with
|
||||
## `TypeName.listen(...)`, which returns a handle to the registered listener.
|
||||
## Listeners are async procs or lambdas that take a single argument of the event type.
|
||||
## Any number of listeners can be registered in different modules.
|
||||
##
|
||||
## Events can be emitted from anywhere with no direct dependency on the listeners by
|
||||
## calling `TypeName.emit(...)` with an instance of the event type.
|
||||
## This will asynchronously notify all registered listeners with the emitted event.
|
||||
##
|
||||
## Whenever you no longer need a listener (or your object instance that listen to the event goes out of scope),
|
||||
## you can remove it from the broker with the handle returned by `listen`.
|
||||
## This is done by calling `TypeName.dropListener(handle)`.
|
||||
## Alternatively, you can remove all registered listeners through `TypeName.dropAllListeners()`.
|
||||
##
|
||||
##
|
||||
## Example:
|
||||
## ```nim
|
||||
## EventBroker:
|
||||
## type GreetingEvent = object
|
||||
## text*: string
|
||||
##
|
||||
## let handle = GreetingEvent.listen(
|
||||
## proc(evt: GreetingEvent): Future[void] {.async.} =
|
||||
## echo evt.text
|
||||
## )
|
||||
## GreetingEvent.emit(text= "hi")
|
||||
## GreetingEvent.dropListener(handle)
|
||||
## ```
|
||||
|
||||
## Example (non-object event type):
|
||||
## ```nim
|
||||
## EventBroker:
|
||||
## type CounterEvent = int # exported as: `distinct int`
|
||||
##
|
||||
## discard CounterEvent.listen(
|
||||
## proc(evt: CounterEvent): Future[void] {.async.} =
|
||||
## echo int(evt)
|
||||
## )
|
||||
## CounterEvent.emit(CounterEvent(42))
|
||||
## ```
|
||||
|
||||
import std/[macros, strutils, tables]
|
||||
import chronos, chronicles, results
|
||||
import ./internal/helper/broker_utils, ./broker_context
|
||||
import ./internal/broker_debug
|
||||
|
||||
when compileOption("threads"):
|
||||
import ./internal/mt_config, ./internal/mt_event_broker
|
||||
export mt_config, mt_event_broker
|
||||
|
||||
when compileOption("threads") and defined(BrokerFfiApi):
|
||||
# Part A — native C-ABI codegen retired. See note in request_broker.nim.
|
||||
import ./internal/api_event_broker_cbor
|
||||
export api_event_broker_cbor
|
||||
|
||||
export chronicles, results, chronos, broker_context
|
||||
|
||||
type EventBrokerMode = enum
|
||||
ebDefault
|
||||
ebMultiThread
|
||||
ebApi
|
||||
|
||||
proc parseEventBrokerMode(modeNode: NimNode): EventBrokerMode =
|
||||
let raw = ($modeNode).strip().toLowerAscii()
|
||||
case raw
|
||||
of "mt":
|
||||
ebMultiThread
|
||||
of "api":
|
||||
ebApi
|
||||
else:
|
||||
error("Unknown EventBroker mode: " & $modeNode & ". Expected: mt or API", modeNode)
|
||||
|
||||
proc generateEventBroker(body: NimNode): NimNode =
|
||||
when defined(brokerDebug):
|
||||
echo body.treeRepr
|
||||
let parsed = parseSingleTypeDef(body, "EventBroker", collectFieldInfo = true)
|
||||
let typeIdent = parsed.typeIdent
|
||||
let objectDef = parsed.objectDef
|
||||
let fieldNames = parsed.fieldNames
|
||||
let fieldTypes = parsed.fieldTypes
|
||||
let hasInlineFields = parsed.hasInlineFields
|
||||
let isVoid = parsed.isVoid
|
||||
## Payload-less event (`type X = void`): the listener proc, the dispatch
|
||||
## task and `emit` all drop the event-value parameter. The parser lowers
|
||||
## `void` to a unique empty `object` so the broker still has a distinct
|
||||
## identity to name; `isVoid` just strips the now-meaningless value arg.
|
||||
|
||||
let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*")
|
||||
let sanitized = sanitizeIdentName(typeIdent)
|
||||
let typeNameLit = newLit($typeIdent)
|
||||
let handlerProcIdent = ident(sanitized & "ListenerProc")
|
||||
let listenerHandleIdent = ident(sanitized & "Listener")
|
||||
let brokerTypeIdent = ident(sanitized & "Broker")
|
||||
let exportedHandlerProcIdent = postfix(copyNimTree(handlerProcIdent), "*")
|
||||
let exportedListenerHandleIdent = postfix(copyNimTree(listenerHandleIdent), "*")
|
||||
let exportedBrokerTypeIdent = postfix(copyNimTree(brokerTypeIdent), "*")
|
||||
let bucketTypeIdent = ident(sanitized & "CtxBucket")
|
||||
let findBucketIdxIdent = ident(sanitized & "FindBucketIdx")
|
||||
let getOrCreateBucketIdxIdent = ident(sanitized & "GetOrCreateBucketIdx")
|
||||
let accessProcIdent = ident("access" & sanitized & "Broker")
|
||||
let globalVarIdent = ident("g" & sanitized & "Broker")
|
||||
let listenImplIdent = ident("register" & sanitized & "Listener")
|
||||
let dropListenerImplIdent = ident("drop" & sanitized & "Listener")
|
||||
let dropAllListenersImplIdent = ident("dropAll" & sanitized & "Listeners")
|
||||
let emitImplIdent = ident("emit" & sanitized & "Value")
|
||||
let listenerTaskIdent = ident("notify" & sanitized & "Listener")
|
||||
let cancelInFlightIdent = ident("cancelInFlight" & sanitized)
|
||||
let pruneInFlightIdent = ident("pruneInFlight" & sanitized)
|
||||
|
||||
result = newStmtList()
|
||||
|
||||
let handlerProcTy =
|
||||
if isVoid:
|
||||
quote:
|
||||
proc(): Future[void] {.async: (raises: []), gcsafe.}
|
||||
else:
|
||||
quote:
|
||||
proc(event: `typeIdent`): Future[void] {.async: (raises: []), gcsafe.}
|
||||
|
||||
result.add(
|
||||
quote do:
|
||||
type
|
||||
`exportedTypeIdent` = `objectDef`
|
||||
`exportedListenerHandleIdent` = object
|
||||
id*: uint64
|
||||
|
||||
`exportedHandlerProcIdent` = `handlerProcTy`
|
||||
`bucketTypeIdent` = object
|
||||
brokerCtx: BrokerContext
|
||||
listeners: Table[uint64, `handlerProcIdent`]
|
||||
nextId: uint64
|
||||
inFlight: seq[Future[void]]
|
||||
|
||||
`exportedBrokerTypeIdent` = ref object
|
||||
buckets: seq[`bucketTypeIdent`]
|
||||
|
||||
)
|
||||
|
||||
result.add(
|
||||
quote do:
|
||||
var `globalVarIdent` {.threadvar.}: `brokerTypeIdent`
|
||||
)
|
||||
|
||||
result.add(
|
||||
quote do:
|
||||
proc `accessProcIdent`(): `brokerTypeIdent` =
|
||||
if `globalVarIdent`.isNil():
|
||||
new(`globalVarIdent`)
|
||||
`globalVarIdent`.buckets = @[
|
||||
`bucketTypeIdent`(
|
||||
brokerCtx: DefaultBrokerContext,
|
||||
listeners: initTable[uint64, `handlerProcIdent`](),
|
||||
nextId: 1'u64,
|
||||
inFlight: @[],
|
||||
)
|
||||
]
|
||||
`globalVarIdent`
|
||||
|
||||
)
|
||||
|
||||
result.add(
|
||||
quote do:
|
||||
proc `findBucketIdxIdent`(
|
||||
broker: `brokerTypeIdent`, brokerCtx: BrokerContext
|
||||
): int =
|
||||
if brokerCtx == DefaultBrokerContext:
|
||||
return 0
|
||||
for i in 1 ..< broker.buckets.len:
|
||||
if broker.buckets[i].brokerCtx == brokerCtx:
|
||||
return i
|
||||
return -1
|
||||
|
||||
proc `getOrCreateBucketIdxIdent`(
|
||||
broker: `brokerTypeIdent`, brokerCtx: BrokerContext
|
||||
): int =
|
||||
let idx = `findBucketIdxIdent`(broker, brokerCtx)
|
||||
if idx >= 0:
|
||||
return idx
|
||||
broker.buckets.add(
|
||||
`bucketTypeIdent`(
|
||||
brokerCtx: brokerCtx,
|
||||
listeners: initTable[uint64, `handlerProcIdent`](),
|
||||
nextId: 1'u64,
|
||||
inFlight: @[],
|
||||
)
|
||||
)
|
||||
return broker.buckets.high
|
||||
|
||||
proc `listenImplIdent`(
|
||||
brokerCtx: BrokerContext, handler: `handlerProcIdent`
|
||||
): Result[`listenerHandleIdent`, string] =
|
||||
if handler.isNil():
|
||||
return err("Must provide a non-nil event handler")
|
||||
var broker = `accessProcIdent`()
|
||||
|
||||
let bucketIdx = `getOrCreateBucketIdxIdent`(broker, brokerCtx)
|
||||
if broker.buckets[bucketIdx].nextId == 0'u64:
|
||||
broker.buckets[bucketIdx].nextId = 1'u64
|
||||
|
||||
if broker.buckets[bucketIdx].nextId == high(uint64):
|
||||
error "Cannot add more listeners: ID space exhausted",
|
||||
nextId = $broker.buckets[bucketIdx].nextId
|
||||
return err("Cannot add more listeners, listener ID space exhausted")
|
||||
|
||||
let newId = broker.buckets[bucketIdx].nextId
|
||||
inc broker.buckets[bucketIdx].nextId
|
||||
broker.buckets[bucketIdx].listeners[newId] = handler
|
||||
return ok(`listenerHandleIdent`(id: newId))
|
||||
|
||||
)
|
||||
|
||||
result.add(
|
||||
quote do:
|
||||
proc `cancelInFlightIdent`(
|
||||
broker: `brokerTypeIdent`, bucketIdx: int
|
||||
) {.async: (raises: []).} =
|
||||
## Cancel all in-flight listener futures for the given bucket,
|
||||
## then clear the in-flight seq. Uses timeout to handle the
|
||||
## self-removal edge case (listener dropping itself inside its handler).
|
||||
var pending: seq[Future[void]] = @[]
|
||||
for fut in broker.buckets[bucketIdx].inFlight:
|
||||
if not fut.finished():
|
||||
pending.add(fut.cancelAndWait())
|
||||
for fut in pending:
|
||||
try:
|
||||
discard await withTimeout(fut, chronos.seconds(5))
|
||||
except CancelledError:
|
||||
# Expected when actively cancelling in-flight listener futures.
|
||||
discard
|
||||
except CatchableError as exc:
|
||||
# Log unexpected errors during cancellation while still completing teardown.
|
||||
error "Failed to cancel in-flight listener future",
|
||||
bucketIdx = bucketIdx, errorMsg = exc.msg
|
||||
broker.buckets[bucketIdx].inFlight.setLen(0)
|
||||
|
||||
proc `pruneInFlightIdent`(broker: `brokerTypeIdent`, bucketIdx: int) =
|
||||
## Sync opportunistic cleanup of completed futures.
|
||||
## Called on each emit to prevent unbounded seq growth.
|
||||
var j = 0
|
||||
while j < broker.buckets[bucketIdx].inFlight.len:
|
||||
if broker.buckets[bucketIdx].inFlight[j].finished():
|
||||
let last = broker.buckets[bucketIdx].inFlight.len - 1
|
||||
broker.buckets[bucketIdx].inFlight[j] =
|
||||
broker.buckets[bucketIdx].inFlight[last]
|
||||
broker.buckets[bucketIdx].inFlight.setLen(last) # swap-delete, O(1)
|
||||
else:
|
||||
inc j
|
||||
|
||||
)
|
||||
|
||||
result.add(
|
||||
quote do:
|
||||
proc `dropListenerImplIdent`(
|
||||
brokerCtx: BrokerContext, handle: `listenerHandleIdent`
|
||||
) {.async: (raises: []).} =
|
||||
if handle.id == 0'u64:
|
||||
return
|
||||
var broker = `accessProcIdent`()
|
||||
|
||||
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
|
||||
if bucketIdx < 0:
|
||||
return
|
||||
|
||||
if broker.buckets[bucketIdx].listeners.len == 0:
|
||||
return
|
||||
|
||||
# Remove from table — prevents future dispatches
|
||||
broker.buckets[bucketIdx].listeners.del(handle.id)
|
||||
|
||||
# Cancel and wait for all in-flight futures (timeout-guarded)
|
||||
await `cancelInFlightIdent`(broker, bucketIdx)
|
||||
|
||||
if brokerCtx != DefaultBrokerContext and
|
||||
broker.buckets[bucketIdx].listeners.len == 0:
|
||||
broker.buckets.delete(bucketIdx)
|
||||
|
||||
)
|
||||
|
||||
result.add(
|
||||
quote do:
|
||||
proc `dropAllListenersImplIdent`(
|
||||
brokerCtx: BrokerContext
|
||||
) {.async: (raises: []).} =
|
||||
var broker = `accessProcIdent`()
|
||||
|
||||
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
|
||||
if bucketIdx < 0:
|
||||
return
|
||||
|
||||
# Clear listeners — prevents new dispatches
|
||||
if broker.buckets[bucketIdx].listeners.len > 0:
|
||||
broker.buckets[bucketIdx].listeners.clear()
|
||||
|
||||
# Cancel and wait for all in-flight futures
|
||||
await `cancelInFlightIdent`(broker, bucketIdx)
|
||||
|
||||
if brokerCtx != DefaultBrokerContext:
|
||||
broker.buckets.delete(bucketIdx)
|
||||
|
||||
)
|
||||
|
||||
result.add(
|
||||
quote do:
|
||||
proc listen*(
|
||||
_: typedesc[`typeIdent`], handler: `handlerProcIdent`
|
||||
): Result[`listenerHandleIdent`, string] =
|
||||
return `listenImplIdent`(DefaultBrokerContext, handler)
|
||||
|
||||
proc listen*(
|
||||
_: typedesc[`typeIdent`],
|
||||
brokerCtx: BrokerContext,
|
||||
handler: `handlerProcIdent`,
|
||||
): Result[`listenerHandleIdent`, string] =
|
||||
return `listenImplIdent`(brokerCtx, handler)
|
||||
|
||||
)
|
||||
|
||||
result.add(
|
||||
quote do:
|
||||
proc dropListener*(
|
||||
_: typedesc[`typeIdent`], handle: `listenerHandleIdent`
|
||||
): Future[void] {.async: (raises: []).} =
|
||||
await `dropListenerImplIdent`(DefaultBrokerContext, handle)
|
||||
|
||||
proc dropListener*(
|
||||
_: typedesc[`typeIdent`],
|
||||
brokerCtx: BrokerContext,
|
||||
handle: `listenerHandleIdent`,
|
||||
): Future[void] {.async: (raises: []).} =
|
||||
await `dropListenerImplIdent`(brokerCtx, handle)
|
||||
|
||||
proc dropAllListeners*(
|
||||
_: typedesc[`typeIdent`]
|
||||
): Future[void] {.async: (raises: []).} =
|
||||
await `dropAllListenersImplIdent`(DefaultBrokerContext)
|
||||
|
||||
proc dropAllListeners*(
|
||||
_: typedesc[`typeIdent`], brokerCtx: BrokerContext
|
||||
): Future[void] {.async: (raises: []).} =
|
||||
await `dropAllListenersImplIdent`(brokerCtx)
|
||||
|
||||
)
|
||||
|
||||
if isVoid:
|
||||
# Payload-less event: listener task, emitImpl and `emit` carry no
|
||||
# event value. `emit` is only the typedesc form (`TypeName.emit()`),
|
||||
# since a bare value-less `emit()` would be hopelessly ambiguous.
|
||||
result.add(
|
||||
quote do:
|
||||
proc `listenerTaskIdent`(
|
||||
callback: `handlerProcIdent`
|
||||
) {.async: (raises: []), gcsafe.} =
|
||||
if callback.isNil():
|
||||
return
|
||||
try:
|
||||
await callback()
|
||||
except Exception:
|
||||
error "Failed to execute event listener", error = getCurrentExceptionMsg()
|
||||
|
||||
proc `emitImplIdent`(
|
||||
brokerCtx: BrokerContext
|
||||
): Future[void] {.async: (raises: []), gcsafe.} =
|
||||
let broker = `accessProcIdent`()
|
||||
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
|
||||
if bucketIdx < 0:
|
||||
# nothing to do as nobody is listening
|
||||
return
|
||||
if broker.buckets[bucketIdx].listeners.len == 0:
|
||||
return
|
||||
|
||||
# Prune completed futures (sync — no yield point)
|
||||
`pruneInFlightIdent`(broker, bucketIdx)
|
||||
|
||||
var callbacks: seq[`handlerProcIdent`] = @[]
|
||||
for cb in broker.buckets[bucketIdx].listeners.values:
|
||||
callbacks.add(cb)
|
||||
for cb in callbacks:
|
||||
let fut = `listenerTaskIdent`(cb)
|
||||
broker.buckets[bucketIdx].inFlight.add(fut)
|
||||
|
||||
proc emit*(_: typedesc[`typeIdent`]) =
|
||||
asyncSpawn `emitImplIdent`(DefaultBrokerContext)
|
||||
|
||||
proc emit*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) =
|
||||
asyncSpawn `emitImplIdent`(brokerCtx)
|
||||
|
||||
)
|
||||
else:
|
||||
result.add(
|
||||
quote do:
|
||||
proc `listenerTaskIdent`(
|
||||
callback: `handlerProcIdent`, event: `typeIdent`
|
||||
) {.async: (raises: []), gcsafe.} =
|
||||
if callback.isNil():
|
||||
return
|
||||
try:
|
||||
await callback(event)
|
||||
except Exception:
|
||||
error "Failed to execute event listener", error = getCurrentExceptionMsg()
|
||||
|
||||
proc `emitImplIdent`(
|
||||
brokerCtx: BrokerContext, event: `typeIdent`
|
||||
): Future[void] {.async: (raises: []), gcsafe.} =
|
||||
when compiles(event.isNil()):
|
||||
if event.isNil():
|
||||
error "Cannot emit uninitialized event object", eventType = `typeNameLit`
|
||||
return
|
||||
let broker = `accessProcIdent`()
|
||||
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
|
||||
if bucketIdx < 0:
|
||||
# nothing to do as nobody is listening
|
||||
return
|
||||
if broker.buckets[bucketIdx].listeners.len == 0:
|
||||
return
|
||||
|
||||
# Prune completed futures (sync — no yield point)
|
||||
`pruneInFlightIdent`(broker, bucketIdx)
|
||||
|
||||
var callbacks: seq[`handlerProcIdent`] = @[]
|
||||
for cb in broker.buckets[bucketIdx].listeners.values:
|
||||
callbacks.add(cb)
|
||||
for cb in callbacks:
|
||||
let fut = `listenerTaskIdent`(cb, event)
|
||||
broker.buckets[bucketIdx].inFlight.add(fut)
|
||||
|
||||
proc emit*(event: `typeIdent`) =
|
||||
asyncSpawn `emitImplIdent`(DefaultBrokerContext, event)
|
||||
|
||||
proc emit*(_: typedesc[`typeIdent`], event: `typeIdent`) =
|
||||
asyncSpawn `emitImplIdent`(DefaultBrokerContext, event)
|
||||
|
||||
proc emit*(
|
||||
_: typedesc[`typeIdent`], brokerCtx: BrokerContext, event: `typeIdent`
|
||||
) =
|
||||
asyncSpawn `emitImplIdent`(brokerCtx, event)
|
||||
|
||||
)
|
||||
|
||||
if hasInlineFields:
|
||||
# Typedesc emit constructor overloads for inline object/ref object types.
|
||||
var emitCtorParams = newTree(nnkFormalParams, newEmptyNode())
|
||||
let typedescParamType =
|
||||
newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent))
|
||||
emitCtorParams.add(
|
||||
newTree(nnkIdentDefs, ident("_"), typedescParamType, newEmptyNode())
|
||||
)
|
||||
for i in 0 ..< fieldNames.len:
|
||||
emitCtorParams.add(
|
||||
newTree(
|
||||
nnkIdentDefs,
|
||||
copyNimTree(fieldNames[i]),
|
||||
copyNimTree(fieldTypes[i]),
|
||||
newEmptyNode(),
|
||||
)
|
||||
)
|
||||
|
||||
var emitCtorExpr = newTree(nnkObjConstr, copyNimTree(typeIdent))
|
||||
for i in 0 ..< fieldNames.len:
|
||||
emitCtorExpr.add(
|
||||
newTree(
|
||||
nnkExprColonExpr, copyNimTree(fieldNames[i]), copyNimTree(fieldNames[i])
|
||||
)
|
||||
)
|
||||
|
||||
let emitCtorCallDefault =
|
||||
newCall(copyNimTree(emitImplIdent), ident("DefaultBrokerContext"), emitCtorExpr)
|
||||
let emitCtorBodyDefault = quote:
|
||||
asyncSpawn `emitCtorCallDefault`
|
||||
|
||||
let typedescEmitProcDefault = newTree(
|
||||
nnkProcDef,
|
||||
postfix(ident("emit"), "*"),
|
||||
newEmptyNode(),
|
||||
newEmptyNode(),
|
||||
emitCtorParams,
|
||||
newEmptyNode(),
|
||||
newEmptyNode(),
|
||||
emitCtorBodyDefault,
|
||||
)
|
||||
result.add(typedescEmitProcDefault)
|
||||
|
||||
var emitCtorParamsCtx = newTree(nnkFormalParams, newEmptyNode())
|
||||
emitCtorParamsCtx.add(
|
||||
newTree(nnkIdentDefs, ident("_"), typedescParamType, newEmptyNode())
|
||||
)
|
||||
emitCtorParamsCtx.add(
|
||||
newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode())
|
||||
)
|
||||
for i in 0 ..< fieldNames.len:
|
||||
emitCtorParamsCtx.add(
|
||||
newTree(
|
||||
nnkIdentDefs,
|
||||
copyNimTree(fieldNames[i]),
|
||||
copyNimTree(fieldTypes[i]),
|
||||
newEmptyNode(),
|
||||
)
|
||||
)
|
||||
|
||||
let emitCtorCallCtx =
|
||||
newCall(copyNimTree(emitImplIdent), ident("brokerCtx"), copyNimTree(emitCtorExpr))
|
||||
let emitCtorBodyCtx = quote:
|
||||
asyncSpawn `emitCtorCallCtx`
|
||||
|
||||
let typedescEmitProcCtx = newTree(
|
||||
nnkProcDef,
|
||||
postfix(ident("emit"), "*"),
|
||||
newEmptyNode(),
|
||||
newEmptyNode(),
|
||||
emitCtorParamsCtx,
|
||||
newEmptyNode(),
|
||||
newEmptyNode(),
|
||||
emitCtorBodyCtx,
|
||||
)
|
||||
result.add(typedescEmitProcCtx)
|
||||
|
||||
when defined(brokerDebug):
|
||||
writeBrokerDebug("EventBroker", sanitized, result)
|
||||
when defined(brokerDebugStdout):
|
||||
echo result.repr
|
||||
|
||||
macro EventBroker*(args: varargs[untyped]): untyped =
|
||||
## Single-thread default mode, or explicit mode selector with optional kwargs.
|
||||
##
|
||||
## Examples:
|
||||
## EventBroker:
|
||||
## type MyEvent = object
|
||||
## value*: int
|
||||
##
|
||||
## EventBroker(mt):
|
||||
## type MyEvent = object
|
||||
## value*: int
|
||||
##
|
||||
## EventBroker(mt, queueDepth = 1024, slabCapacity = 4096):
|
||||
## type MyEvent = object
|
||||
## value*: int
|
||||
if args.len == 0:
|
||||
macros.error("EventBroker requires a body block")
|
||||
if args.len == 1:
|
||||
return generateEventBroker(args[0])
|
||||
let mode = args[0]
|
||||
let body = args[^1]
|
||||
if body.kind notin {nnkStmtList, nnkTypeDef, nnkTypeSection}:
|
||||
error(
|
||||
"EventBroker(" & mode.repr & ") body must be a `:` block of type definitions (got " &
|
||||
$body.kind & ")",
|
||||
body,
|
||||
)
|
||||
var kwargs: seq[NimNode]
|
||||
for i in 1 ..< args.len - 1:
|
||||
kwargs.add(args[i])
|
||||
let m = parseEventBrokerMode(mode)
|
||||
let split = (kwargs: kwargs, body: body)
|
||||
case m
|
||||
of ebMultiThread:
|
||||
when not compileOption("threads"):
|
||||
macros.error("EventBroker(mt) requires --threads:on. " &
|
||||
"Compile with `--threads:on` to use multi-thread EventBroker.")
|
||||
else:
|
||||
let cfg = parseMtEvtKwargs(split.kwargs)
|
||||
generateMtEventBroker(body, cfg)
|
||||
of ebApi:
|
||||
when not compileOption("threads"):
|
||||
macros.error("EventBroker(API) requires --threads:on. " &
|
||||
"Compile with `--threads:on` to use API EventBroker.")
|
||||
else:
|
||||
when defined(BrokerFfiApi):
|
||||
# Validate kwargs at the outer macro so errors point at the
|
||||
# user's call site, then pass them through to the deferred
|
||||
# codegen which re-parses them into an MtEvtCfg (the API
|
||||
# broker rides the same MT lane internally, so the same
|
||||
# capacity knobs apply).
|
||||
discard parseMtEvtKwargs(split.kwargs)
|
||||
generateApiCborEventBroker(body, split.kwargs)
|
||||
else:
|
||||
let cfg = parseMtEvtKwargs(split.kwargs)
|
||||
generateMtEventBroker(body, cfg)
|
||||
of ebDefault:
|
||||
if split.kwargs.len > 0:
|
||||
error(
|
||||
"EventBroker(" & mode.repr & ") does not accept kwargs (kwargs are mt-only)",
|
||||
split.kwargs[0],
|
||||
)
|
||||
generateEventBroker(body)
|
||||
@@ -0,0 +1,250 @@
|
||||
## API CBOR Codec
|
||||
## ---------------
|
||||
## CBOR encode/decode primitives for the CBOR FFI strategy.
|
||||
##
|
||||
## This module owns the `BrokerCbor` flavor (configured with strict-but-
|
||||
## forward-compat settings), the `CborResponseEnvelope[T]` wire type that
|
||||
## represents `Result[T, string]` on the wire, and the encode/decode helpers
|
||||
## that wrap `nim-cbor-serialization`'s exception-raising API as
|
||||
## `Result`-returning procs suitable for `raises: []` call sites.
|
||||
##
|
||||
## Design choices (see plan §4):
|
||||
## - Response envelope is a CBOR map with two optional fields:
|
||||
## `{ "ok": T }` for success, `{ "err": tstr }` for failure.
|
||||
## The map form lets us extend the schema without breaking older wrappers.
|
||||
## - Void responses use the `CborUnit` zero-field marker so the generic
|
||||
## `CborResponseEnvelope[T]` type also covers `Result[void, string]`.
|
||||
## - Encoding never raises — failures are surfaced as `Result.err`. Caller
|
||||
## threads (often foreign threads via the FFI gate) cannot meaningfully
|
||||
## handle a Nim `IOError` so all serialization exceptions are caught and
|
||||
## stringified at this layer.
|
||||
##
|
||||
## All buffers exchanged with the FFI boundary live elsewhere
|
||||
## (`api_common`'s shared-heap helpers); this module deals only in
|
||||
## `seq[byte]` / `openArray[byte]`.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[options, typetraits]
|
||||
import results
|
||||
import cbor_serialization
|
||||
import cbor_serialization/[reader_impl, writer]
|
||||
import cbor_serialization/std/options as cbor_options
|
||||
|
||||
export results, cbor_serialization, cbor_options
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Flavor
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
createCborFlavor(
|
||||
BrokerCbor,
|
||||
automaticObjectSerialization = true,
|
||||
automaticPrimitivesSerialization = true,
|
||||
requireAllFields = true,
|
||||
# Provider-side decode rejects malformed requests up front rather than
|
||||
# silently zero-initialising missing fields.
|
||||
omitOptionalFields = true, # Compactness: only populated Options hit the wire.
|
||||
allowUnknownFields = true,
|
||||
# Wrappers built against a newer schema can still talk to an older Nim
|
||||
# library — unknown fields are dropped on decode rather than failing.
|
||||
skipNullFields = false,
|
||||
)
|
||||
|
||||
# Encode enums as numeric ordinals so the wire format matches what
|
||||
# Python's IntEnum and C++'s underlying enum class produce naturally.
|
||||
# Without this override the upstream default is `EnumAsString`, which
|
||||
# decodes fine on the Nim side but diverges from foreign-language
|
||||
# wrappers that send enum values as ints.
|
||||
enumRep(Cbor, BrokerCbor, EnumRepresentation.EnumAsNumber)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Distinct-type bridging
|
||||
#
|
||||
# nim-cbor-serialization 0.3.0 ships a generic writer for distinct types
|
||||
# (`proc write*[T: distinct]` in writer.nim) but the matching reader is
|
||||
# commented out upstream. We provide both halves here:
|
||||
# - a generic `read[T: distinct]` that decodes into the underlying type
|
||||
# and casts back, mirroring the writer's behaviour.
|
||||
# - the flavor-level `defaultReader(distinct)` / `defaultWriter(distinct)`
|
||||
# bindings so user-defined distinct types work out of the box on the
|
||||
# `BrokerCbor` flavor without per-type registration boilerplate.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc read*[T: distinct](
|
||||
r: var CborReader, value: var T
|
||||
) {.raises: [SerializationError, IOError].} =
|
||||
mixin readValue
|
||||
var underlying: distinctBase(T, recursive = false)
|
||||
readValue(r, underlying)
|
||||
value = T(underlying)
|
||||
|
||||
BrokerCbor.defaultReader(distinct)
|
||||
# Writer side is already bound by `defaultPrimitiveWriter` (see
|
||||
# cbor_serialization/format.nim:99). Re-binding here causes
|
||||
# `ambiguous call writeValue` at user call sites.
|
||||
|
||||
# Enum reader override.
|
||||
#
|
||||
# With `enumRep = EnumAsNumber` (set above) the writer emits enum values
|
||||
# as CBOR Unsigned ints, matching what Python's `IntEnum` and C++'s
|
||||
# `enum class` underlying values produce on the wire. The upstream
|
||||
# `read[T: enum]` only accepts CBOR strings (its private `parseEnum`
|
||||
# helper hard-codes `allowNumericRepr = false`), so we provide a
|
||||
# numeric-aware override at the flavor level: read an int via the
|
||||
# already-bound `read[T: SomeInteger]`, range-check against the enum's
|
||||
# low/high ordinals, then cast.
|
||||
proc readValue*[T: enum](
|
||||
r: var (BrokerCbor.Reader), value: var T
|
||||
) {.raises: [IOError, SerializationError].} =
|
||||
mixin read
|
||||
var i: int
|
||||
read(r, i)
|
||||
if i < ord(T.low) or i > ord(T.high):
|
||||
raise
|
||||
newException(CborReaderError, "CBOR enum value " & $i & " out of range for " & $T)
|
||||
value = T(i)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Wire types
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
type CborUnit* = object
|
||||
## Empty marker used as the payload of `Result[void, string]` envelopes.
|
||||
## Encodes as a zero-field CBOR map (`{}`).
|
||||
|
||||
type CborResponseEnvelope*[T] = object
|
||||
## Wire representation of `Result[T, string]`.
|
||||
##
|
||||
## With the BrokerCbor flavor (`omitOptionalFields = true`), exactly one
|
||||
## of `ok` and `err` is populated on a well-formed envelope. Decode
|
||||
## validates this in `fromEnvelope`.
|
||||
ok*: Option[T]
|
||||
err*: Option[string]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Result <-> Envelope
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc toEnvelope*[T](r: Result[T, string]): CborResponseEnvelope[T] =
|
||||
if r.isOk():
|
||||
CborResponseEnvelope[T](ok: some(r.value), err: none(string))
|
||||
else:
|
||||
CborResponseEnvelope[T](ok: none(T), err: some(r.error))
|
||||
|
||||
proc fromEnvelope*[T](e: CborResponseEnvelope[T]): Result[T, string] {.raises: [].} =
|
||||
if e.ok.isSome() and e.err.isSome():
|
||||
return Result[T, string].err(
|
||||
"malformed CBOR response envelope: both 'ok' and 'err' present"
|
||||
)
|
||||
if e.ok.isSome():
|
||||
return Result[T, string].ok(e.ok.get())
|
||||
if e.err.isSome():
|
||||
return Result[T, string].err(e.err.get())
|
||||
Result[T, string].err(
|
||||
"malformed CBOR response envelope: neither 'ok' nor 'err' present"
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Encode / Decode helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
template cborEncode*[T](value: T): Result[seq[byte], string] =
|
||||
## Encode `value` to CBOR using the BrokerCbor flavor. Wraps every encode
|
||||
## failure as `Result.err`; never raises.
|
||||
##
|
||||
## Implemented as a template so that `BrokerCbor`'s flavor-bound templates
|
||||
## (`init`, `writeValue`, `PreferredOutputType`) resolve at the user's
|
||||
## call site rather than inside a generic proc — the latter loses access
|
||||
## to the flavor's auto-generated object writers.
|
||||
block:
|
||||
var encRes: Result[seq[byte], string]
|
||||
try:
|
||||
let buf = BrokerCbor.encode(value)
|
||||
encRes = Result[seq[byte], string].ok(buf)
|
||||
except SerializationError as exc:
|
||||
encRes = Result[seq[byte], string].err("cbor encode failed: " & exc.msg)
|
||||
except IOError as exc:
|
||||
encRes = Result[seq[byte], string].err("cbor encode IO failure: " & exc.msg)
|
||||
except CatchableError as exc:
|
||||
encRes =
|
||||
Result[seq[byte], string].err("cbor encode unexpected failure: " & exc.msg)
|
||||
encRes
|
||||
|
||||
template cborEncodeShared*[T](
|
||||
value: T, bufOut: var pointer, lenOut: var int
|
||||
): Result[void, string] =
|
||||
## Refc-safe variant of `cborEncode`: produces an `allocShared0`-owned
|
||||
## buffer and never lets the intermediate `seq[byte]` escape across thread
|
||||
## boundaries.
|
||||
##
|
||||
## On `ok` the caller owns `bufOut` (size `lenOut` bytes) and must
|
||||
## `deallocShared(bufOut)` once done. On empty input `bufOut` is `nil` and
|
||||
## `lenOut` is 0. Used by the CBOR FFI listener path: under `--mm:refc` a
|
||||
## `seq[byte]` produced on the delivery thread cannot be safely shared with
|
||||
## subscriber callbacks invoked synchronously, so we copy the bytes into
|
||||
## shared heap immediately and drop the seq.
|
||||
##
|
||||
## Same template-vs-generic-proc rationale as `cborEncode`.
|
||||
block:
|
||||
bufOut = nil
|
||||
lenOut = 0
|
||||
var encShRes: Result[void, string]
|
||||
try:
|
||||
let buf = BrokerCbor.encode(value)
|
||||
if buf.len > 0:
|
||||
let p = allocShared0(buf.len)
|
||||
copyMem(p, unsafeAddr buf[0], buf.len)
|
||||
bufOut = p
|
||||
lenOut = buf.len
|
||||
encShRes = Result[void, string].ok()
|
||||
except SerializationError as exc:
|
||||
encShRes = Result[void, string].err("cbor encode failed: " & exc.msg)
|
||||
except IOError as exc:
|
||||
encShRes = Result[void, string].err("cbor encode IO failure: " & exc.msg)
|
||||
except CatchableError as exc:
|
||||
encShRes = Result[void, string].err("cbor encode unexpected failure: " & exc.msg)
|
||||
encShRes
|
||||
|
||||
template cborDecode*[T](buf: openArray[byte], _: typedesc[T]): Result[T, string] =
|
||||
## Decode a CBOR-encoded buffer into `T` using the BrokerCbor flavor.
|
||||
## Wraps every decode failure as `Result.err`; never raises. Same
|
||||
## template-vs-generic-proc rationale as `cborEncode`.
|
||||
block:
|
||||
var decRes: Result[T, string]
|
||||
try:
|
||||
let v = BrokerCbor.decode(buf, T)
|
||||
decRes = Result[T, string].ok(v)
|
||||
except SerializationError as exc:
|
||||
decRes = Result[T, string].err("cbor decode failed: " & exc.msg)
|
||||
except IOError as exc:
|
||||
decRes = Result[T, string].err("cbor decode IO failure: " & exc.msg)
|
||||
except CatchableError as exc:
|
||||
decRes = Result[T, string].err("cbor decode unexpected failure: " & exc.msg)
|
||||
decRes
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Result envelope shortcuts
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
template cborEncodeResultEnvelope*[T](r: Result[T, string]): Result[seq[byte], string] =
|
||||
## Encode `Result[T, string]` as a CBOR response envelope.
|
||||
cborEncode(toEnvelope(r))
|
||||
|
||||
template cborDecodeResultEnvelope*[T](
|
||||
buf: openArray[byte], _: typedesc[T]
|
||||
): Result[T, string] =
|
||||
## Decode a CBOR response envelope into `Result[T, string]`.
|
||||
##
|
||||
## Returns the inner `Result` on success, or a framework error string
|
||||
## (prefixed `cbor decode failed: ...`) on a CBOR-level failure.
|
||||
block:
|
||||
let envRes = cborDecode(buf, CborResponseEnvelope[T])
|
||||
var res: Result[T, string]
|
||||
if envRes.isErr():
|
||||
res = Result[T, string].err(envRes.error)
|
||||
else:
|
||||
res = fromEnvelope(envRes.value)
|
||||
res
|
||||
|
||||
{.pop.}
|
||||
@@ -0,0 +1,345 @@
|
||||
## api_cbor_courier — runtime support for the CBOR FFI "buffer courier".
|
||||
## =====================================================================
|
||||
## Part C of the CBOR refactoring (doc/CBOR_Refactoring.md §6).
|
||||
##
|
||||
## A CBOR-mode `<lib>_call` runs on a foreign caller's thread. Instead of
|
||||
## decoding CBOR and driving a momentary chronos loop on that foreign
|
||||
## thread, it becomes a pure courier:
|
||||
##
|
||||
## 1. copy the API name into a fixed POD message,
|
||||
## 2. hand the raw request buffer (by pointer, ownership transferred)
|
||||
## to the processing thread over a `Channel`,
|
||||
## 3. block on a per-call response slot until the processing thread
|
||||
## writes the response back.
|
||||
##
|
||||
## The processing thread owns CBOR decode/encode and the provider call.
|
||||
##
|
||||
## This module is plain runtime code (NOT codegen) used by the generated
|
||||
## library runtime in `api_library.nim`. It deliberately contains no Nim
|
||||
## GC types on the cross-thread message path: `CborCallMsg` is pure POD,
|
||||
## so a foreign thread can enqueue one with zero GC involvement.
|
||||
##
|
||||
## Memory model:
|
||||
## - `reqBuf` — `allocShared0` by `<lib>_allocBuffer`; ownership moves
|
||||
## into the `CborCallMsg`; the processing thread frees it exactly once
|
||||
## after copying the bytes out.
|
||||
## - `respBuf` — `allocShared0` on the processing thread; ownership
|
||||
## returns to the `_call` thread via the slot; the foreign caller
|
||||
## frees it via `<lib>_freeBuffer`.
|
||||
## - Response slots use a `Lock`+`Cond` (zero OS handles) for the
|
||||
## blocking handoff — no busy-poll, no per-slot `ThreadSignalPtr`.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[atomics, locks]
|
||||
|
||||
const CborApiNameMax* = 256
|
||||
## Inline fixed-size buffer for the ASCII API name carried in a courier
|
||||
## message. Carrying the name itself (rather than an interned id) keeps
|
||||
## the message self-describing and avoids a separate id table that could
|
||||
## silently desync from the dispatch `case`.
|
||||
|
||||
const CborMaxSlotSegments = 4
|
||||
## Doubling the slot pool from `origSlotCount` to the 4× ceiling appends at
|
||||
## most two segments beyond the initial one (N → +N → +2N), so three are
|
||||
## ever live; 4 leaves a margin.
|
||||
|
||||
type
|
||||
CborCallMsg* = object
|
||||
## Pure-POD message a foreign `_call` thread hands to the processing
|
||||
## thread. No Nim `string`/`seq`/`ref` — safe to copy through a
|
||||
## `Channel` with zero GC involvement on the foreign thread.
|
||||
apiName*: array[CborApiNameMax, char] ## NUL-terminated ASCII
|
||||
reqBuf*: pointer ## allocShared0; ownership transfers to the processing thread
|
||||
reqLen*: int32
|
||||
slotIdx*: int32 ## index of the response slot to complete
|
||||
targetCtx*: uint32
|
||||
## reduced-A: the FULL BrokerContext the foreign caller addressed. For a
|
||||
## main-context call this equals the library ctx; for a sub-instance call
|
||||
## it carries the sub ctx (same classCtx as the library, distinct
|
||||
## instanceCtx). The processing thread dispatches the adapter against this
|
||||
## so the provider keyed by the sub ctx is reached.
|
||||
|
||||
CborRespSlot = object
|
||||
lock: Lock
|
||||
cond: Cond
|
||||
inUse: Atomic[int] ## 0 free, 1 claimed — claimed via CAS
|
||||
ready: int ## guarded by `lock`: 0 pending, 1 complete
|
||||
respBuf: pointer ## allocShared0; ownership returns to the `_call` thread
|
||||
respLen: int32
|
||||
status: int32 ## the int32 `<lib>_call` returns to the foreign caller
|
||||
|
||||
CborCallRing* = object
|
||||
## Single-lock POD-element MPSC ring, allocated wholly in shared
|
||||
## heap. Replaces `system.Channel[CborCallMsg]` deliberately: that
|
||||
## channel allocates its message slots out of the sender thread's
|
||||
## per-thread Nim allocator, and once the sender thread exits its
|
||||
## TLS-tied allocator descriptor is freed by pthread cleanup. A
|
||||
## subsequent `close()` on the shutdown thread walks straight into
|
||||
## that dead descriptor (caught by ASAN on the stress_mt teardown
|
||||
## path). This ring uses `allocShared` for its storage — single
|
||||
## owner (the `CborCourier`), freed from the same thread that
|
||||
## allocated it, no per-thread allocator involvement.
|
||||
buf: ptr UncheckedArray[CborCallMsg]
|
||||
cap: int
|
||||
head: int ## next index the consumer reads
|
||||
tail: int ## next index a producer writes
|
||||
count: int ## guarded by `lock`
|
||||
lock: Lock
|
||||
|
||||
CborSlotSegment = object
|
||||
## One append-only block of response slots. Existing segments are never
|
||||
## moved or freed until teardown, so a foreign thread blocked in
|
||||
## `waitSlot` on a slot's `Cond` keeps a stable address. This is the
|
||||
## reason the pool grows by *appending* segments rather than
|
||||
## reallocating one array: relocating a slot whose `Lock`/`Cond` a
|
||||
## blocked `_call` is waiting on is a use-after-free.
|
||||
slots: ptr UncheckedArray[CborRespSlot]
|
||||
base: int ## global index of `slots[0]`
|
||||
len: int ## number of slots in this segment
|
||||
|
||||
CborCourier* = object
|
||||
## One per library context. Lives in shared heap; created in
|
||||
## `_createContext`, freed in `_shutdown` after the processing thread
|
||||
## has joined and all in-flight `_call`s have drained.
|
||||
ring*: CborCallRing
|
||||
segs: array[CborMaxSlotSegments, CborSlotSegment]
|
||||
nSegs: Atomic[int]
|
||||
## Live segment count. Published with `moRelease` after a new segment is
|
||||
## fully populated; the lock-free claim scan reads it with `moAcquire`.
|
||||
## Append-only — segments are never removed before teardown.
|
||||
slotCount: int
|
||||
## Total live slots across all segments. Read/written only under
|
||||
## `ring.lock` (growth coordinates the slot pool and the ring together).
|
||||
origSlotCount: int
|
||||
## Set once at construction; the growth ceiling is `4 * origSlotCount`.
|
||||
inFlight*: Atomic[int]
|
||||
## Count of `_call`s that passed the active-check but have not yet
|
||||
## finished reading their slot. `_shutdown` waits for this to reach
|
||||
## zero — while the processing thread is still handling — before it
|
||||
## tells the processing thread to stop.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc newCborCourier*(slotCount: int): ptr CborCourier =
|
||||
## Allocate a courier with `slotCount` response slots. `slotCount` is the
|
||||
## *initial* ceiling on concurrent in-flight `_call`s; the request ring is
|
||||
## sized the same, so the slot pool gates the ring (a `_call` always claims
|
||||
## a slot before enqueuing). On exhaustion the pool and ring grow together
|
||||
## by doubling, up to a hard ceiling of `4 * slotCount` — see `claimSlot`.
|
||||
let c = cast[ptr CborCourier](allocShared0(sizeof(CborCourier)))
|
||||
c.ring.buf =
|
||||
cast[ptr UncheckedArray[CborCallMsg]](allocShared0(slotCount * sizeof(CborCallMsg)))
|
||||
c.ring.cap = slotCount
|
||||
c.ring.head = 0
|
||||
c.ring.tail = 0
|
||||
c.ring.count = 0
|
||||
initLock(c.ring.lock)
|
||||
c.origSlotCount = slotCount
|
||||
c.slotCount = slotCount
|
||||
let seg0 = cast[ptr UncheckedArray[CborRespSlot]](allocShared0(
|
||||
slotCount * sizeof(CborRespSlot)
|
||||
))
|
||||
for i in 0 ..< slotCount:
|
||||
initLock(seg0[i].lock)
|
||||
initCond(seg0[i].cond)
|
||||
seg0[i].inUse.store(0, moRelaxed)
|
||||
c.segs[0] = CborSlotSegment(slots: seg0, base: 0, len: slotCount)
|
||||
c.nSegs.store(1, moRelease)
|
||||
c
|
||||
|
||||
proc freeCborCourier*(c: ptr CborCourier) =
|
||||
## Release a courier. MUST be called only after the processing thread
|
||||
## has joined and `inFlight` has reached zero — see `_shutdown`.
|
||||
if c.isNil:
|
||||
return
|
||||
for s in 0 ..< c.nSegs.load(moAcquire):
|
||||
let seg = addr c.segs[s]
|
||||
for i in 0 ..< seg.len:
|
||||
deinitCond(seg.slots[i].cond)
|
||||
deinitLock(seg.slots[i].lock)
|
||||
deallocShared(seg.slots)
|
||||
deinitLock(c.ring.lock)
|
||||
if not c.ring.buf.isNil:
|
||||
deallocShared(c.ring.buf)
|
||||
deallocShared(c)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ring — MPSC over a fixed-size POD slot array. Single lock for both ends;
|
||||
# the ring is not the contended path (per-call cost is dominated by the
|
||||
# Cond handoff and the chronos coroutine spawn).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc growRingLocked(r: ptr CborCallRing, newCap: int): bool =
|
||||
## Grow the POD ring to `newCap` (> `r.cap`), linearising live elements.
|
||||
## Caller MUST hold `r.lock`. Safe because `CborCallMsg` is pure POD and no
|
||||
## thread holds a pointer into `buf` across the lock.
|
||||
##
|
||||
## Returns false — leaving the ring completely untouched — if the new buffer
|
||||
## cannot be allocated, so the caller can roll back the coordinated pool+ring
|
||||
## growth instead of dereferencing nil while holding the lock.
|
||||
let newBuf =
|
||||
cast[ptr UncheckedArray[CborCallMsg]](allocShared0(newCap * sizeof(CborCallMsg)))
|
||||
if newBuf.isNil:
|
||||
return false
|
||||
for i in 0 ..< r.count:
|
||||
newBuf[i] = r.buf[(r.head + i) mod r.cap]
|
||||
deallocShared(r.buf)
|
||||
r.buf = newBuf
|
||||
r.head = 0
|
||||
r.tail = r.count
|
||||
r.cap = newCap
|
||||
true
|
||||
|
||||
proc tryEnqueue*(r: ptr CborCallRing, msg: CborCallMsg): bool =
|
||||
## Multi-producer. Returns false on full. A `_call` always claims a
|
||||
## response slot before enqueuing and the ring is grown in step with the
|
||||
## slot pool (see `claimSlot`), so the ring cap always matches the live
|
||||
## slot count and a `false` here is a programming error, not backpressure.
|
||||
acquire(r.lock)
|
||||
if r.count >= r.cap:
|
||||
release(r.lock)
|
||||
return false
|
||||
r.buf[r.tail] = msg
|
||||
r.tail = (r.tail + 1) mod r.cap
|
||||
inc r.count
|
||||
release(r.lock)
|
||||
true
|
||||
|
||||
proc tryDequeue*(r: ptr CborCallRing, dst: var CborCallMsg): bool =
|
||||
## Single consumer. Returns false on empty.
|
||||
acquire(r.lock)
|
||||
if r.count == 0:
|
||||
release(r.lock)
|
||||
return false
|
||||
dst = r.buf[r.head]
|
||||
r.head = (r.head + 1) mod r.cap
|
||||
dec r.count
|
||||
release(r.lock)
|
||||
true
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Response slots
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc slotAt(c: ptr CborCourier, idx: int): ptr CborRespSlot {.inline.} =
|
||||
## Map a global slot index to its slot in the owning segment. Segments are
|
||||
## append-only and never relocated, so a published index stays valid.
|
||||
for s in 0 ..< c.nSegs.load(moAcquire):
|
||||
let seg = addr c.segs[s]
|
||||
if idx >= seg.base and idx < seg.base + seg.len:
|
||||
return addr seg.slots[idx - seg.base]
|
||||
nil
|
||||
|
||||
proc initClaimedSlot(s: ptr CborRespSlot) {.inline.} =
|
||||
acquire(s.lock)
|
||||
s.ready = 0
|
||||
s.respBuf = nil
|
||||
s.respLen = 0
|
||||
s.status = 0
|
||||
release(s.lock)
|
||||
|
||||
proc tryClaimScan(c: ptr CborCourier): int =
|
||||
## Scan all live slots for a free one; CAS-claim and reset it. Returns the
|
||||
## global index, or -1 if none free. Lock-free over the published segments.
|
||||
for sgi in 0 ..< c.nSegs.load(moAcquire):
|
||||
let seg = addr c.segs[sgi]
|
||||
for i in 0 ..< seg.len:
|
||||
var expected = 0
|
||||
if seg.slots[i].inUse.compareExchange(expected, 1, moAcquire, moRelaxed):
|
||||
initClaimedSlot(addr seg.slots[i])
|
||||
return seg.base + i
|
||||
-1
|
||||
|
||||
proc claimSlot*(c: ptr CborCourier): int =
|
||||
## Claim a free response slot. Returns its index, or -1 only when the pool
|
||||
## is at its `4 * origSlotCount` ceiling and fully in-use. On exhaustion
|
||||
## below the ceiling the pool grows by appending a new segment (existing
|
||||
## slots are never moved) and the ring grows in step — both under
|
||||
## `ring.lock`. Growth is the rare slow path.
|
||||
let fast = tryClaimScan(c)
|
||||
if fast >= 0:
|
||||
return fast
|
||||
# Pool exhausted. Coordinate growth under the ring lock.
|
||||
acquire(c.ring.lock)
|
||||
# Re-scan under the lock: a concurrent release or a concurrent grow may
|
||||
# have produced a usable slot since the lock-free scan above.
|
||||
let again = tryClaimScan(c)
|
||||
if again >= 0:
|
||||
release(c.ring.lock)
|
||||
return again
|
||||
let curCount = c.slotCount
|
||||
let newCount = min(curCount * 2, c.origSlotCount * 4)
|
||||
let segIdx = c.nSegs.load(moAcquire)
|
||||
if newCount == curCount or segIdx >= CborMaxSlotSegments:
|
||||
release(c.ring.lock) # at the ceiling — retain the drop contract
|
||||
return -1
|
||||
let addLen = newCount - curCount
|
||||
let seg =
|
||||
cast[ptr UncheckedArray[CborRespSlot]](allocShared0(addLen * sizeof(CborRespSlot)))
|
||||
if seg.isNil:
|
||||
# OOM allocating the new slot segment: nothing has been mutated yet, so
|
||||
# release the lock and retain the refusal (drop) contract rather than
|
||||
# crashing in initLock/initCond.
|
||||
release(c.ring.lock)
|
||||
return -1
|
||||
for i in 0 ..< addLen:
|
||||
initLock(seg[i].lock)
|
||||
initCond(seg[i].cond)
|
||||
seg[i].inUse.store(0, moRelaxed)
|
||||
# Grow the ring in step BEFORE committing any pool state. If the ring buffer
|
||||
# can't be allocated, roll back the freshly-built segment (nothing has been
|
||||
# published — slotCount/segs/nSegs are untouched and the ring is left intact)
|
||||
# and retain the refusal contract.
|
||||
if not growRingLocked(addr c.ring, newCount):
|
||||
for i in 0 ..< addLen:
|
||||
deinitCond(seg[i].cond)
|
||||
deinitLock(seg[i].lock)
|
||||
deallocShared(seg)
|
||||
release(c.ring.lock)
|
||||
return -1
|
||||
# Ring grown; the pool+ring growth is guaranteed to complete. Claim slot 0 of
|
||||
# the new segment BEFORE publishing it, so no concurrent scanner can race us.
|
||||
var expected = 0
|
||||
discard seg[0].inUse.compareExchange(expected, 1, moAcquire, moRelaxed)
|
||||
initClaimedSlot(addr seg[0])
|
||||
c.segs[segIdx] = CborSlotSegment(slots: seg, base: curCount, len: addLen)
|
||||
c.slotCount = newCount # ring cap already == newCount
|
||||
c.nSegs.store(segIdx + 1, moRelease) # publish last
|
||||
release(c.ring.lock)
|
||||
curCount # global index of seg[0], already claimed
|
||||
|
||||
proc releaseSlot*(c: ptr CborCourier, idx: int) =
|
||||
## Return a slot to the free pool. Call only after `waitSlot` returned.
|
||||
slotAt(c, idx).inUse.store(0, moRelease)
|
||||
|
||||
proc completeSlot*(
|
||||
c: ptr CborCourier, idx: int, respBuf: pointer, respLen: int32, status: int32
|
||||
) =
|
||||
## Processing-thread side: publish a response and wake the waiting
|
||||
## `_call`. `respBuf` ownership passes to the `_call` thread.
|
||||
let s = slotAt(c, idx)
|
||||
acquire(s.lock)
|
||||
s.respBuf = respBuf
|
||||
s.respLen = respLen
|
||||
s.status = status
|
||||
s.ready = 1
|
||||
signal(s.cond)
|
||||
release(s.lock)
|
||||
|
||||
proc waitSlot*(
|
||||
c: ptr CborCourier, idx: int
|
||||
): tuple[respBuf: pointer, respLen: int32, status: int32] =
|
||||
## Foreign `_call` side: block until `completeSlot` publishes a response.
|
||||
## Zero-fd blocking handoff via `Cond` — no busy-poll.
|
||||
let s = slotAt(c, idx)
|
||||
acquire(s.lock)
|
||||
while s.ready == 0:
|
||||
wait(s.cond, s.lock)
|
||||
result = (s.respBuf, s.respLen, s.status)
|
||||
s.ready = 0
|
||||
release(s.lock)
|
||||
|
||||
{.pop.}
|
||||
@@ -0,0 +1,117 @@
|
||||
## Runtime schema-descriptor types for the CBOR FFI discovery API.
|
||||
##
|
||||
## `<lib>_listApis` and `<lib>_getSchema` return JSON-encoded views of these
|
||||
## records so dynamic clients can introspect a library's surface without
|
||||
## referring to the build-time generated headers.
|
||||
##
|
||||
## These types are hand-rolled (not produced by the broker macros) and are
|
||||
## therefore part of the *stable* CBOR FFI v1 contract: changes to fields
|
||||
## here are wire-breaking. Add new fields rather than rename / reorder.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[json, options]
|
||||
import ./api_cbor_codec
|
||||
|
||||
export api_cbor_codec
|
||||
|
||||
type
|
||||
ApiFieldInfo* = object
|
||||
name*: string
|
||||
nimType*: string
|
||||
|
||||
ApiEnumValueInfo* = object
|
||||
name*: string
|
||||
ordinal*: int
|
||||
|
||||
ApiTypeInfo* = object
|
||||
name*: string
|
||||
kind*: string ## "object" / "enum" / "alias" / "distinct"; matches `ApiTypeKind`.
|
||||
fields*: seq[ApiFieldInfo]
|
||||
enumValues*: seq[ApiEnumValueInfo]
|
||||
underlyingType*: string
|
||||
|
||||
ApiRequestInfo* = object
|
||||
apiName*: string
|
||||
argsType*: string
|
||||
## Nim type name of the synthesised args struct;
|
||||
## empty string for zero-arg requests.
|
||||
argFields*: seq[ApiFieldInfo]
|
||||
responseType*: string
|
||||
|
||||
ApiEventInfo* = object
|
||||
apiName*: string
|
||||
payloadType*: string
|
||||
|
||||
ApiList* = object ## Lightweight payload returned by `<lib>_listApis`.
|
||||
libName*: string
|
||||
requests*: seq[string]
|
||||
events*: seq[string]
|
||||
|
||||
LibraryDescriptor* = object ## Full payload returned by `<lib>_getSchema`.
|
||||
libName*: string
|
||||
cddl*: string ## Verbatim contents of the generated `<lib>.cddl`.
|
||||
requests*: seq[ApiRequestInfo]
|
||||
events*: seq[ApiEventInfo]
|
||||
types*: seq[ApiTypeInfo]
|
||||
|
||||
{.pop.}
|
||||
|
||||
# JSON serialisation lives outside `{.push raises: [].}` because std/json
|
||||
# indexing can raise KeyError.
|
||||
|
||||
proc toJson*(f: ApiFieldInfo): JsonNode =
|
||||
%*{"name": f.name, "nimType": f.nimType}
|
||||
|
||||
proc toJson*(v: ApiEnumValueInfo): JsonNode =
|
||||
%*{"name": v.name, "ordinal": v.ordinal}
|
||||
|
||||
proc toJson*(t: ApiTypeInfo): JsonNode =
|
||||
result = %*{
|
||||
"name": t.name,
|
||||
"kind": t.kind,
|
||||
"fields": newJArray(),
|
||||
"enumValues": newJArray(),
|
||||
"underlyingType": t.underlyingType,
|
||||
}
|
||||
for f in t.fields:
|
||||
result["fields"].add(f.toJson())
|
||||
for v in t.enumValues:
|
||||
result["enumValues"].add(v.toJson())
|
||||
|
||||
proc toJson*(r: ApiRequestInfo): JsonNode =
|
||||
result = %*{
|
||||
"apiName": r.apiName,
|
||||
"argsType": r.argsType,
|
||||
"argFields": newJArray(),
|
||||
"responseType": r.responseType,
|
||||
}
|
||||
for f in r.argFields:
|
||||
result["argFields"].add(f.toJson())
|
||||
|
||||
proc toJson*(e: ApiEventInfo): JsonNode =
|
||||
%*{"apiName": e.apiName, "payloadType": e.payloadType}
|
||||
|
||||
proc toJson*(a: ApiList): JsonNode =
|
||||
%*{"libName": a.libName, "requests": a.requests, "events": a.events}
|
||||
|
||||
proc toJson*(d: LibraryDescriptor): JsonNode =
|
||||
result = %*{
|
||||
"libName": d.libName,
|
||||
"cddl": d.cddl,
|
||||
"requests": newJArray(),
|
||||
"events": newJArray(),
|
||||
"types": newJArray(),
|
||||
}
|
||||
for r in d.requests:
|
||||
result["requests"].add(r.toJson())
|
||||
for e in d.events:
|
||||
result["events"].add(e.toJson())
|
||||
for t in d.types:
|
||||
result["types"].add(t.toJson())
|
||||
|
||||
proc toJsonString*(a: ApiList): string =
|
||||
$a.toJson()
|
||||
|
||||
proc toJsonString*(d: LibraryDescriptor): string =
|
||||
$d.toJson()
|
||||
@@ -0,0 +1,171 @@
|
||||
## api_cbor_event_courier — fire-and-forget ring for CBOR FFI event delivery.
|
||||
## ============================================================================
|
||||
## Part D-3 of the CBOR refactoring (doc/CBOR_Round2_PartD_EventCourier.md).
|
||||
##
|
||||
## A CBOR-mode event emitted by a provider on the processing thread needs to
|
||||
## fan out to all foreign-callback subscribers without blocking the provider.
|
||||
## The shape is the mirror image of `api_cbor_courier`:
|
||||
##
|
||||
## producer (processing thread)
|
||||
## 1. CBOR-encode the event payload **once** into a shared-heap buffer,
|
||||
## 2. enqueue an `EventMsg` carrying `(eventName, ctx, buf, bufLen)`
|
||||
## — ownership of `buf` transfers to the consumer,
|
||||
## 3. wake the delivery thread via its broker dispatch signal.
|
||||
##
|
||||
## consumer (delivery thread, via `registerBrokerPoller`)
|
||||
## 1. dequeue messages from the ring,
|
||||
## 2. snapshot the foreign-subscriber list for `(ctx, eventName)`,
|
||||
## 3. invoke each foreign callback synchronously,
|
||||
## 4. free the buffer.
|
||||
##
|
||||
## Differences from `api_cbor_courier`:
|
||||
## - **No response slots, no `inFlight` counter** — events are
|
||||
## fire-and-forget. Producers do not block, do not wait for a reply.
|
||||
## - Ring is sized for **burst capacity** (default 256), not for
|
||||
## concurrent in-flight count. A full ring drops the event with a
|
||||
## diagnostic (logged by the caller) — appropriate for the
|
||||
## fire-and-forget contract.
|
||||
## - `eventName` is carried inline as a fixed-size NUL-terminated
|
||||
## ASCII buffer (same convention as `CborCallMsg.apiName`) so the
|
||||
## message stays POD — zero GC involvement on the producer side.
|
||||
##
|
||||
## This module is plain runtime code (NOT codegen) used by the generated
|
||||
## library runtime in `api_library.nim`.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/locks
|
||||
|
||||
const CborEventNameMax* = 256
|
||||
## Inline fixed-size buffer for the ASCII event name carried in a
|
||||
## courier message. Same value as `CborApiNameMax` — every event name
|
||||
## the CBOR-mode subscribe surface accepts already fits within this
|
||||
## bound (the wrapper validates name length).
|
||||
|
||||
type
|
||||
CborEventMsg* = object
|
||||
## Pure-POD message handed from the processing thread (producer) to
|
||||
## the delivery thread (consumer). No Nim `string` / `seq` / `ref`
|
||||
## crosses the channel — the producer encoded the payload into a
|
||||
## shared-heap buffer and transfers ownership of it via `buf`.
|
||||
eventName*: array[CborEventNameMax, char] ## NUL-terminated ASCII
|
||||
ctx*: uint32 ## BrokerContext.uint32; identifies the per-ctx sub list
|
||||
buf*: pointer
|
||||
## `allocShared0`; ownership transferred to the consumer.
|
||||
## The consumer frees this exactly once after the fan-out completes.
|
||||
bufLen*: int32
|
||||
|
||||
CborEventRing* = object
|
||||
## Single-lock POD-element ring, allocated wholly in shared heap.
|
||||
## Same shape (and same rationale) as `CborCallRing` —
|
||||
## `system.Channel[T]` is avoided to keep the storage out of the
|
||||
## producer thread's per-thread Nim allocator (would leak/UAF when
|
||||
## the producer thread exits before the consumer fully drains).
|
||||
buf: ptr UncheckedArray[CborEventMsg]
|
||||
cap: int
|
||||
origCap: int ## set once at construction; growth ceiling is `4 * origCap`
|
||||
head: int ## next index the consumer reads
|
||||
tail: int ## next index a producer writes
|
||||
count: int ## guarded by `lock`
|
||||
lock: Lock
|
||||
|
||||
CborEventCourier* = object
|
||||
## One per library context. Lives in shared heap; created in
|
||||
## `_createContext`, freed in `_shutdown` **after both threads have
|
||||
## joined**. The teardown sequence drains any messages still in the
|
||||
## ring (freeing their `buf`s) before deallocating the ring storage.
|
||||
ring*: CborEventRing
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc newCborEventCourier*(ringCap: int): ptr CborEventCourier =
|
||||
## Allocate an event courier sized for `ringCap` outstanding events.
|
||||
## Producers that find the ring full drop the event (events are
|
||||
## fire-and-forget). Pick `ringCap` generously — there's no slot pool
|
||||
## gating it the way `CborCourier`'s slot count gates its ring.
|
||||
let c = cast[ptr CborEventCourier](allocShared0(sizeof(CborEventCourier)))
|
||||
c.ring.buf =
|
||||
cast[ptr UncheckedArray[CborEventMsg]](allocShared0(ringCap * sizeof(CborEventMsg)))
|
||||
c.ring.cap = ringCap
|
||||
c.ring.origCap = ringCap
|
||||
c.ring.head = 0
|
||||
c.ring.tail = 0
|
||||
c.ring.count = 0
|
||||
initLock(c.ring.lock)
|
||||
c
|
||||
|
||||
proc drainAndFree*(c: ptr CborEventCourier) =
|
||||
## Free any messages still in the ring (deallocating their `buf`),
|
||||
## then free the ring storage and the courier itself. MUST be called
|
||||
## only after both the producer and consumer threads have joined.
|
||||
if c.isNil:
|
||||
return
|
||||
# Drain remaining messages — buffers must be freed exactly once.
|
||||
acquire(c.ring.lock)
|
||||
while c.ring.count > 0:
|
||||
let m = c.ring.buf[c.ring.head]
|
||||
if not m.buf.isNil:
|
||||
deallocShared(m.buf)
|
||||
c.ring.head = (c.ring.head + 1) mod c.ring.cap
|
||||
dec c.ring.count
|
||||
release(c.ring.lock)
|
||||
deinitLock(c.ring.lock)
|
||||
if not c.ring.buf.isNil:
|
||||
deallocShared(c.ring.buf)
|
||||
deallocShared(c)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ring — single-lock MPSC over a fixed-size POD slot array.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc tryEnqueue*(r: ptr CborEventRing, msg: CborEventMsg): bool =
|
||||
## Multi-producer (though in practice the producer is the single
|
||||
## processing thread). Returns false on full — the caller is
|
||||
## responsible for freeing `msg.buf` in that case (the buffer never
|
||||
## entered the ring, so the ring never took ownership).
|
||||
acquire(r.lock)
|
||||
if r.count >= r.cap:
|
||||
# Full: grow by doubling, up to a hard ceiling of `4 * origCap`. At the
|
||||
# ceiling retain the fire-and-forget drop contract.
|
||||
let newCap = min(r.cap * 2, r.origCap * 4)
|
||||
if newCap == r.cap:
|
||||
release(r.lock)
|
||||
return false
|
||||
let newBuf = cast[ptr UncheckedArray[CborEventMsg]](allocShared0(
|
||||
newCap * sizeof(CborEventMsg)
|
||||
))
|
||||
if newBuf.isNil:
|
||||
# OOM: keep the existing buffer untouched and fall back to the drop
|
||||
# contract (same as hitting the ceiling) rather than dereferencing nil.
|
||||
release(r.lock)
|
||||
return false
|
||||
for i in 0 ..< r.count:
|
||||
newBuf[i] = r.buf[(r.head + i) mod r.cap]
|
||||
deallocShared(r.buf)
|
||||
r.buf = newBuf
|
||||
r.head = 0
|
||||
r.tail = r.count
|
||||
r.cap = newCap
|
||||
r.buf[r.tail] = msg
|
||||
r.tail = (r.tail + 1) mod r.cap
|
||||
inc r.count
|
||||
release(r.lock)
|
||||
true
|
||||
|
||||
proc tryDequeue*(r: ptr CborEventRing, dst: var CborEventMsg): bool =
|
||||
## Single consumer (the delivery thread's event-courier poller).
|
||||
## Returns false on empty. Ownership of `dst.buf` transfers to the
|
||||
## caller — they must `deallocShared` it after the fan-out.
|
||||
acquire(r.lock)
|
||||
if r.count == 0:
|
||||
release(r.lock)
|
||||
return false
|
||||
dst = r.buf[r.head]
|
||||
r.head = (r.head + 1) mod r.cap
|
||||
dec r.count
|
||||
release(r.lock)
|
||||
true
|
||||
|
||||
{.pop.}
|
||||
@@ -0,0 +1,403 @@
|
||||
## CBOR Subscription Registry
|
||||
## --------------------------
|
||||
## Refc-safe subscription book-keeping for the CBOR FFI listener path.
|
||||
##
|
||||
## The CBOR-mode listener delivery thread crosses GC boundaries with the
|
||||
## subscriber registration path (subscribe/unsubscribe run on foreign caller
|
||||
## threads via the C ABI). Under `--mm:orc` atomic refcounts make a plain
|
||||
## `Table[(uint32, string), seq[Subscription]]` work; under `--mm:refc` the
|
||||
## per-thread heaps + STW collector cannot safely see another thread's
|
||||
## refcounted pointers, which used to gate the Phase 9F listener stress
|
||||
## under macOS+Nim 2.2.4+refc+debug.
|
||||
##
|
||||
## This module replaces that GC'd container with a hand-rolled shared-heap
|
||||
## hash table:
|
||||
## - `BucketHead` (one per `(ctx, eventName)` key) and `SubNode` (one per
|
||||
## subscription) are allocated via `allocShared0`.
|
||||
## - The event-name key is stored as an owned `cstring`
|
||||
## (`allocCStringCopy` at insertion, `freeCString` when the bucket goes
|
||||
## away).
|
||||
## - Bucket arrays are `ptr UncheckedArray[ptr BucketHead]`, never `seq`.
|
||||
##
|
||||
## All public procs are `{.gcsafe, raises: [].}` and acquire the registry's
|
||||
## internal `Lock`. Snapshot copies `(cb, userData)` to a freshly-allocated
|
||||
## shared buffer under the lock, so callbacks fan out unlocked against POD
|
||||
## values that no concurrent unsubscriber can free.
|
||||
##
|
||||
## Callback type: stored as `pointer` so this module is generic across
|
||||
## libraries. Callers cast back to the per-library `<lib>CborEventCallback`
|
||||
## (a `cdecl, gcsafe, raises: []` proc type) at the call site.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/locks
|
||||
|
||||
type
|
||||
SubSnapshot* = object ## A POD copy of `(cb, userData)`. Callbacks fan out unlocked.
|
||||
cb*: pointer
|
||||
userData*: pointer
|
||||
|
||||
SubNode = object
|
||||
handle: uint64
|
||||
cb: pointer
|
||||
userData: pointer
|
||||
next: ptr SubNode
|
||||
|
||||
BucketHead = object
|
||||
ctx: uint32
|
||||
eventName: cstring # owned (allocCStringCopy)
|
||||
eventNameLen: int
|
||||
subsHead: ptr SubNode
|
||||
subsCount: int
|
||||
next: ptr BucketHead # collision chain
|
||||
|
||||
SubsRegistry* = object
|
||||
buckets: ptr UncheckedArray[ptr BucketHead]
|
||||
bucketsLen: uint32 # always a power of two; mask = bucketsLen - 1
|
||||
entryCount: int # live BucketHead count, drives resize
|
||||
lock: Lock
|
||||
|
||||
const
|
||||
InitialBuckets: uint32 = 32
|
||||
ResizeNumerator = 3
|
||||
ResizeDenominator = 4 # resize at load factor 0.75
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers (no locking — caller must hold reg.lock)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc cstrLen(s: cstring): int {.inline, raises: [].} =
|
||||
if s.isNil:
|
||||
return 0
|
||||
var p = cast[ptr UncheckedArray[char]](s)
|
||||
var i = 0
|
||||
while p[i] != '\0':
|
||||
inc i
|
||||
i
|
||||
|
||||
proc cstrEq(a: cstring, aLen: int, b: cstring, bLen: int): bool {.inline.} =
|
||||
if aLen != bLen:
|
||||
return false
|
||||
if aLen == 0:
|
||||
return true
|
||||
let ap = cast[ptr UncheckedArray[byte]](a)
|
||||
let bp = cast[ptr UncheckedArray[byte]](b)
|
||||
for i in 0 ..< aLen:
|
||||
if ap[i] != bp[i]:
|
||||
return false
|
||||
true
|
||||
|
||||
proc cstrAlloc(s: cstring, sLen: int): cstring {.inline, raises: [].} =
|
||||
## Local mirror of `allocCStringCopy(string)` for `cstring` input — avoids
|
||||
## pulling in `api_common` (and its chronos chain) here.
|
||||
if sLen == 0:
|
||||
return cast[cstring](nil)
|
||||
let buf = cast[cstring](allocShared(sLen + 1))
|
||||
let src = cast[pointer](s)
|
||||
copyMem(buf, src, sLen)
|
||||
cast[ptr char](cast[int](buf) + sLen)[] = '\0'
|
||||
buf
|
||||
|
||||
proc cstrFree(s: cstring) {.inline.} =
|
||||
if not s.isNil:
|
||||
deallocShared(s)
|
||||
|
||||
proc keyHash(ctx: uint32, name: cstring, nameLen: int): uint32 {.inline.} =
|
||||
# FNV-1a-ish, seeded with ctx so two ctxs sharing a name spread across buckets.
|
||||
var h: uint32 = 2166136261'u32 xor ctx
|
||||
if nameLen > 0:
|
||||
let p = cast[ptr UncheckedArray[byte]](name)
|
||||
for i in 0 ..< nameLen:
|
||||
h = h xor uint32(p[i])
|
||||
h = h * 16777619'u32
|
||||
h
|
||||
|
||||
proc bucketIndex(
|
||||
reg: ptr SubsRegistry, ctx: uint32, name: cstring, nameLen: int
|
||||
): uint32 {.inline.} =
|
||||
keyHash(ctx, name, nameLen) and (reg.bucketsLen - 1'u32)
|
||||
|
||||
proc findBucket(
|
||||
reg: ptr SubsRegistry, ctx: uint32, name: cstring, nameLen: int
|
||||
): ptr BucketHead =
|
||||
let idx = bucketIndex(reg, ctx, name, nameLen)
|
||||
var b = reg.buckets[idx]
|
||||
while not b.isNil:
|
||||
if b.ctx == ctx and cstrEq(b.eventName, b.eventNameLen, name, nameLen):
|
||||
return b
|
||||
b = b.next
|
||||
nil
|
||||
|
||||
proc unlinkBucket(reg: ptr SubsRegistry, target: ptr BucketHead) =
|
||||
let idx = bucketIndex(reg, target.ctx, target.eventName, target.eventNameLen)
|
||||
var prev: ptr BucketHead = nil
|
||||
var cur = reg.buckets[idx]
|
||||
while not cur.isNil:
|
||||
if cur == target:
|
||||
if prev.isNil:
|
||||
reg.buckets[idx] = cur.next
|
||||
else:
|
||||
prev.next = cur.next
|
||||
return
|
||||
prev = cur
|
||||
cur = cur.next
|
||||
|
||||
proc freeNodeChain(head: ptr SubNode) =
|
||||
var cur = head
|
||||
while not cur.isNil:
|
||||
let nxt = cur.next
|
||||
deallocShared(cur)
|
||||
cur = nxt
|
||||
|
||||
proc disposeBucket(b: ptr BucketHead) =
|
||||
freeNodeChain(b.subsHead)
|
||||
cstrFree(b.eventName)
|
||||
deallocShared(b)
|
||||
|
||||
proc resize(reg: ptr SubsRegistry, newLen: uint32) =
|
||||
## Double-or-larger rehash. Caller holds the lock.
|
||||
let bytes = sizeof(ptr BucketHead) * int(newLen)
|
||||
let newBuckets = cast[ptr UncheckedArray[ptr BucketHead]](allocShared0(bytes))
|
||||
let oldBuckets = reg.buckets
|
||||
let oldLen = reg.bucketsLen
|
||||
reg.buckets = newBuckets
|
||||
reg.bucketsLen = newLen
|
||||
for i in 0 ..< oldLen:
|
||||
var cur = oldBuckets[i]
|
||||
while not cur.isNil:
|
||||
let nxt = cur.next
|
||||
let idx = bucketIndex(reg, cur.ctx, cur.eventName, cur.eventNameLen)
|
||||
cur.next = reg.buckets[idx]
|
||||
reg.buckets[idx] = cur
|
||||
cur = nxt
|
||||
deallocShared(oldBuckets)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc subsRegistryNew*(): ptr SubsRegistry {.gcsafe, raises: [].} =
|
||||
let reg = cast[ptr SubsRegistry](allocShared0(sizeof(SubsRegistry)))
|
||||
let bytes = sizeof(ptr BucketHead) * int(InitialBuckets)
|
||||
reg.buckets = cast[ptr UncheckedArray[ptr BucketHead]](allocShared0(bytes))
|
||||
reg.bucketsLen = InitialBuckets
|
||||
reg.entryCount = 0
|
||||
initLock(reg.lock)
|
||||
reg
|
||||
|
||||
proc subsRegistryFree*(reg: ptr SubsRegistry) {.gcsafe, raises: [].} =
|
||||
## Tear the entire registry down. Not normally called by the codegen — the
|
||||
## generated runtime currently leaks the registry at process exit, matching
|
||||
## the prior `Table` behaviour. Provided for completeness / tests.
|
||||
if reg.isNil:
|
||||
return
|
||||
for i in 0 ..< reg.bucketsLen:
|
||||
var cur = reg.buckets[i]
|
||||
while not cur.isNil:
|
||||
let nxt = cur.next
|
||||
disposeBucket(cur)
|
||||
cur = nxt
|
||||
deallocShared(reg.buckets)
|
||||
deinitLock(reg.lock)
|
||||
deallocShared(reg)
|
||||
|
||||
proc subsRegistryAdd*(
|
||||
reg: ptr SubsRegistry,
|
||||
ctx: uint32,
|
||||
name: cstring,
|
||||
handle: uint64,
|
||||
cb: pointer,
|
||||
userData: pointer,
|
||||
) {.gcsafe, raises: [].} =
|
||||
## Idempotent on `handle`: if a node with the same handle already exists for
|
||||
## the key, the call is a no-op. Handles are minted by an atomic counter at
|
||||
## the codegen call site so this branch normally never fires; it exists to
|
||||
## keep the data structure self-consistent under bizarre caller bugs.
|
||||
{.cast(gcsafe).}:
|
||||
withLock reg.lock:
|
||||
let nameLen = cstrLen(name)
|
||||
var bucket = findBucket(reg, ctx, name, nameLen)
|
||||
if bucket.isNil:
|
||||
bucket = cast[ptr BucketHead](allocShared0(sizeof(BucketHead)))
|
||||
bucket.ctx = ctx
|
||||
bucket.eventName = cstrAlloc(name, nameLen)
|
||||
bucket.eventNameLen = nameLen
|
||||
bucket.subsHead = nil
|
||||
bucket.subsCount = 0
|
||||
let idx = bucketIndex(reg, ctx, name, nameLen)
|
||||
bucket.next = reg.buckets[idx]
|
||||
reg.buckets[idx] = bucket
|
||||
inc reg.entryCount
|
||||
if reg.entryCount * ResizeDenominator > int(reg.bucketsLen) * ResizeNumerator:
|
||||
resize(reg, reg.bucketsLen * 2'u32)
|
||||
else:
|
||||
var cur = bucket.subsHead
|
||||
while not cur.isNil:
|
||||
if cur.handle == handle:
|
||||
return
|
||||
cur = cur.next
|
||||
let node = cast[ptr SubNode](allocShared0(sizeof(SubNode)))
|
||||
node.handle = handle
|
||||
node.cb = cb
|
||||
node.userData = userData
|
||||
node.next = bucket.subsHead
|
||||
bucket.subsHead = node
|
||||
inc bucket.subsCount
|
||||
|
||||
proc subsRegistryRemoveOne*(
|
||||
reg: ptr SubsRegistry, ctx: uint32, name: cstring, handle: uint64
|
||||
): int32 {.gcsafe, raises: [], discardable.} =
|
||||
## Returns: 0 ok, -2 key not found, -3 handle not found.
|
||||
{.cast(gcsafe).}:
|
||||
withLock reg.lock:
|
||||
let nameLen = cstrLen(name)
|
||||
let bucket = findBucket(reg, ctx, name, nameLen)
|
||||
if bucket.isNil:
|
||||
return -2'i32
|
||||
var prev: ptr SubNode = nil
|
||||
var cur = bucket.subsHead
|
||||
while not cur.isNil:
|
||||
if cur.handle == handle:
|
||||
if prev.isNil:
|
||||
bucket.subsHead = cur.next
|
||||
else:
|
||||
prev.next = cur.next
|
||||
deallocShared(cur)
|
||||
dec bucket.subsCount
|
||||
if bucket.subsCount == 0:
|
||||
unlinkBucket(reg, bucket)
|
||||
disposeBucket(bucket)
|
||||
dec reg.entryCount
|
||||
return 0'i32
|
||||
prev = cur
|
||||
cur = cur.next
|
||||
return -3'i32
|
||||
|
||||
proc subsRegistryRemoveAllForKey*(
|
||||
reg: ptr SubsRegistry, ctx: uint32, name: cstring
|
||||
): int32 {.gcsafe, raises: [], discardable.} =
|
||||
## Returns 0 if the key existed (and was dropped), -2 otherwise.
|
||||
{.cast(gcsafe).}:
|
||||
withLock reg.lock:
|
||||
let nameLen = cstrLen(name)
|
||||
let bucket = findBucket(reg, ctx, name, nameLen)
|
||||
if bucket.isNil:
|
||||
return -2'i32
|
||||
unlinkBucket(reg, bucket)
|
||||
disposeBucket(bucket)
|
||||
dec reg.entryCount
|
||||
return 0'i32
|
||||
|
||||
proc subsRegistryRemoveAllForKeyN*(
|
||||
reg: ptr SubsRegistry, ctx: uint32, name: cstring
|
||||
): int32 {.gcsafe, raises: [].} =
|
||||
## Returns the number of subscriptions removed (>= 0), or -2 if the key was
|
||||
## not found. Teardown paths must decrement the shared per-event subs-count
|
||||
## by the exact number removed (not reset to 0) so a sibling context/instance
|
||||
## sharing the event name is not silenced.
|
||||
{.cast(gcsafe).}:
|
||||
withLock reg.lock:
|
||||
let nameLen = cstrLen(name)
|
||||
let bucket = findBucket(reg, ctx, name, nameLen)
|
||||
if bucket.isNil:
|
||||
return -2'i32
|
||||
let removed = int32(bucket.subsCount)
|
||||
unlinkBucket(reg, bucket)
|
||||
disposeBucket(bucket)
|
||||
dec reg.entryCount
|
||||
return removed
|
||||
|
||||
proc subsRegistrySnapshot*(
|
||||
reg: ptr SubsRegistry,
|
||||
ctx: uint32,
|
||||
name: cstring,
|
||||
bufOut: var ptr UncheckedArray[SubSnapshot],
|
||||
lenOut: var int,
|
||||
) {.gcsafe, raises: [].} =
|
||||
## Allocates a shared-heap array of `(cb, userData)` for the bucket. Sets
|
||||
## `bufOut = nil`, `lenOut = 0` if there are no subscribers — callers should
|
||||
## then skip `subsRegistrySnapshotFree`.
|
||||
bufOut = nil
|
||||
lenOut = 0
|
||||
{.cast(gcsafe).}:
|
||||
withLock reg.lock:
|
||||
let nameLen = cstrLen(name)
|
||||
let bucket = findBucket(reg, ctx, name, nameLen)
|
||||
if bucket.isNil or bucket.subsCount == 0:
|
||||
return
|
||||
let n = bucket.subsCount
|
||||
let bytes = sizeof(SubSnapshot) * n
|
||||
let buf = cast[ptr UncheckedArray[SubSnapshot]](allocShared0(bytes))
|
||||
var cur = bucket.subsHead
|
||||
var i = 0
|
||||
while not cur.isNil and i < n:
|
||||
buf[i].cb = cur.cb
|
||||
buf[i].userData = cur.userData
|
||||
cur = cur.next
|
||||
inc i
|
||||
bufOut = buf
|
||||
lenOut = i
|
||||
|
||||
proc subsRegistrySnapshotFree*(buf: ptr UncheckedArray[SubSnapshot]) {.inline.} =
|
||||
if not buf.isNil:
|
||||
deallocShared(buf)
|
||||
|
||||
proc subsRegistryFreeForCtx*(
|
||||
reg: ptr SubsRegistry, ctx: uint32
|
||||
) {.gcsafe, raises: [].} =
|
||||
## Drops every bucket whose `ctx` matches. Called from `_shutdown(ctx)`
|
||||
## after the processing thread has been joined, so no concurrent delivery
|
||||
## can race with this teardown.
|
||||
{.cast(gcsafe).}:
|
||||
withLock reg.lock:
|
||||
for i in 0 ..< reg.bucketsLen:
|
||||
var prev: ptr BucketHead = nil
|
||||
var cur = reg.buckets[i]
|
||||
while not cur.isNil:
|
||||
let nxt = cur.next
|
||||
if cur.ctx == ctx:
|
||||
if prev.isNil:
|
||||
reg.buckets[i] = nxt
|
||||
else:
|
||||
prev.next = nxt
|
||||
disposeBucket(cur)
|
||||
dec reg.entryCount
|
||||
else:
|
||||
prev = cur
|
||||
cur = nxt
|
||||
|
||||
type SubsFreedCb* = proc(name: cstring, count: int32) {.gcsafe, raises: [].}
|
||||
## Invoked once per disposed bucket by `subsRegistryFreeForClass` with the
|
||||
## bucket's event name and live subscription count, so the caller can
|
||||
## decrement the matching per-event subs-count atomic.
|
||||
|
||||
proc subsRegistryFreeForClass*(
|
||||
reg: ptr SubsRegistry, classCtx: uint16, onFreed: SubsFreedCb
|
||||
) {.gcsafe, raises: [].} =
|
||||
## Drops every bucket whose ctx low16 == `classCtx` — the lib ctx itself
|
||||
## (instanceCtx 0) plus every sub-instance sharing its classCtx. For each
|
||||
## disposed bucket with live subs, invokes `onFreed(eventName, subsCount)`
|
||||
## so the caller can decrement the shared per-event subs-count. Called from
|
||||
## `_shutdown(libCtx)` after both threads are joined, so no concurrent
|
||||
## delivery can race this teardown.
|
||||
{.cast(gcsafe).}:
|
||||
withLock reg.lock:
|
||||
for i in 0 ..< reg.bucketsLen:
|
||||
var prev: ptr BucketHead = nil
|
||||
var cur = reg.buckets[i]
|
||||
while not cur.isNil:
|
||||
let nxt = cur.next
|
||||
if (cur.ctx and 0x0000FFFF'u32) == uint32(classCtx):
|
||||
if not onFreed.isNil and cur.subsCount > 0:
|
||||
onFreed(cur.eventName, int32(cur.subsCount))
|
||||
if prev.isNil:
|
||||
reg.buckets[i] = nxt
|
||||
else:
|
||||
prev.next = nxt
|
||||
disposeBucket(cur)
|
||||
dec reg.entryCount
|
||||
else:
|
||||
prev = cur
|
||||
cur = nxt
|
||||
|
||||
{.pop.}
|
||||
@@ -0,0 +1,85 @@
|
||||
## api_cbor_tuple
|
||||
## ---------------
|
||||
## Map-shaped CBOR encoders/decoders for named Nim tuple aliases used
|
||||
## across the FFI boundary.
|
||||
##
|
||||
## ## Why this exists
|
||||
##
|
||||
## `cbor_serialization` 0.3.0 emits Nim tuples as positional CBOR arrays
|
||||
## (`writer.nim:423` — `proc write*[T: tuple]`) and decodes them
|
||||
## symmetrically (`reader_impl.nim:144` — `proc read*[T: tuple]`).
|
||||
## Wrapper-side codegen (Cpp / Py / Rust / Go) emits a tuple alias as
|
||||
## a struct with NAMED fields and expects a CBOR map keyed by those
|
||||
## names. Without alignment, a wrapper round-trip of `seq[TupleRow]`
|
||||
## fails with "invalid type: sequence, expected map".
|
||||
##
|
||||
## This module provides a macro `bindCborTupleMap(T)` that emits a
|
||||
## per-tuple `write` / `read` overload bound to the `BrokerCbor` flavor.
|
||||
## The overloads encode/consume a CBOR map keyed by the Nim field
|
||||
## names. Resolver code calls the macro for every named tuple alias
|
||||
## that's auto-registered as part of the FFI surface.
|
||||
##
|
||||
## ## Limitation
|
||||
##
|
||||
## Only NAMED tuple aliases are supported (e.g.
|
||||
## `type TupleRow = tuple[key: string, payload: string]`). Unnamed
|
||||
## positional tuples (`tuple[int32, string]`) keep the library default
|
||||
## (positional CBOR array) — wrappers receive synthesised field names
|
||||
## (`first`, `second`, ...) on the struct side which would not match
|
||||
## a positional CBOR shape; the tuple-as-struct codegen rejects > 9
|
||||
## positional elements anyway, so no wrapper currently emits structs
|
||||
## from unnamed tuples.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/macros
|
||||
import cbor_serialization
|
||||
import cbor_serialization/[reader_impl, writer]
|
||||
|
||||
import ./api_cbor_codec
|
||||
|
||||
export api_cbor_codec
|
||||
|
||||
macro bindCborTupleMap*(T: typed): untyped =
|
||||
## Emit `write` and `read` overloads for tuple type `T` that use the
|
||||
## CBOR map shape (field name → value) instead of the default
|
||||
## positional CBOR array. The overloads bind to `BrokerCbor.Writer` /
|
||||
## `BrokerCbor.Reader` so they take precedence over the generic
|
||||
## `write[T: tuple]` / `read[T: tuple]` from cbor_serialization.
|
||||
let typeIdent = T
|
||||
let writerSym = bindSym("CborWriter")
|
||||
let readerSym = bindSym("CborReader")
|
||||
let valueIdent = ident("value")
|
||||
let writerIdent = ident("w")
|
||||
let readerIdent = ident("r")
|
||||
let keyIdent = ident("key")
|
||||
|
||||
# Field names are extracted at proc body-instantiation time via
|
||||
# `fieldPairs`, so the macro only needs to emit the proc skeletons —
|
||||
# the proc body iterates the type's fields generically.
|
||||
result = quote:
|
||||
proc write*(
|
||||
`writerIdent`: var `writerSym`, `valueIdent`: `typeIdent`
|
||||
) {.raises: [IOError].} =
|
||||
var fieldsCount = 0
|
||||
for _, _ in fieldPairs(`valueIdent`):
|
||||
inc fieldsCount
|
||||
`writerIdent`.beginObject(fieldsCount)
|
||||
for fieldName, fieldValue in fieldPairs(`valueIdent`):
|
||||
`writerIdent`.writeField(fieldName, fieldValue)
|
||||
`writerIdent`.endObject(stopCode = false)
|
||||
|
||||
proc read*(
|
||||
`readerIdent`: var `readerSym`, `valueIdent`: var `typeIdent`
|
||||
) {.raises: [SerializationError, IOError].} =
|
||||
mixin readValue
|
||||
`readerIdent`.parseObject(`keyIdent`):
|
||||
var matched = false
|
||||
for fieldName, fieldValue in fieldPairs(`valueIdent`):
|
||||
if not matched and fieldName == `keyIdent`:
|
||||
`readerIdent`.readValue(fieldValue)
|
||||
matched = true
|
||||
if not matched:
|
||||
`readerIdent`.skipSingleValue()
|
||||
|
||||
{.pop.}
|
||||
@@ -0,0 +1,244 @@
|
||||
## CDDL emission for the CBOR FFI surface.
|
||||
##
|
||||
## Walks the per-library `CborRequestEntry` / `CborEventEntry` accumulators
|
||||
## and the shared `gApiTypeRegistry` to produce a `<lib>.cddl` schema file
|
||||
## next to the generated C/C++/Python wrappers. The schema is consumable by
|
||||
## external CDDL tooling (`cddl validate`, `cuddle`, …) and is also embedded
|
||||
## verbatim in the runtime discovery descriptor returned from
|
||||
## `<lib>_getSchema`.
|
||||
##
|
||||
## CDDL mapping summary:
|
||||
## bool -> bool
|
||||
## int / intN -> int
|
||||
## uint / uintN / byte -> uint
|
||||
## float / floatN -> float
|
||||
## string / cstring -> tstr
|
||||
## seq[T] -> [* T-cddl]
|
||||
## array[N, T] -> [N*N T-cddl]
|
||||
## Option[T] -> T-cddl / null
|
||||
## <registered enum> -> uint
|
||||
## <registered alias/distinct -> resolved underlying type
|
||||
## <registered object> -> rule reference (PascalCase name)
|
||||
##
|
||||
## The args type for a request is emitted inline as a synthetic
|
||||
## `<UpperCamel>Args` rule. The response envelope shape is a single
|
||||
## reusable rule `BrokerResultEnvelope` parameterised by inlining the
|
||||
## payload type per request — CDDL has no generics, so we expand it.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[macros, os, strutils]
|
||||
import ./api_schema, ./api_common
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Type-name helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc upperCamel*(s: string): string {.compileTime.} =
|
||||
## "device_updated" -> "DeviceUpdated"; "GetStatus" stays "GetStatus".
|
||||
result = ""
|
||||
var capNext = true
|
||||
for ch in s:
|
||||
if ch == '_' or ch == '-':
|
||||
capNext = true
|
||||
else:
|
||||
if capNext:
|
||||
result.add(ch.toUpperAscii())
|
||||
capNext = false
|
||||
else:
|
||||
result.add(ch)
|
||||
|
||||
proc stripGenericPrefix(s: string, prefix: string): string {.compileTime.} =
|
||||
## Returns the inner of `prefix[...]`, e.g. `seq[int32]` -> `int32`.
|
||||
## Caller has already verified the prefix.
|
||||
let inner = s[prefix.len + 1 .. ^2]
|
||||
inner.strip()
|
||||
|
||||
proc parseArrayParts(s: string): tuple[size: string, elem: string] {.compileTime.} =
|
||||
## Parse `array[N, T]` into (N, T). Returns ("", "") on malformed input.
|
||||
if not s.toLowerAscii().startsWith("array["):
|
||||
return ("", "")
|
||||
let inner = s[6 .. ^2]
|
||||
let comma = inner.find(',')
|
||||
if comma < 0:
|
||||
return ("", "")
|
||||
(inner[0 ..< comma].strip(), inner[comma + 1 .. ^1].strip())
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Nim type -> CDDL fragment
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc nimTypeToCddl*(nimType: string): string {.compileTime.} =
|
||||
## Maps a Nim type spelling to a CDDL fragment. Falls back to a rule
|
||||
## reference (the type name itself) for registered objects/enums; the
|
||||
## caller is responsible for emitting that rule elsewhere in the file.
|
||||
let t = nimType.strip()
|
||||
let lower = t.toLowerAscii()
|
||||
|
||||
case lower
|
||||
of "bool":
|
||||
return "bool"
|
||||
of "string", "cstring":
|
||||
return "tstr"
|
||||
of "char":
|
||||
return "uint .size 1"
|
||||
of "int", "int8", "int16", "int32", "int64":
|
||||
return "int"
|
||||
of "uint", "uint8", "uint16", "uint32", "uint64", "byte":
|
||||
return "uint"
|
||||
of "float", "float32", "float64":
|
||||
return "float"
|
||||
else:
|
||||
discard
|
||||
|
||||
if lower.startsWith("seq[") and lower.endsWith("]"):
|
||||
return "[* " & nimTypeToCddl(stripGenericPrefix(t, "seq")) & "]"
|
||||
|
||||
if lower.startsWith("option[") and lower.endsWith("]"):
|
||||
return nimTypeToCddl(stripGenericPrefix(t, "option")) & " / null"
|
||||
|
||||
if lower.startsWith("array["):
|
||||
let (sz, elem) = parseArrayParts(t)
|
||||
if sz.len > 0 and elem.len > 0:
|
||||
return "[" & sz & "*" & sz & " " & nimTypeToCddl(elem) & "]"
|
||||
|
||||
if isAliasOrDistinctRegistered(t):
|
||||
return nimTypeToCddl(resolveUnderlyingType(t))
|
||||
|
||||
if isEnumRegistered(t):
|
||||
return "uint"
|
||||
|
||||
if isTypeRegistered(t):
|
||||
return t
|
||||
|
||||
# Unknown type — emit verbatim and let the CDDL consumer surface the
|
||||
# missing rule. This preserves debuggability without aborting codegen
|
||||
# for legitimate generic types we haven't taught the mapper about yet.
|
||||
t
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Type-rule emission
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc emitObjectRule(entry: ApiTypeEntry): string {.compileTime.} =
|
||||
result = entry.name & " = {\n"
|
||||
for f in entry.fields:
|
||||
result.add(" " & f.name & ": " & nimTypeToCddl(f.nimType) & ",\n")
|
||||
result.add("}\n")
|
||||
|
||||
proc emitEnumRule(entry: ApiTypeEntry): string {.compileTime.} =
|
||||
result = "; enum " & entry.name & ":\n"
|
||||
for v in entry.enumValues:
|
||||
result.add("; " & v.name & " = " & $v.ordinal & "\n")
|
||||
result.add(entry.name & " = uint\n")
|
||||
|
||||
proc emitAliasRule(entry: ApiTypeEntry): string {.compileTime.} =
|
||||
let kind =
|
||||
case entry.kind
|
||||
of atkAlias: "alias"
|
||||
of atkDistinct: "distinct"
|
||||
else: "alias"
|
||||
result = "; " & kind & " of " & entry.underlyingType & "\n"
|
||||
result.add(entry.name & " = " & nimTypeToCddl(entry.underlyingType) & "\n")
|
||||
|
||||
proc emitTypeRule(entry: ApiTypeEntry): string {.compileTime.} =
|
||||
case entry.kind
|
||||
of atkObject:
|
||||
emitObjectRule(entry)
|
||||
of atkEnum:
|
||||
emitEnumRule(entry)
|
||||
of atkAlias, atkDistinct:
|
||||
emitAliasRule(entry)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Args / envelope rule emission
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc emitArgsRule(
|
||||
ruleName: string, argFields: seq[(string, string)]
|
||||
): string {.compileTime.} =
|
||||
result = ruleName & " = {\n"
|
||||
for (fname, ftype) in argFields:
|
||||
result.add(" " & fname & ": " & nimTypeToCddl(ftype) & ",\n")
|
||||
result.add("}\n")
|
||||
|
||||
proc emitEnvelopeRule(ruleName: string, payloadCddl: string): string {.compileTime.} =
|
||||
## CBOR encoding produced by `omitOptionalFields = true`: a map with at
|
||||
## most one of `ok` / `err`, mutually exclusive.
|
||||
result = ruleName & " = { ? ok: " & payloadCddl & ", ? err: tstr }\n"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# File emission
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc cddlPath(outDir, libName: string): string {.compileTime.} =
|
||||
if outDir.len > 0:
|
||||
outDir & "/" & libName & ".cddl"
|
||||
else:
|
||||
libName & ".cddl"
|
||||
|
||||
proc generateCborCddl*(
|
||||
libName: string,
|
||||
requestEntries: seq[CborRequestEntry],
|
||||
eventEntries: seq[CborEventEntry],
|
||||
typeRegistry: seq[ApiTypeEntry],
|
||||
): string {.compileTime.} =
|
||||
## Pure-string assembly so the same blob can be both written to disk and
|
||||
## embedded as a string literal in the generated runtime descriptor.
|
||||
result = "; Generated by nim-brokers CBOR FFI codegen for '" & libName & "'.\n"
|
||||
result.add("; Do not edit — regenerate by recompiling the library.\n\n")
|
||||
|
||||
result.add("; ----- Shared types ----------------------------------------\n")
|
||||
for entry in typeRegistry:
|
||||
if entry.name.endsWith("CborArgs"):
|
||||
# Synthetic args structs emitted per-request below.
|
||||
continue
|
||||
result.add(emitTypeRule(entry))
|
||||
result.add("\n")
|
||||
|
||||
if requestEntries.len > 0:
|
||||
result.add("; ----- Requests --------------------------------------------\n")
|
||||
for r in requestEntries:
|
||||
let argsRule = upperCamel(r.apiName) & "Args"
|
||||
let respEnvRule = upperCamel(r.apiName) & "Response"
|
||||
let payloadCddl =
|
||||
if r.responseTypeName.len > 0:
|
||||
nimTypeToCddl(r.responseTypeName)
|
||||
else:
|
||||
"{}"
|
||||
|
||||
result.add("; apiName: \"" & r.apiName & "\"\n")
|
||||
if r.argFields.len > 0:
|
||||
result.add(emitArgsRule(argsRule, r.argFields))
|
||||
else:
|
||||
result.add(argsRule & " = {}\n")
|
||||
result.add(emitEnvelopeRule(respEnvRule, payloadCddl))
|
||||
result.add("\n")
|
||||
|
||||
if eventEntries.len > 0:
|
||||
result.add("; ----- Events ----------------------------------------------\n")
|
||||
for e in eventEntries:
|
||||
result.add("; eventName: \"" & e.apiName & "\"\n")
|
||||
result.add(
|
||||
upperCamel(e.apiName) & "Event = " & nimTypeToCddl(e.typeName) & "\n\n"
|
||||
)
|
||||
|
||||
proc generateCborCddlFile*(
|
||||
outDir: string,
|
||||
libName: string,
|
||||
requestEntries: seq[CborRequestEntry],
|
||||
eventEntries: seq[CborEventEntry],
|
||||
typeRegistry: seq[ApiTypeEntry],
|
||||
): string {.compileTime, raises: [].} =
|
||||
## Writes `<libName>.cddl` and returns the file's contents so the caller
|
||||
## can embed the same string in the generated runtime discovery payload.
|
||||
ensureGeneratedOutputDir(outDir)
|
||||
let body = generateCborCddl(libName, requestEntries, eventEntries, typeRegistry)
|
||||
let path = cddlPath(outDir, libName)
|
||||
try:
|
||||
writeFile(path, body)
|
||||
except IOError:
|
||||
error("Failed to write generated CDDL '" & path & "': " & getCurrentExceptionMsg())
|
||||
body
|
||||
|
||||
{.pop.}
|
||||
@@ -0,0 +1,865 @@
|
||||
## CBOR-mode Go wrapper code generation.
|
||||
##
|
||||
## Mirrors `api_codegen_cbor_rust.nim` but emits idiomatic Go with
|
||||
## `(T, error)` returns. Uses `github.com/fxamacker/cbor/v2` for
|
||||
## CBOR encoding/decoding (struct tags map Nim camelCase wire keys to
|
||||
## Go-style PascalCase fields).
|
||||
##
|
||||
## Native and CBOR generations write to separate `<outDir>` trees
|
||||
## (`nimlib/build/` vs `nimlib/build_cbor/`), so each generated module
|
||||
## directory contains exactly one wrapper. The filename is the same in
|
||||
## both modes — `<libname>.go` and `<libname>_callbacks.c` — matching
|
||||
## the C/C++/Rust convention where consumers pick build vs build_cbor
|
||||
## via their build system, not via build-tag selection inside the
|
||||
## module.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[macros, strutils, tables]
|
||||
import ./api_common, ./api_schema
|
||||
import ./helper/broker_utils # reduced-A: per-interface partitioning
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Nim → Go type mapping (registry-aware, used in CBOR mode)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const goPrimMap = {
|
||||
"bool": "bool",
|
||||
"string": "string",
|
||||
"char": "string",
|
||||
"int": "int32",
|
||||
"int8": "int8",
|
||||
"int16": "int16",
|
||||
"int32": "int32",
|
||||
"int64": "int64",
|
||||
"uint": "uint32",
|
||||
"uint8": "uint8",
|
||||
"uint16": "uint16",
|
||||
"uint32": "uint32",
|
||||
"uint64": "uint64",
|
||||
"byte": "byte",
|
||||
"float": "float64",
|
||||
"float32": "float32",
|
||||
"float64": "float64",
|
||||
}.toTable
|
||||
|
||||
proc isGoPrimitive(nimType: string): bool {.compileTime.} =
|
||||
nimType.strip() in goPrimMap
|
||||
|
||||
proc primGoHint(nimType: string): string {.compileTime.} =
|
||||
goPrimMap.getOrDefault(nimType.strip(), "")
|
||||
|
||||
proc unwrapBracket(s, head: string): string {.compileTime.} =
|
||||
let t = s.strip()
|
||||
t[head.len + 1 .. ^2].strip()
|
||||
|
||||
proc parseArrayInner(s: string): string {.compileTime.} =
|
||||
let inner = s.strip()[6 ..^ 2]
|
||||
let comma = inner.find(',')
|
||||
if comma < 0:
|
||||
return ""
|
||||
inner[comma + 1 .. ^1].strip()
|
||||
|
||||
proc nimTypeToGoCborHint*(nimType: string): string {.compileTime.} =
|
||||
## Recursive Nim → Go type for CBOR mode. Returns "" when unmappable.
|
||||
let t = nimType.strip()
|
||||
let lower = t.toLowerAscii()
|
||||
if isGoPrimitive(t):
|
||||
return primGoHint(t)
|
||||
if lower.startsWith("seq[") and lower.endsWith("]"):
|
||||
let inner = nimTypeToGoCborHint(unwrapBracket(t, "seq"))
|
||||
return
|
||||
if inner.len > 0:
|
||||
# Compact CBOR for seq[byte] uses a Go []byte (cbor lib auto-detects).
|
||||
"[]" & inner
|
||||
else:
|
||||
""
|
||||
if lower.startsWith("array["):
|
||||
let elem = parseArrayInner(t)
|
||||
let inner = nimTypeToGoCborHint(elem)
|
||||
return
|
||||
if inner.len > 0:
|
||||
"[]" & inner
|
||||
else:
|
||||
""
|
||||
if lower.startsWith("option[") and lower.endsWith("]"):
|
||||
let inner = nimTypeToGoCborHint(unwrapBracket(t, "option"))
|
||||
return
|
||||
if inner.len > 0:
|
||||
"*" & inner
|
||||
else:
|
||||
""
|
||||
if isTypeRegistered(t):
|
||||
let entry = lookupTypeEntry(t)
|
||||
case entry.kind
|
||||
of atkObject, atkEnum:
|
||||
return t
|
||||
of atkAlias, atkDistinct:
|
||||
# Recurse via outer mapper for distinct/alias-over-compound (e.g.
|
||||
# `distinct seq[byte]` → `[]byte` rather than `""`).
|
||||
return nimTypeToGoCborHint(resolveUnderlyingType(t))
|
||||
""
|
||||
|
||||
proc isGoCborMappable*(nimType: string): bool {.compileTime.} =
|
||||
nimTypeToGoCborHint(nimType).len > 0
|
||||
|
||||
proc goExportedField*(name: string): string {.compileTime.} =
|
||||
if name.len > 0 and name[0] >= 'a' and name[0] <= 'z':
|
||||
chr(ord(name[0]) - 32) & name[1 ..^ 1]
|
||||
else:
|
||||
name
|
||||
|
||||
const goReservedWords = [
|
||||
"break", "case", "chan", "const", "continue", "default", "defer", "else",
|
||||
"fallthrough", "for", "func", "go", "goto", "if", "import", "interface", "map",
|
||||
"package", "range", "return", "select", "struct", "switch", "type", "var",
|
||||
]
|
||||
|
||||
proc goSafeParam*(name: string): string {.compileTime.} =
|
||||
## Returns a Go-legal local identifier — appends `Arg` suffix when the
|
||||
## Nim parameter name collides with a Go reserved keyword (e.g.
|
||||
## `range` → `rangeArg`, `type` → `typeArg`). The CBOR wire field is
|
||||
## emitted from the original name, so wire compatibility is preserved.
|
||||
if name in goReservedWords:
|
||||
name & "Arg"
|
||||
else:
|
||||
name
|
||||
|
||||
proc snakeToPascal(name: string): string {.compileTime.} =
|
||||
## Converts a snake_case identifier (CBOR apiName / event name) to
|
||||
## PascalCase for Go method exports.
|
||||
result = ""
|
||||
var capitalize = true
|
||||
for ch in name:
|
||||
if ch == '_' or ch == '-':
|
||||
capitalize = true
|
||||
elif capitalize:
|
||||
result.add(
|
||||
if ch >= 'a' and ch <= 'z':
|
||||
chr(ord(ch) - 32)
|
||||
else:
|
||||
ch
|
||||
)
|
||||
capitalize = false
|
||||
else:
|
||||
result.add(ch)
|
||||
|
||||
proc goCborClassName(libName: string): string {.compileTime.} =
|
||||
result = ""
|
||||
var capitalize = true
|
||||
for ch in libName:
|
||||
if ch == '_' or ch == '-':
|
||||
capitalize = true
|
||||
elif capitalize:
|
||||
result.add(chr(ord(ch) - 32 * ord(ch in {'a' .. 'z'})))
|
||||
capitalize = false
|
||||
else:
|
||||
result.add(ch)
|
||||
|
||||
proc goCborPackageName(libName: string): string {.compileTime.} =
|
||||
result = ""
|
||||
for ch in libName:
|
||||
if ch != '_' and ch != '-':
|
||||
result.add(
|
||||
if ch >= 'A' and ch <= 'Z':
|
||||
chr(ord(ch) + 32)
|
||||
else:
|
||||
ch
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# File emission
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
{.pop.}
|
||||
|
||||
proc goSubStructName(iface: string): string {.compileTime.} =
|
||||
## Wrapper struct name for a sub-interface: strip a leading `I` before an
|
||||
## uppercase letter (IWidget -> Widget), else use the name as-is.
|
||||
if iface.len > 1 and iface[0] == 'I' and iface[1] in {'A' .. 'Z'}:
|
||||
iface[1 ..^ 1]
|
||||
else:
|
||||
iface
|
||||
|
||||
proc generateCborGoFile*(
|
||||
outDir: string,
|
||||
libName: string,
|
||||
requestEntries: seq[CborRequestEntry],
|
||||
eventEntries: seq[CborEventEntry],
|
||||
mainClass: string = "",
|
||||
) {.compileTime, raises: [].} =
|
||||
## Emits `<outDir>/<libName>_go/{<libName>.go, <libName>_callbacks.c}`.
|
||||
## Same filenames as the native generator — only one wrapper exists per
|
||||
## build dir, so no build tags / no `_cbor` suffix.
|
||||
ensureGeneratedOutputDir(outDir)
|
||||
|
||||
# reduced-A: per-interface partition. Sub-interface names derived from the
|
||||
# entries via interfaceOwningRequestType (NOT apiInterfaces() — the VM aliases
|
||||
# a by-value seq return to an empty copy).
|
||||
proc ownsReqMain(e: CborRequestEntry): bool {.compileTime.} =
|
||||
if mainClass.len == 0:
|
||||
return true
|
||||
let o = interfaceOwningRequestType(e.responseTypeName)
|
||||
o.len == 0 or o == mainClass
|
||||
|
||||
proc ownsEvtMain(ev: CborEventEntry): bool {.compileTime.} =
|
||||
if mainClass.len == 0:
|
||||
return true
|
||||
let o = interfaceOwningEventType(ev.typeName)
|
||||
o.len == 0 or o == mainClass
|
||||
|
||||
var subInterfaceNames: seq[string] = @[]
|
||||
if mainClass.len > 0:
|
||||
for e in requestEntries:
|
||||
let o = interfaceOwningRequestType(e.responseTypeName)
|
||||
if o.len > 0 and o != mainClass and o notin subInterfaceNames:
|
||||
subInterfaceNames.add(o)
|
||||
let modDir =
|
||||
if outDir.len > 0:
|
||||
outDir & "/" & libName & "_go"
|
||||
else:
|
||||
libName & "_go"
|
||||
ensureGeneratedOutputDir(modDir)
|
||||
|
||||
let pkgName = goCborPackageName(libName)
|
||||
let className = goCborClassName(libName)
|
||||
let p = libName & "_"
|
||||
|
||||
# ---------------------- go.mod ----------------------
|
||||
# Always emit go.mod with the cbor dependency. (If a native-only build
|
||||
# ran first and wrote go.mod without it, overwrite.)
|
||||
var goMod = "// Generated by nim-brokers Go FFI codegen — do not edit.\n"
|
||||
goMod.add("module " & libName & "\n\n")
|
||||
goMod.add("go 1.21\n\n")
|
||||
goMod.add("require github.com/fxamacker/cbor/v2 v2.7.0\n")
|
||||
try:
|
||||
writeFile(modDir & "/go.mod", goMod)
|
||||
except IOError:
|
||||
error("Failed to write go.mod: " & getCurrentExceptionMsg())
|
||||
|
||||
# ---------------------- <libName>.go ----------------------
|
||||
var g = "// Generated by nim-brokers CBOR FFI Go codegen — do not edit.\n"
|
||||
g.add("//\n")
|
||||
g.add(
|
||||
"// CBOR-mode Go wrapper around the fixed 11-fn ABI declared by the `" & libName &
|
||||
"` shared library.\n"
|
||||
)
|
||||
g.add("//\n")
|
||||
g.add("// Public surface mirrors the native build:\n")
|
||||
g.add("// " & libName & ".Version()\n")
|
||||
g.add("// " & libName & ".New() + lib.CreateContext()\n")
|
||||
g.add("// <Request>(args) -> (T, error)\n")
|
||||
g.add("// On<Event>(callback) -> uint64 / Off<Event>(handle uint64)\n")
|
||||
g.add("//\n")
|
||||
for e in requestEntries:
|
||||
var sigParams = ""
|
||||
for i, (n, t) in e.argFields.pairs:
|
||||
if i > 0:
|
||||
sigParams.add(", ")
|
||||
let h = nimTypeToGoCborHint(t)
|
||||
sigParams.add(goExportedField(n) & " " & (if h.len > 0: h else: "any"))
|
||||
g.add(
|
||||
"// " & snakeToPascal(e.apiName) & "(" & sigParams & ") (" & e.responseTypeName &
|
||||
", error)\n"
|
||||
)
|
||||
for ev in eventEntries:
|
||||
g.add("// On" & snakeToPascal(ev.apiName) & "(callback) uint64\n")
|
||||
g.add("// Off" & snakeToPascal(ev.apiName) & "(handle uint64)\n")
|
||||
g.add("\n")
|
||||
|
||||
g.add("package " & pkgName & "\n\n")
|
||||
|
||||
# cgo prelude
|
||||
g.add("/*\n")
|
||||
g.add("#cgo CFLAGS: -I${SRCDIR}/..\n")
|
||||
g.add("#cgo LDFLAGS: -L${SRCDIR}/.. -l" & libName & "\n")
|
||||
g.add("#cgo darwin LDFLAGS: -Wl,-rpath,${SRCDIR}/..\n")
|
||||
g.add("#cgo linux LDFLAGS: -Wl,-rpath,${SRCDIR}/..\n")
|
||||
g.add("#include <stdlib.h>\n")
|
||||
g.add("#include <string.h>\n")
|
||||
g.add("#include <stdint.h>\n")
|
||||
g.add("#include \"" & libName & ".h\"\n")
|
||||
g.add(
|
||||
"uint64_t go_cbor_subscribe(uint32_t ctx, const char* name, void* user_data);\n"
|
||||
)
|
||||
g.add("*/\n")
|
||||
g.add("import \"C\"\n\n")
|
||||
|
||||
g.add("import (\n")
|
||||
g.add("\t\"errors\"\n")
|
||||
g.add("\t\"runtime\"\n")
|
||||
g.add("\t\"runtime/cgo\"\n")
|
||||
g.add("\t\"sync\"\n")
|
||||
g.add("\t\"unsafe\"\n")
|
||||
g.add("\t\"github.com/fxamacker/cbor/v2\"\n")
|
||||
g.add(")\n\n")
|
||||
g.add("var _ = errors.New\n")
|
||||
g.add("var _ = runtime.SetFinalizer\n")
|
||||
g.add("var _ cgo.Handle\n")
|
||||
g.add("var _ sync.Mutex\n")
|
||||
g.add("var _ unsafe.Pointer\n")
|
||||
g.add("var _ = cbor.Marshal\n\n")
|
||||
|
||||
# Per-context cgo.Handle registry — same UAF-safe pattern as native:
|
||||
# the closure stays alive across Off<Event> until Close() runs.
|
||||
g.add("var cborHandleReg = struct {\n")
|
||||
g.add("\tmu sync.Mutex\n")
|
||||
g.add("\tperCtx map[uint32][]cgo.Handle\n")
|
||||
g.add("}{perCtx: make(map[uint32][]cgo.Handle)}\n\n")
|
||||
g.add("func registerCborHandle(ctx C.uint32_t, h cgo.Handle) {\n")
|
||||
g.add("\tcborHandleReg.mu.Lock()\n")
|
||||
g.add(
|
||||
"\tcborHandleReg.perCtx[uint32(ctx)] = append(cborHandleReg.perCtx[uint32(ctx)], h)\n"
|
||||
)
|
||||
g.add("\tcborHandleReg.mu.Unlock()\n")
|
||||
g.add("}\n\n")
|
||||
g.add("func dropCborHandlesForCtx(ctx C.uint32_t) {\n")
|
||||
g.add("\tcborHandleReg.mu.Lock()\n")
|
||||
g.add("\thandles := cborHandleReg.perCtx[uint32(ctx)]\n")
|
||||
g.add("\tdelete(cborHandleReg.perCtx, uint32(ctx))\n")
|
||||
g.add("\tcborHandleReg.mu.Unlock()\n")
|
||||
g.add("\tfor _, h := range handles { h.Delete() }\n")
|
||||
g.add("}\n\n")
|
||||
|
||||
# ---- Generated payload types ------------------------------------------
|
||||
var enumNames: seq[string] = @[]
|
||||
for entry in gApiTypeRegistry:
|
||||
if entry.kind == atkEnum:
|
||||
enumNames.add(entry.name)
|
||||
var aliasNames: seq[string] = @[]
|
||||
for entry in gApiTypeRegistry:
|
||||
if entry.kind in {atkDistinct, atkAlias}:
|
||||
aliasNames.add(entry.name)
|
||||
var objectNames: seq[string] = @[]
|
||||
for entry in gApiTypeRegistry:
|
||||
if entry.kind == atkObject and not entry.name.endsWith("CborArgs"):
|
||||
objectNames.add(entry.name)
|
||||
|
||||
# A "scalar payload" is a primitive (non-object) broker type — `type X =
|
||||
# int32` — registered as a distinct alias of its underlying primitive.
|
||||
# Its CBOR wire value is a bare scalar; the Go surface uses the
|
||||
# `type X = <prim>` alias directly. Such a type has no object fields, so
|
||||
# the event handler delivers the bare value rather than unpacked fields.
|
||||
proc isScalarPayload(name: string): bool {.compileTime.} =
|
||||
name.len > 0 and isTypeRegistered(name) and
|
||||
lookupTypeEntry(name).kind in {atkAlias, atkDistinct} and
|
||||
primGoHint(resolveUnderlyingType(name)).len > 0
|
||||
|
||||
if enumNames.len > 0 or aliasNames.len > 0 or objectNames.len > 0:
|
||||
g.add("// -------- Generated payload types --------\n\n")
|
||||
|
||||
for name in enumNames:
|
||||
let entry = lookupTypeEntry(name)
|
||||
g.add("type " & name & " int32\n\n")
|
||||
g.add("const (\n")
|
||||
if entry.enumValues.len == 0:
|
||||
g.add("\t" & name & "_Unknown " & name & " = 0\n")
|
||||
else:
|
||||
for v in entry.enumValues:
|
||||
g.add("\t" & name & "_" & v.name & " " & name & " = " & $v.ordinal & "\n")
|
||||
g.add(")\n\n")
|
||||
|
||||
for name in aliasNames:
|
||||
let underlying = resolveUnderlyingType(name)
|
||||
let goU = primGoHint(underlying)
|
||||
if goU.len == 0:
|
||||
g.add(
|
||||
"// TODO: alias '" & name & "' resolves to '" & underlying &
|
||||
"' (no Go primitive)\n\n"
|
||||
)
|
||||
continue
|
||||
g.add("type " & name & " = " & goU & "\n\n")
|
||||
|
||||
for name in objectNames:
|
||||
let entry = lookupTypeEntry(name)
|
||||
g.add("type " & name & " struct {\n")
|
||||
var anyField = false
|
||||
for f in entry.fields:
|
||||
let hint = nimTypeToGoCborHint(f.nimType)
|
||||
if hint.len == 0:
|
||||
g.add("\t// TODO: Nim type '" & f.nimType & "' not yet mappable\n")
|
||||
continue
|
||||
let fx = goExportedField(f.name)
|
||||
g.add("\t" & fx & " " & hint & " `cbor:\"" & f.name & "\"`\n")
|
||||
anyField = true
|
||||
if not anyField:
|
||||
g.add("\t_ struct{}\n")
|
||||
g.add("}\n\n")
|
||||
|
||||
# ---- Lib struct + event handler type -----------------------------------
|
||||
g.add("// -------- Event dispatch --------\n\n")
|
||||
g.add("// cborEventHandler is what we anchor on the Go side via cgo.NewHandle.\n")
|
||||
g.add("// Each subscription's user_data is the corresponding cgo.Handle, so\n")
|
||||
g.add("// the trampoline retrieves and invokes exactly that one closure per\n")
|
||||
g.add("// event emit — no global map, no fan-out, no cross-context leakage.\n")
|
||||
g.add("type cborEventHandler func([]byte)\n\n")
|
||||
|
||||
g.add("// -------- Lib struct --------\n\n")
|
||||
g.add("type " & className & " struct {\n")
|
||||
g.add("\tctx C.uint32_t\n")
|
||||
g.add("\tmu sync.Mutex\n")
|
||||
g.add("}\n\n")
|
||||
|
||||
g.add("func Version() string {\n")
|
||||
g.add("\treturn C.GoString(C." & p & "version())\n")
|
||||
g.add("}\n\n")
|
||||
|
||||
g.add("func New() *" & className & " {\n")
|
||||
g.add("\tC." & p & "initialize()\n")
|
||||
g.add("\tl := &" & className & "{}\n")
|
||||
g.add("\truntime.SetFinalizer(l, func(x *" & className & ") { x.Close() })\n")
|
||||
g.add("\treturn l\n")
|
||||
g.add("}\n\n")
|
||||
|
||||
g.add("func (l *" & className & ") CreateContext() error {\n")
|
||||
g.add("\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n")
|
||||
g.add("\tif l.ctx != 0 { return errors.New(\"context already created\") }\n")
|
||||
g.add("\tvar errPtr *C.char\n")
|
||||
g.add("\tctx := C." & p & "createContext(&errPtr)\n")
|
||||
g.add("\tif ctx == 0 {\n")
|
||||
g.add("\t\tmsg := \"createContext returned 0\"\n")
|
||||
g.add(
|
||||
"\t\tif errPtr != nil { msg = C.GoString(errPtr); C." & p &
|
||||
"freeBuffer(unsafe.Pointer(errPtr)) }\n"
|
||||
)
|
||||
g.add("\t\treturn errors.New(msg)\n")
|
||||
g.add("\t}\n")
|
||||
g.add("\tl.ctx = ctx\n")
|
||||
g.add("\treturn nil\n")
|
||||
g.add("}\n\n")
|
||||
|
||||
g.add("func (l *" & className & ") ValidContext() bool { return l.ctx != 0 }\n")
|
||||
g.add("func (l *" & className & ") Ctx() uint32 { return uint32(l.ctx) }\n\n")
|
||||
|
||||
g.add("func (l *" & className & ") Close() {\n")
|
||||
g.add("\tl.mu.Lock()\n\tdefer l.mu.Unlock()\n")
|
||||
g.add("\tif l.ctx != 0 {\n")
|
||||
g.add("\t\tC." & p & "shutdown(l.ctx)\n")
|
||||
g.add("\t\tdropCborHandlesForCtx(l.ctx)\n")
|
||||
g.add("\t\tl.ctx = 0\n")
|
||||
g.add("\t}\n")
|
||||
g.add("}\n\n")
|
||||
|
||||
# ---- Internal call helper ----------------------------------------------
|
||||
g.add("// internalCborCall encodes args via CBOR, copies into a library-\n")
|
||||
g.add("// allocated buffer (the C ABI frees it), dispatches, and returns\n")
|
||||
g.add("// the response bytes (caller-side copy of a library-owned buffer).\n")
|
||||
g.add(
|
||||
"func (l *" & className &
|
||||
") internalCborCall(apiName string, args interface{}) ([]byte, error) {\n"
|
||||
)
|
||||
g.add(
|
||||
"\tif l.ctx == 0 { return nil, errors.New(\"library context is not created\") }\n"
|
||||
)
|
||||
g.add("\tvar inBytes []byte\n")
|
||||
g.add("\tif args != nil {\n")
|
||||
g.add("\t\tvar err error\n")
|
||||
g.add("\t\tinBytes, err = cbor.Marshal(args)\n")
|
||||
g.add("\t\tif err != nil { return nil, err }\n")
|
||||
g.add("\t}\n")
|
||||
g.add("\tcName := C.CString(apiName)\n")
|
||||
g.add("\tdefer C.free(unsafe.Pointer(cName))\n")
|
||||
g.add("\t// The library expects an `<lib>_allocBuffer`-allocated input\n")
|
||||
g.add("\t// buffer that it can free. Copy the Go bytes into one.\n")
|
||||
g.add("\tvar inPtr unsafe.Pointer\n")
|
||||
g.add("\tif len(inBytes) > 0 {\n")
|
||||
g.add("\t\tinPtr = C." & p & "allocBuffer(C.int32_t(len(inBytes)))\n")
|
||||
g.add("\t\tif inPtr == nil { return nil, errors.New(\"allocBuffer failed\") }\n")
|
||||
g.add("\t\tC.memcpy(inPtr, unsafe.Pointer(&inBytes[0]), C.size_t(len(inBytes)))\n")
|
||||
g.add("\t}\n")
|
||||
g.add("\tvar outBuf unsafe.Pointer\n")
|
||||
g.add("\tvar outLen C.int32_t\n")
|
||||
g.add(
|
||||
"\trc := C." & p &
|
||||
"call(l.ctx, cName, inPtr, C.int32_t(len(inBytes)), &outBuf, &outLen)\n"
|
||||
)
|
||||
g.add("\tif rc != 0 {\n")
|
||||
g.add("\t\tif outBuf != nil { C." & p & "freeBuffer(outBuf) }\n")
|
||||
g.add("\t\treturn nil, errors.New(\"call returned non-zero\")\n")
|
||||
g.add("\t}\n")
|
||||
g.add("\tif outBuf == nil { return nil, nil }\n")
|
||||
g.add("\tout := C.GoBytes(outBuf, C.int(outLen))\n")
|
||||
g.add("\tC." & p & "freeBuffer(outBuf)\n")
|
||||
g.add("\treturn out, nil\n")
|
||||
g.add("}\n\n")
|
||||
|
||||
# ---- Per-request methods ------------------------------------------------
|
||||
# Factored emitters reused by the main Lib and each sub-interface struct.
|
||||
proc emitGoReqMethod(e: CborRequestEntry, recv: string): string {.compileTime.} =
|
||||
let methodName = snakeToPascal(e.apiName)
|
||||
let respType = e.responseTypeName
|
||||
var argsStructFields = ""
|
||||
var argsAssign = ""
|
||||
var firstNonZero = false
|
||||
for (n, t) in e.argFields:
|
||||
let h = nimTypeToGoCborHint(t)
|
||||
let hType = if h.len > 0: h else: "any"
|
||||
let exN = goExportedField(n)
|
||||
argsStructFields.add("\t\t" & exN & " " & hType & " `cbor:\"" & n & "\"`\n")
|
||||
argsAssign.add("\t\t" & exN & ": " & goSafeParam(n) & ",\n")
|
||||
firstNonZero = true
|
||||
result.add("func (l *" & recv & ") " & methodName & "(")
|
||||
var firstP = true
|
||||
for (n, t) in e.argFields:
|
||||
let h = nimTypeToGoCborHint(t)
|
||||
let hType = if h.len > 0: h else: "any"
|
||||
if not firstP:
|
||||
result.add(", ")
|
||||
result.add(goSafeParam(n) & " " & hType)
|
||||
firstP = false
|
||||
result.add(") (" & respType & ", error) {\n")
|
||||
result.add("\tvar zeroResp " & respType & "\n")
|
||||
if firstNonZero:
|
||||
result.add("\targs := struct {\n")
|
||||
result.add(argsStructFields)
|
||||
result.add("\t}{\n")
|
||||
result.add(argsAssign)
|
||||
result.add("\t}\n")
|
||||
result.add("\tout, err := l.internalCborCall(\"" & e.apiName & "\", args)\n")
|
||||
else:
|
||||
result.add("\tout, err := l.internalCborCall(\"" & e.apiName & "\", nil)\n")
|
||||
result.add("\tif err != nil { return zeroResp, err }\n")
|
||||
result.add("\tvar env struct {\n")
|
||||
result.add("\t\tOk *" & respType & " `cbor:\"ok\"`\n")
|
||||
result.add("\t\tErr *string `cbor:\"err\"`\n")
|
||||
result.add("\t}\n")
|
||||
result.add(
|
||||
"\tif derr := cbor.Unmarshal(out, &env); derr != nil { return zeroResp, derr }\n"
|
||||
)
|
||||
result.add("\tif env.Err != nil { return zeroResp, errors.New(*env.Err) }\n")
|
||||
result.add("\tif env.Ok != nil { return *env.Ok, nil }\n")
|
||||
result.add("\treturn zeroResp, errors.New(\"empty response envelope\")\n")
|
||||
result.add("}\n\n")
|
||||
|
||||
# reduced-A: a create-instance method returns the typed sub-wrapper. The wire
|
||||
# ok value is a bare uint32 ctx; build &Sub{ctx} from it + a finalizer backstop.
|
||||
proc emitGoInstanceMethod(e: CborRequestEntry, recv: string): string {.compileTime.} =
|
||||
let methodName = snakeToPascal(e.apiName)
|
||||
let sub = goSubStructName(e.returnsInterface)
|
||||
var argsStructFields = ""
|
||||
var argsAssign = ""
|
||||
var firstNonZero = false
|
||||
for (n, t) in e.argFields:
|
||||
let h = nimTypeToGoCborHint(t)
|
||||
let hType = if h.len > 0: h else: "any"
|
||||
let exN = goExportedField(n)
|
||||
argsStructFields.add("\t\t" & exN & " " & hType & " `cbor:\"" & n & "\"`\n")
|
||||
argsAssign.add("\t\t" & exN & ": " & goSafeParam(n) & ",\n")
|
||||
firstNonZero = true
|
||||
result.add("func (l *" & recv & ") " & methodName & "(")
|
||||
var firstP = true
|
||||
for (n, t) in e.argFields:
|
||||
let h = nimTypeToGoCborHint(t)
|
||||
let hType = if h.len > 0: h else: "any"
|
||||
if not firstP:
|
||||
result.add(", ")
|
||||
result.add(goSafeParam(n) & " " & hType)
|
||||
firstP = false
|
||||
result.add(") (*" & sub & ", error) {\n")
|
||||
if firstNonZero:
|
||||
result.add("\targs := struct {\n")
|
||||
result.add(argsStructFields)
|
||||
result.add("\t}{\n")
|
||||
result.add(argsAssign)
|
||||
result.add("\t}\n")
|
||||
result.add("\tout, err := l.internalCborCall(\"" & e.apiName & "\", args)\n")
|
||||
else:
|
||||
result.add("\tout, err := l.internalCborCall(\"" & e.apiName & "\", nil)\n")
|
||||
result.add("\tif err != nil { return nil, err }\n")
|
||||
result.add("\tvar env struct {\n")
|
||||
result.add("\t\tOk *uint32 `cbor:\"ok\"`\n")
|
||||
result.add("\t\tErr *string `cbor:\"err\"`\n")
|
||||
result.add("\t}\n")
|
||||
result.add(
|
||||
"\tif derr := cbor.Unmarshal(out, &env); derr != nil { return nil, derr }\n"
|
||||
)
|
||||
result.add("\tif env.Err != nil { return nil, errors.New(*env.Err) }\n")
|
||||
result.add(
|
||||
"\tif env.Ok == nil { return nil, errors.New(\"empty response envelope\") }\n"
|
||||
)
|
||||
result.add("\tw := &" & sub & "{ctx: C.uint32_t(*env.Ok)}\n")
|
||||
result.add("\truntime.SetFinalizer(w, func(x *" & sub & ") { x.Close() })\n")
|
||||
result.add("\treturn w, nil\n")
|
||||
result.add("}\n\n")
|
||||
|
||||
for e in requestEntries:
|
||||
if not ownsReqMain(e):
|
||||
continue
|
||||
if e.returnsInterface.len > 0:
|
||||
g.add(emitGoInstanceMethod(e, className))
|
||||
else:
|
||||
g.add(emitGoReqMethod(e, className))
|
||||
|
||||
# ---- Single CBOR event trampoline + per-event On/Off ---------------------
|
||||
if eventEntries.len > 0:
|
||||
g.add("// -------- CBOR event trampoline --------\n\n")
|
||||
g.add("//export goCborEventTrampoline\n")
|
||||
g.add(
|
||||
"func goCborEventTrampoline(ctx C.uint32_t, name *C.char, buf unsafe.Pointer, bufLen C.int32_t, ud unsafe.Pointer) {\n"
|
||||
)
|
||||
g.add("\t_ = ctx\n")
|
||||
g.add("\t_ = name\n")
|
||||
g.add("\tif ud == nil { return }\n")
|
||||
g.add("\tvar payload []byte\n")
|
||||
g.add("\tif buf != nil && bufLen > 0 {\n")
|
||||
g.add("\t\tpayload = C.GoBytes(buf, C.int(bufLen))\n")
|
||||
g.add("\t}\n")
|
||||
g.add("\th := cgo.Handle(uintptr(ud))\n")
|
||||
g.add("\tcb, ok := h.Value().(cborEventHandler)\n")
|
||||
g.add("\tif !ok { return }\n")
|
||||
g.add("\tcb(payload)\n")
|
||||
g.add("}\n\n")
|
||||
|
||||
for ev in eventEntries:
|
||||
if not ownsEvtMain(ev):
|
||||
continue
|
||||
let exName = snakeToPascal(ev.apiName)
|
||||
let payloadType = ev.typeName
|
||||
# Walk the payload struct fields to build an unpacked-field handler
|
||||
# signature that matches the native build's `func(f1, f2, ...)` shape.
|
||||
var fieldNames: seq[string] = @[]
|
||||
var fieldGoTypes: seq[string] = @[]
|
||||
var fieldExNames: seq[string] = @[]
|
||||
var fieldsOk = true
|
||||
let scalarEvt = isScalarPayload(payloadType)
|
||||
if scalarEvt:
|
||||
# Scalar payload: the decoded `p` IS the value — one bare arg.
|
||||
fieldNames.add("value")
|
||||
fieldGoTypes.add(primGoHint(resolveUnderlyingType(payloadType)))
|
||||
fieldExNames.add("value")
|
||||
elif isTypeRegistered(payloadType):
|
||||
let entry = lookupTypeEntry(payloadType)
|
||||
for f in entry.fields:
|
||||
let h = nimTypeToGoCborHint(f.nimType)
|
||||
if h.len == 0:
|
||||
fieldsOk = false
|
||||
break
|
||||
fieldNames.add(f.name)
|
||||
fieldGoTypes.add(h)
|
||||
fieldExNames.add(goExportedField(f.name))
|
||||
else:
|
||||
fieldsOk = false
|
||||
|
||||
if not fieldsOk:
|
||||
# Fall back to whole-struct callback if the payload has unmappable
|
||||
# fields (no native equivalent — both modes share the same gap).
|
||||
g.add(
|
||||
"// TODO(go-codegen-cbor): event '" & payloadType &
|
||||
"' has fields not yet mappable\n"
|
||||
)
|
||||
g.add(
|
||||
"func (l *" & className & ") On" & exName & "(cb func(" & payloadType &
|
||||
")) uint64 { _ = cb; return 0 }\n\n"
|
||||
)
|
||||
else:
|
||||
var sig = ""
|
||||
for i in 0 ..< fieldNames.len:
|
||||
if i > 0:
|
||||
sig.add(", ")
|
||||
sig.add(fieldNames[i] & " " & fieldGoTypes[i])
|
||||
g.add(
|
||||
"func (l *" & className & ") On" & exName & "(cb func(" & sig & ")) uint64 {\n"
|
||||
)
|
||||
g.add("\tif l.ctx == 0 { return 0 }\n")
|
||||
g.add("\twrap := cborEventHandler(func(payload []byte) {\n")
|
||||
g.add("\t\tvar p " & payloadType & "\n")
|
||||
g.add("\t\tif derr := cbor.Unmarshal(payload, &p); derr != nil { return }\n")
|
||||
g.add("\t\tcb(")
|
||||
if scalarEvt:
|
||||
# Scalar payload: `p` IS the value — pass it directly.
|
||||
g.add("p")
|
||||
else:
|
||||
for i in 0 ..< fieldNames.len:
|
||||
if i > 0:
|
||||
g.add(", ")
|
||||
g.add("p." & fieldExNames[i])
|
||||
g.add(")\n")
|
||||
g.add("\t})\n")
|
||||
g.add("\th := cgo.NewHandle(wrap)\n")
|
||||
g.add("\tcName := C.CString(\"" & ev.apiName & "\")\n")
|
||||
g.add("\tdefer C.free(unsafe.Pointer(cName))\n")
|
||||
g.add(
|
||||
"\thandle := uint64(C.go_cbor_subscribe(l.ctx, cName, unsafe.Pointer(h)))\n"
|
||||
)
|
||||
g.add("\tif handle == 0 {\n")
|
||||
g.add("\t\th.Delete()\n")
|
||||
g.add("\t\treturn 0\n")
|
||||
g.add("\t}\n")
|
||||
g.add("\tregisterCborHandle(l.ctx, h)\n")
|
||||
g.add("\treturn handle\n")
|
||||
g.add("}\n\n")
|
||||
|
||||
g.add("func (l *" & className & ") Off" & exName & "(handle uint64) {\n")
|
||||
g.add("\tif l.ctx == 0 { return }\n")
|
||||
g.add("\tcName := C.CString(\"" & ev.apiName & "\")\n")
|
||||
g.add("\tdefer C.free(unsafe.Pointer(cName))\n")
|
||||
g.add("\tC." & p & "unsubscribe(l.ctx, cName, C.uint64_t(handle))\n")
|
||||
g.add("}\n\n")
|
||||
|
||||
# reduced-A: sub-interface wrapper structs. Each shares the single C ABI: its
|
||||
# methods call C.<lib>_call(ctx, ...) which the library routes by classCtx to
|
||||
# the same processing thread. Close() (+ finalizer backstop) calls
|
||||
# C.<lib>_releaseInstance, after which the Nim instance is GC-reclaimed.
|
||||
for ifaceName in subInterfaceNames:
|
||||
let sub = goSubStructName(ifaceName)
|
||||
g.add(
|
||||
"// -------- " & sub & " — sub-instance wrapper of " & ifaceName &
|
||||
" --------\n\n"
|
||||
)
|
||||
g.add("type " & sub & " struct {\n")
|
||||
g.add("\tctx C.uint32_t\n")
|
||||
g.add("\tmu sync.Mutex\n")
|
||||
g.add("}\n\n")
|
||||
g.add("func (w *" & sub & ") Ctx() uint32 { return uint32(w.ctx) }\n")
|
||||
g.add("func (w *" & sub & ") Valid() bool { return w.ctx != 0 }\n\n")
|
||||
g.add("func (w *" & sub & ") Close() {\n")
|
||||
g.add("\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n")
|
||||
g.add("\tif w.ctx != 0 {\n")
|
||||
g.add("\t\tC." & p & "releaseInstance(w.ctx)\n")
|
||||
g.add("\t\tw.ctx = 0\n")
|
||||
g.add("\t}\n")
|
||||
g.add("}\n\n")
|
||||
# internalCborCall (same shape as the Lib method, keyed by w.ctx). The
|
||||
# receiver var is named `l` so the shared request-method emitter (which
|
||||
# calls `l.internalCborCall`) works unchanged.
|
||||
g.add(
|
||||
"func (l *" & sub &
|
||||
") internalCborCall(apiName string, args interface{}) ([]byte, error) {\n"
|
||||
)
|
||||
g.add("\tif l.ctx == 0 { return nil, errors.New(\"sub-instance is released\") }\n")
|
||||
g.add("\tvar inBytes []byte\n")
|
||||
g.add("\tif args != nil {\n")
|
||||
g.add("\t\tvar err error\n")
|
||||
g.add("\t\tinBytes, err = cbor.Marshal(args)\n")
|
||||
g.add("\t\tif err != nil { return nil, err }\n")
|
||||
g.add("\t}\n")
|
||||
g.add("\tcName := C.CString(apiName)\n")
|
||||
g.add("\tdefer C.free(unsafe.Pointer(cName))\n")
|
||||
g.add("\tvar inPtr unsafe.Pointer\n")
|
||||
g.add("\tif len(inBytes) > 0 {\n")
|
||||
g.add("\t\tinPtr = C." & p & "allocBuffer(C.int32_t(len(inBytes)))\n")
|
||||
g.add("\t\tif inPtr == nil { return nil, errors.New(\"allocBuffer failed\") }\n")
|
||||
g.add("\t\tC.memcpy(inPtr, unsafe.Pointer(&inBytes[0]), C.size_t(len(inBytes)))\n")
|
||||
g.add("\t}\n")
|
||||
g.add("\tvar outBuf unsafe.Pointer\n")
|
||||
g.add("\tvar outLen C.int32_t\n")
|
||||
g.add(
|
||||
"\trc := C." & p &
|
||||
"call(l.ctx, cName, inPtr, C.int32_t(len(inBytes)), &outBuf, &outLen)\n"
|
||||
)
|
||||
g.add("\tif rc != 0 {\n")
|
||||
g.add("\t\tif outBuf != nil { C." & p & "freeBuffer(outBuf) }\n")
|
||||
g.add("\t\treturn nil, errors.New(\"call returned non-zero\")\n")
|
||||
g.add("\t}\n")
|
||||
g.add("\tif outBuf == nil { return nil, nil }\n")
|
||||
g.add("\tout := C.GoBytes(outBuf, C.int(outLen))\n")
|
||||
g.add("\tC." & p & "freeBuffer(outBuf)\n")
|
||||
g.add("\treturn out, nil\n")
|
||||
g.add("}\n\n")
|
||||
for e in requestEntries:
|
||||
if interfaceOwningRequestType(e.responseTypeName) == ifaceName:
|
||||
g.add(emitGoReqMethod(e, sub))
|
||||
# Sub-interface event methods (subscribe/unsubscribe keyed by l.ctx).
|
||||
for ev in eventEntries:
|
||||
if interfaceOwningEventType(ev.typeName) != ifaceName:
|
||||
continue
|
||||
let exName = snakeToPascal(ev.apiName)
|
||||
let payloadType = ev.typeName
|
||||
var fieldNames: seq[string] = @[]
|
||||
var fieldGoTypes: seq[string] = @[]
|
||||
var fieldExNames: seq[string] = @[]
|
||||
var fieldsOk = true
|
||||
let scalarEvt = isScalarPayload(payloadType)
|
||||
if scalarEvt:
|
||||
fieldNames.add("value")
|
||||
fieldGoTypes.add(primGoHint(resolveUnderlyingType(payloadType)))
|
||||
fieldExNames.add("value")
|
||||
elif isTypeRegistered(payloadType):
|
||||
let entry = lookupTypeEntry(payloadType)
|
||||
for f in entry.fields:
|
||||
let h = nimTypeToGoCborHint(f.nimType)
|
||||
if h.len == 0:
|
||||
fieldsOk = false
|
||||
break
|
||||
fieldNames.add(f.name)
|
||||
fieldGoTypes.add(h)
|
||||
fieldExNames.add(goExportedField(f.name))
|
||||
else:
|
||||
fieldsOk = false
|
||||
if not fieldsOk:
|
||||
g.add(
|
||||
"// TODO(go-codegen-cbor): event '" & payloadType &
|
||||
"' has fields not yet mappable\n"
|
||||
)
|
||||
g.add(
|
||||
"func (l *" & sub & ") On" & exName & "(cb func(" & payloadType &
|
||||
")) uint64 { _ = cb; return 0 }\n\n"
|
||||
)
|
||||
else:
|
||||
var sig = ""
|
||||
for i in 0 ..< fieldNames.len:
|
||||
if i > 0:
|
||||
sig.add(", ")
|
||||
sig.add(fieldNames[i] & " " & fieldGoTypes[i])
|
||||
g.add("func (l *" & sub & ") On" & exName & "(cb func(" & sig & ")) uint64 {\n")
|
||||
g.add("\tif l.ctx == 0 { return 0 }\n")
|
||||
g.add("\twrap := cborEventHandler(func(payload []byte) {\n")
|
||||
g.add("\t\tvar p " & payloadType & "\n")
|
||||
g.add("\t\tif derr := cbor.Unmarshal(payload, &p); derr != nil { return }\n")
|
||||
g.add("\t\tcb(")
|
||||
if scalarEvt:
|
||||
g.add("p")
|
||||
else:
|
||||
for i in 0 ..< fieldNames.len:
|
||||
if i > 0:
|
||||
g.add(", ")
|
||||
g.add("p." & fieldExNames[i])
|
||||
g.add(")\n")
|
||||
g.add("\t})\n")
|
||||
g.add("\th := cgo.NewHandle(wrap)\n")
|
||||
g.add("\tcName := C.CString(\"" & ev.apiName & "\")\n")
|
||||
g.add("\tdefer C.free(unsafe.Pointer(cName))\n")
|
||||
g.add(
|
||||
"\thandle := uint64(C.go_cbor_subscribe(l.ctx, cName, unsafe.Pointer(h)))\n"
|
||||
)
|
||||
g.add("\tif handle == 0 {\n")
|
||||
g.add("\t\th.Delete()\n")
|
||||
g.add("\t\treturn 0\n")
|
||||
g.add("\t}\n")
|
||||
g.add("\tregisterCborHandle(l.ctx, h)\n")
|
||||
g.add("\treturn handle\n")
|
||||
g.add("}\n\n")
|
||||
g.add("func (l *" & sub & ") Off" & exName & "(handle uint64) {\n")
|
||||
g.add("\tif l.ctx == 0 { return }\n")
|
||||
g.add("\tcName := C.CString(\"" & ev.apiName & "\")\n")
|
||||
g.add("\tdefer C.free(unsafe.Pointer(cName))\n")
|
||||
g.add("\tC." & p & "unsubscribe(l.ctx, cName, C.uint64_t(handle))\n")
|
||||
g.add("}\n\n")
|
||||
|
||||
try:
|
||||
writeFile(modDir & "/" & libName & ".go", g)
|
||||
except IOError:
|
||||
error("Failed to write CBOR Go file: " & getCurrentExceptionMsg())
|
||||
|
||||
# ---------------------- <libName>_callbacks.c ----------------------
|
||||
if eventEntries.len > 0:
|
||||
var c = "// Generated by nim-brokers CBOR FFI Go codegen — do not edit.\n"
|
||||
c.add("#include <stdint.h>\n")
|
||||
c.add("#include <stdlib.h>\n")
|
||||
c.add("#include \"" & libName & ".h\"\n")
|
||||
c.add("#include \"_cgo_export.h\"\n\n")
|
||||
c.add(
|
||||
"uint64_t go_cbor_subscribe(uint32_t ctx, const char* name, void* user_data) {\n"
|
||||
)
|
||||
c.add(
|
||||
" return " & p & "subscribe(ctx, name, (" & p &
|
||||
"event_cb_t)goCborEventTrampoline, user_data);\n"
|
||||
)
|
||||
c.add("}\n")
|
||||
try:
|
||||
writeFile(modDir & "/" & libName & "_callbacks.c", c)
|
||||
except IOError:
|
||||
error("Failed to write CBOR Go callbacks file: " & getCurrentExceptionMsg())
|
||||
|
||||
{.push raises: [].}
|
||||
{.pop.}
|
||||
@@ -0,0 +1,203 @@
|
||||
## Generated C header for the CBOR FFI surface.
|
||||
##
|
||||
## Unlike the native codegen path (which accumulates per-request structs
|
||||
## into `gApiHeaderDeclarations`), the CBOR ABI is fixed: every library
|
||||
## exposes the same eight functions plus one typedef. The only per-library
|
||||
## variation is the symbol prefix and the documented sets of supported
|
||||
## apiNames / eventNames, which we emit as comment blocks for human
|
||||
## readers and language wrappers that aren't using the runtime discovery
|
||||
## API.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[macros, os, strutils]
|
||||
import ./api_common
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C header emission
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
{.pop.}
|
||||
|
||||
proc generateCborCHeaderFile*(
|
||||
outDir: string,
|
||||
libName: string,
|
||||
version: string,
|
||||
requestApiNames: seq[string],
|
||||
eventApiNames: seq[string],
|
||||
) {.compileTime, raises: [].} =
|
||||
## Writes the fixed-shape C header for a CBOR-mode library.
|
||||
ensureGeneratedOutputDir(outDir)
|
||||
|
||||
let guardName = libName.toUpperAscii().replace("-", "_") & "_H"
|
||||
let headerPath =
|
||||
if outDir.len > 0:
|
||||
outDir & "/" & libName & ".h"
|
||||
else:
|
||||
libName & ".h"
|
||||
let p = libName & "_"
|
||||
|
||||
var h = "/* Generated by nim-brokers CBOR FFI codegen — do not edit. */\n"
|
||||
h.add("#ifndef " & guardName & "\n")
|
||||
h.add("#define " & guardName & "\n\n")
|
||||
h.add("#include <stdint.h>\n")
|
||||
h.add("#include <stdbool.h>\n")
|
||||
h.add("#include <stddef.h>\n\n")
|
||||
h.add("#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n")
|
||||
|
||||
h.add("/* ----------------------------------------------------------------\n")
|
||||
h.add(" * Library identity\n")
|
||||
h.add(" * ---------------------------------------------------------------- */\n\n")
|
||||
h.add(
|
||||
"/* Returns a NUL-terminated semver string for this library build (\"" & version &
|
||||
"\").\n" & " * The returned pointer is owned by the library — do NOT free. */\n"
|
||||
)
|
||||
h.add("const char* " & p & "version(void);\n\n")
|
||||
|
||||
h.add("/* ----------------------------------------------------------------\n")
|
||||
h.add(" * Lifecycle\n")
|
||||
h.add(" * ---------------------------------------------------------------- */\n\n")
|
||||
h.add(
|
||||
"/* Initialise the Nim runtime and per-thread foreign GC state.\n" &
|
||||
" * Idempotent; safe to call from any thread before other entry points. */\n"
|
||||
)
|
||||
h.add("void " & p & "initialize(void);\n\n")
|
||||
h.add(
|
||||
"/* Create a new context. Returns the context id (>0 on success), or\n" &
|
||||
" * 0 on failure with *errOut populated by a Nim-allocated error\n" &
|
||||
" * message that the caller MUST free with " & p & "freeBuffer. */\n"
|
||||
)
|
||||
h.add("uint32_t " & p & "createContext(char** errOut);\n\n")
|
||||
h.add(
|
||||
"/* Tear down a context. Returns 0 on success, -1 if the context was\n" &
|
||||
" * not found or already shut down. */\n"
|
||||
)
|
||||
h.add("int32_t " & p & "shutdown(uint32_t ctx);\n\n")
|
||||
h.add(
|
||||
"/* reduced-A: release a sub-instance created by a create-instance request.\n" &
|
||||
" * Drops that ctx's request providers + event listeners on the processing\n" &
|
||||
" * thread; the Nim instance is then reclaimed by the GC. Idempotent and\n" &
|
||||
" * safe on an unknown/already-released ctx. Returns 0 on success. */\n"
|
||||
)
|
||||
h.add("int32_t " & p & "releaseInstance(uint32_t ctx);\n\n")
|
||||
|
||||
h.add("/* ----------------------------------------------------------------\n")
|
||||
h.add(" * Buffer ownership\n")
|
||||
h.add(" *\n")
|
||||
h.add(" * Every void* crossing this ABI is allocated by Nim and freed by\n")
|
||||
h.add(" * Nim. Callers obtain inbound request buffers via " & p & "allocBuffer,\n")
|
||||
h.add(" * fill them with CBOR, and pass them into " & p & "call (which frees\n")
|
||||
h.add(" * them before returning). Outbound response and error buffers are\n")
|
||||
h.add(" * allocated by the library and the caller frees them with\n")
|
||||
h.add(" * " & p & "freeBuffer.\n")
|
||||
h.add(" * ---------------------------------------------------------------- */\n\n")
|
||||
h.add(
|
||||
"/* Allocate a Nim-owned buffer of `size` bytes. Returns NULL on size\n" &
|
||||
" * <= 0, size > 64 MiB, or allocation failure. */\n"
|
||||
)
|
||||
h.add("void* " & p & "allocBuffer(int32_t size);\n\n")
|
||||
h.add("/* Free a buffer previously returned by " & p & "allocBuffer or by an\n")
|
||||
h.add(" * out-parameter from " & p & "call / " & p & "createContext. NULL is a\n")
|
||||
h.add(" * no-op. */\n")
|
||||
h.add("void " & p & "freeBuffer(void* buf);\n\n")
|
||||
|
||||
h.add("/* ----------------------------------------------------------------\n")
|
||||
h.add(" * Sync request gate\n")
|
||||
h.add(" *\n")
|
||||
h.add(" * Returns:\n")
|
||||
h.add(" * 0 — success; *respBufOut holds the CBOR response envelope\n")
|
||||
h.add(" * -1 — respBufOut or respLenOut is NULL\n")
|
||||
h.add(" * -2 — apiName is NULL\n")
|
||||
h.add(" * -3 — reqLen is negative or exceeds 64 MiB\n")
|
||||
h.add(" * -4 — apiName is unknown; *respBufOut holds a UTF-8 message\n")
|
||||
h.add(" * -10 — internal dispatch failure\n")
|
||||
h.add(" * ---------------------------------------------------------------- */\n\n")
|
||||
h.add(
|
||||
"int32_t " & p & "call(uint32_t ctx,\n" &
|
||||
" const char* apiName,\n" &
|
||||
" const void* reqBuf, int32_t reqLen,\n" &
|
||||
" void** respBufOut, int32_t* respLenOut);\n\n"
|
||||
)
|
||||
|
||||
h.add("/* ----------------------------------------------------------------\n")
|
||||
h.add(" * Event subscription\n")
|
||||
h.add(" *\n")
|
||||
h.add(" * Subscribe with cb == NULL probes whether the eventName is\n")
|
||||
h.add(" * supported by this library version: returns 1 (sentinel) when\n")
|
||||
h.add(" * supported, 0 when not. Real subscription handles are >= 2.\n")
|
||||
h.add(" *\n")
|
||||
h.add(" * Unsubscribe returns:\n")
|
||||
h.add(" * 0 — success\n")
|
||||
h.add(" * -1 — eventName is NULL\n")
|
||||
h.add(" * -2 — no subscriptions registered for (ctx, eventName)\n")
|
||||
h.add(" * -3 — handle not found in the subscription list\n")
|
||||
h.add(" *\n")
|
||||
h.add(" * Pass handle == 0 to remove every subscription for (ctx, eventName).\n")
|
||||
h.add(" * ---------------------------------------------------------------- */\n\n")
|
||||
h.add(
|
||||
"typedef void (*" & p & "event_cb_t)(uint32_t ctx,\n" &
|
||||
" const char* eventName,\n" &
|
||||
" const void* payloadBuf,\n" &
|
||||
" int32_t payloadLen,\n" &
|
||||
" void* userData);\n\n"
|
||||
)
|
||||
h.add(
|
||||
"uint64_t " & p & "subscribe(uint32_t ctx,\n" &
|
||||
" const char* eventName,\n" &
|
||||
" " & p & "event_cb_t cb,\n" &
|
||||
" void* userData);\n\n"
|
||||
)
|
||||
h.add(
|
||||
"int32_t " & p & "unsubscribe(uint32_t ctx,\n" &
|
||||
" const char* eventName,\n" &
|
||||
" uint64_t handle);\n\n"
|
||||
)
|
||||
|
||||
h.add("/* ----------------------------------------------------------------\n")
|
||||
h.add(" * Discovery API\n")
|
||||
h.add(" *\n")
|
||||
h.add(" * Both functions allocate the response with " & p & "allocBuffer; the\n")
|
||||
h.add(" * caller frees it via " & p & "freeBuffer.\n")
|
||||
h.add(" *\n")
|
||||
h.add(" * " & p & "listApis returns a JSON-encoded ApiList string:\n")
|
||||
h.add(" * {\"libName\": \"...\", \"requests\": [...], \"events\": [...]}\n")
|
||||
h.add(" *\n")
|
||||
h.add(" * " & p & "getSchema returns a JSON-encoded LibraryDescriptor string (full\n")
|
||||
h.add(" * schema including the embedded CDDL text). See <" & libName & ".cddl>\n")
|
||||
h.add(" * for the static schema.\n")
|
||||
h.add(" *\n")
|
||||
h.add(" * The response buffer is a UTF-8 JSON string (not null-terminated).\n")
|
||||
h.add(" *\n")
|
||||
h.add(" * Returns 0 on success, -1 if any out-pointer is NULL.\n")
|
||||
h.add(" * ---------------------------------------------------------------- */\n\n")
|
||||
h.add("int32_t " & p & "listApis(void** respBufOut, int32_t* respLenOut);\n\n")
|
||||
h.add("int32_t " & p & "getSchema(void** respBufOut, int32_t* respLenOut);\n\n")
|
||||
|
||||
if requestApiNames.len > 0 or eventApiNames.len > 0:
|
||||
h.add("/* ----------------------------------------------------------------\n")
|
||||
h.add(" * Documented apiNames\n")
|
||||
h.add(" * ---------------------------------------------------------------- */\n\n")
|
||||
if requestApiNames.len > 0:
|
||||
h.add("/* Requests (pass these as `apiName` to " & p & "call):\n")
|
||||
for n in requestApiNames:
|
||||
h.add(" * \"" & n & "\"\n")
|
||||
h.add(" */\n\n")
|
||||
if eventApiNames.len > 0:
|
||||
h.add("/* Events (pass these as `eventName` to " & p & "subscribe):\n")
|
||||
for n in eventApiNames:
|
||||
h.add(" * \"" & n & "\"\n")
|
||||
h.add(" */\n\n")
|
||||
|
||||
h.add("#ifdef __cplusplus\n}\n#endif\n\n")
|
||||
h.add("#endif /* " & guardName & " */\n")
|
||||
|
||||
try:
|
||||
writeFile(headerPath, h)
|
||||
except IOError:
|
||||
error(
|
||||
"Failed to write generated CBOR C header '" & headerPath & "': " &
|
||||
getCurrentExceptionMsg()
|
||||
)
|
||||
|
||||
{.push raises: [].}
|
||||
{.pop.}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,213 @@
|
||||
## api_codegen_cmake
|
||||
## ----------------
|
||||
## Emits a per-library CMake package next to the generated headers and shared
|
||||
## library so consumers can do:
|
||||
##
|
||||
## find_package(<lib> CONFIG REQUIRED)
|
||||
## target_link_libraries(myapp PRIVATE <lib>::<lib>) # C consumers
|
||||
## target_link_libraries(myapp PRIVATE <lib>::<lib>_cpp) # C++ consumers
|
||||
##
|
||||
## Files written into `outDir`:
|
||||
## <lib>Config.cmake — defines IMPORTED targets
|
||||
## <lib>ConfigVersion.cmake — version compatibility (SameMajorVersion)
|
||||
##
|
||||
## The package is fully relocatable: it resolves the shared library and headers
|
||||
## relative to its own location (`CMAKE_CURRENT_LIST_DIR`), which is the same
|
||||
## directory the Nim build dropped them into.
|
||||
##
|
||||
## CBOR mode adds a header-only jsoncons dependency on the C++ INTERFACE
|
||||
## target. Consumers can either install jsoncons system-wide or set
|
||||
## `<LIB>_JSONCONS_INCLUDE_DIR` before `find_package`.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/strutils
|
||||
import ./api_outdir
|
||||
|
||||
{.pop.}
|
||||
|
||||
proc generateCMakePackageFiles*(
|
||||
outDir: string, libName: string, version: string, cborMode: bool, hasCpp: bool
|
||||
) {.compileTime, raises: [].} =
|
||||
## Emits <lib>Config.cmake and <lib>ConfigVersion.cmake into `outDir`.
|
||||
ensureGeneratedOutputDir(outDir)
|
||||
|
||||
let baseDir =
|
||||
if outDir.len > 0:
|
||||
outDir & "/"
|
||||
else:
|
||||
""
|
||||
let configPath = baseDir & libName & "Config.cmake"
|
||||
let versionPath = baseDir & libName & "ConfigVersion.cmake"
|
||||
|
||||
let upperName = libName.toUpperAscii().replace("-", "_")
|
||||
let nsName = libName # same as IMPORTED namespace prefix
|
||||
|
||||
# ---------------- ConfigVersion.cmake ----------------
|
||||
# Hand-rolled SameMajorVersion logic so consumers don't need to invoke
|
||||
# CMakePackageConfigHelpers — the file is self-contained.
|
||||
let semverParts = version.split('.')
|
||||
let pkgMajor =
|
||||
if semverParts.len >= 1:
|
||||
semverParts[0]
|
||||
else:
|
||||
"0"
|
||||
var versionFile = ""
|
||||
versionFile.add(
|
||||
"# Auto-generated by brokers/api_codegen_cmake.nim — do not edit.\n"
|
||||
)
|
||||
versionFile.add("set(PACKAGE_VERSION \"" & version & "\")\n\n")
|
||||
versionFile.add("if(PACKAGE_FIND_VERSION VERSION_EQUAL PACKAGE_VERSION)\n")
|
||||
versionFile.add(" set(PACKAGE_VERSION_EXACT TRUE)\n")
|
||||
versionFile.add("endif()\n\n")
|
||||
versionFile.add(
|
||||
"if(NOT PACKAGE_FIND_VERSION OR PACKAGE_FIND_VERSION VERSION_LESS_EQUAL PACKAGE_VERSION)\n"
|
||||
)
|
||||
versionFile.add(" set(PACKAGE_VERSION_COMPATIBLE FALSE)\n")
|
||||
versionFile.add(
|
||||
" if(NOT PACKAGE_FIND_VERSION OR \"${PACKAGE_FIND_VERSION_MAJOR}\" STREQUAL \"" &
|
||||
pkgMajor & "\")\n"
|
||||
)
|
||||
versionFile.add(" set(PACKAGE_VERSION_COMPATIBLE TRUE)\n")
|
||||
versionFile.add(" endif()\n")
|
||||
versionFile.add("else()\n")
|
||||
versionFile.add(" set(PACKAGE_VERSION_COMPATIBLE FALSE)\n")
|
||||
versionFile.add("endif()\n")
|
||||
|
||||
try:
|
||||
writeFile(versionPath, versionFile)
|
||||
except IOError:
|
||||
discard # keep raises:[] — codegen errors shouldn't crash compilation
|
||||
|
||||
# ---------------- Config.cmake ----------------
|
||||
var cfg = ""
|
||||
cfg.add("# Auto-generated by brokers/api_codegen_cmake.nim — do not edit.\n")
|
||||
cfg.add("# CMake package for the '" & libName & "' broker FFI library.\n")
|
||||
cfg.add("#\n")
|
||||
cfg.add("# Provides:\n")
|
||||
cfg.add(
|
||||
"# " & nsName & "::" & libName & " — IMPORTED SHARED library + C headers\n"
|
||||
)
|
||||
if hasCpp:
|
||||
cfg.add(
|
||||
"# " & nsName & "::" & libName &
|
||||
"_cpp — INTERFACE for C++ consumers (C++20)\n"
|
||||
)
|
||||
if cborMode:
|
||||
cfg.add(
|
||||
"# (depends on jsoncons; set " & upperName &
|
||||
"_JSONCONS_INCLUDE_DIR to override discovery)\n"
|
||||
)
|
||||
cfg.add("\n")
|
||||
|
||||
cfg.add("cmake_minimum_required(VERSION 3.16)\n\n")
|
||||
|
||||
cfg.add(
|
||||
"get_filename_component(_" & libName &
|
||||
"_pkg_dir \"${CMAKE_CURRENT_LIST_DIR}\" ABSOLUTE)\n\n"
|
||||
)
|
||||
|
||||
# Resolve platform-specific shared library filename.
|
||||
cfg.add("if(WIN32)\n")
|
||||
cfg.add(" set(_" & libName & "_shared_name \"" & libName & ".dll\")\n")
|
||||
cfg.add(" set(_" & libName & "_import_name \"" & libName & ".lib\")\n")
|
||||
cfg.add("elseif(APPLE)\n")
|
||||
cfg.add(" set(_" & libName & "_shared_name \"lib" & libName & ".dylib\")\n")
|
||||
cfg.add("else()\n")
|
||||
cfg.add(" set(_" & libName & "_shared_name \"lib" & libName & ".so\")\n")
|
||||
cfg.add("endif()\n\n")
|
||||
|
||||
cfg.add(
|
||||
"set(_" & libName & "_shared_path \"${_" & libName & "_pkg_dir}/${_" & libName &
|
||||
"_shared_name}\")\n"
|
||||
)
|
||||
cfg.add("if(NOT EXISTS \"${_" & libName & "_shared_path}\")\n")
|
||||
cfg.add(
|
||||
" message(FATAL_ERROR \"" & libName & ": shared library not found at '${_" & libName &
|
||||
"_shared_path}'.\")\n"
|
||||
)
|
||||
cfg.add("endif()\n\n")
|
||||
|
||||
cfg.add(
|
||||
"set(_" & libName & "_header_path \"${_" & libName & "_pkg_dir}/" & libName &
|
||||
".h\")\n"
|
||||
)
|
||||
cfg.add("if(NOT EXISTS \"${_" & libName & "_header_path}\")\n")
|
||||
cfg.add(
|
||||
" message(FATAL_ERROR \"" & libName & ": C header not found at '${_" & libName &
|
||||
"_header_path}'.\")\n"
|
||||
)
|
||||
cfg.add("endif()\n\n")
|
||||
|
||||
# IMPORTED SHARED target — the C-level surface. Carries headers + library.
|
||||
cfg.add("if(NOT TARGET " & nsName & "::" & libName & ")\n")
|
||||
cfg.add(" add_library(" & nsName & "::" & libName & " SHARED IMPORTED)\n")
|
||||
cfg.add(" set_target_properties(" & nsName & "::" & libName & " PROPERTIES\n")
|
||||
cfg.add(" IMPORTED_LOCATION \"${_" & libName & "_shared_path}\"\n")
|
||||
cfg.add(" INTERFACE_INCLUDE_DIRECTORIES \"${_" & libName & "_pkg_dir}\"\n")
|
||||
cfg.add(" )\n")
|
||||
cfg.add(" if(WIN32)\n")
|
||||
cfg.add(
|
||||
" set(_" & libName & "_import_path \"${_" & libName & "_pkg_dir}/${_" & libName &
|
||||
"_import_name}\")\n"
|
||||
)
|
||||
cfg.add(" if(EXISTS \"${_" & libName & "_import_path}\")\n")
|
||||
cfg.add(
|
||||
" set_target_properties(" & nsName & "::" & libName &
|
||||
" PROPERTIES IMPORTED_IMPLIB \"${_" & libName & "_import_path}\")\n"
|
||||
)
|
||||
cfg.add(" endif()\n")
|
||||
cfg.add(" endif()\n")
|
||||
cfg.add("endif()\n\n")
|
||||
|
||||
if hasCpp:
|
||||
# INTERFACE target for C++ consumers — pulls in the C target, requires
|
||||
# C++20, and (CBOR mode) wires jsoncons.
|
||||
cfg.add("if(NOT TARGET " & nsName & "::" & libName & "_cpp)\n")
|
||||
cfg.add(" add_library(" & nsName & "::" & libName & "_cpp INTERFACE IMPORTED)\n")
|
||||
cfg.add(
|
||||
" set_property(TARGET " & nsName & "::" & libName &
|
||||
"_cpp PROPERTY INTERFACE_LINK_LIBRARIES " & nsName & "::" & libName & ")\n"
|
||||
)
|
||||
cfg.add(
|
||||
" set_property(TARGET " & nsName & "::" & libName &
|
||||
"_cpp PROPERTY INTERFACE_COMPILE_FEATURES cxx_std_20)\n"
|
||||
)
|
||||
|
||||
if cborMode:
|
||||
cfg.add("\n")
|
||||
cfg.add(" # jsoncons (header-only) is required by the CBOR-mode C++ wrapper.\n")
|
||||
cfg.add(" if(NOT DEFINED " & upperName & "_JSONCONS_INCLUDE_DIR)\n")
|
||||
cfg.add(
|
||||
" find_path(" & upperName & "_JSONCONS_INCLUDE_DIR\n" &
|
||||
" NAMES jsoncons/json.hpp\n" & " PATHS\n" & " \"${_" & libName &
|
||||
"_pkg_dir}/../../vendor/jsoncons/include\"\n" & " \"${_" & libName &
|
||||
"_pkg_dir}/../vendor/jsoncons/include\"\n" & " \"${_" & libName &
|
||||
"_pkg_dir}/vendor/jsoncons/include\"\n" &
|
||||
" DOC \"Path to the jsoncons header-only library (root containing 'jsoncons/json.hpp').\"\n" &
|
||||
" )\n"
|
||||
)
|
||||
cfg.add(" endif()\n")
|
||||
cfg.add(" if(NOT " & upperName & "_JSONCONS_INCLUDE_DIR)\n")
|
||||
cfg.add(
|
||||
" message(FATAL_ERROR \"" & libName &
|
||||
" (CBOR mode): jsoncons headers not found. Install jsoncons or set " &
|
||||
upperName & "_JSONCONS_INCLUDE_DIR.\")\n"
|
||||
)
|
||||
cfg.add(" endif()\n")
|
||||
cfg.add(
|
||||
" set_property(TARGET " & nsName & "::" & libName &
|
||||
"_cpp APPEND PROPERTY INTERFACE_INCLUDE_DIRECTORIES \"${" & upperName &
|
||||
"_JSONCONS_INCLUDE_DIR}\")\n"
|
||||
)
|
||||
cfg.add("endif()\n\n")
|
||||
|
||||
cfg.add("set(" & libName & "_FOUND TRUE)\n")
|
||||
cfg.add("set(" & libName & "_VERSION \"" & version & "\")\n")
|
||||
cfg.add("set(" & libName & "_LIBRARY \"${_" & libName & "_shared_path}\")\n")
|
||||
cfg.add("set(" & libName & "_INCLUDE_DIR \"${_" & libName & "_pkg_dir}\")\n")
|
||||
|
||||
try:
|
||||
writeFile(configPath, cfg)
|
||||
except IOError:
|
||||
discard
|
||||
@@ -0,0 +1,259 @@
|
||||
## API Common
|
||||
## ----------
|
||||
## Shared utilities for FFI API broker code generation.
|
||||
##
|
||||
## After the native FFI codegen surface was retired (see
|
||||
## `doc/CBOR_Refactoring.md`), this module is a thin coordination layer
|
||||
## that:
|
||||
## - Re-exports the type schema registry and FFI mode flag
|
||||
## - Owns the legacy FFI struct registry bridge
|
||||
## - Owns compile-time accumulators that are shared across broker macros
|
||||
## (event counters, handler entries, cleanup proc names)
|
||||
## - Provides runtime memory helpers for the FFI boundary
|
||||
##
|
||||
## This module is only used when compiling with `-d:BrokerFfiApi`.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/macros
|
||||
|
||||
import ./api_schema
|
||||
import ./api_outdir
|
||||
import ./helper/broker_utils
|
||||
|
||||
export api_schema
|
||||
export api_outdir
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Library name accumulator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var gApiLibraryName* {.compileTime.}: string = ""
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compile-time accumulators for delivery thread event system
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var gApiEventTypeCounter* {.compileTime.}: int = 0
|
||||
## Auto-incrementing type ID for EventBroker(API) types.
|
||||
## NOTE: Must be incremented directly (not via a helper proc) because the
|
||||
## Nim VM does not persist side effects from called compileTime procs.
|
||||
|
||||
var gApiSharedBrokerGenerated* {.compileTime.}: bool = false
|
||||
## Flag: has the shared RegisterEventListenerResult RequestBroker been emitted?
|
||||
|
||||
var gApiEventHandlerEntries* {.compileTime.}: seq[(int, string)] =
|
||||
@[] ## Accumulates (typeId, handlerProcName) pairs for the aggregate provider.
|
||||
|
||||
var gApiEventCleanupProcNames* {.compileTime.}: seq[string] =
|
||||
@[] ## Accumulates cleanup proc names for delivery thread teardown.
|
||||
|
||||
var gApiRequestCleanupProcNames* {.compileTime.}: seq[string] =
|
||||
@[] ## Accumulates cleanup proc names for request provider teardown.
|
||||
|
||||
var gApiEventProcessLoopShutdownProcNames* {.compileTime.}: seq[string] =
|
||||
@[] ## Accumulates async processLoop shutdown proc names for delivery thread teardown.
|
||||
|
||||
var gApiForeignGcHelperEmitted* {.compileTime.}: bool = false
|
||||
## Flag: has the ensureForeignThreadGc() helper been emitted?
|
||||
## Each broker codegen module checks this before emitting the helper
|
||||
## to avoid duplicate definitions.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CBOR-mode dispatch table accumulator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
type CborRequestEntry* = object
|
||||
apiName*: string ## Wire name foreign callers pass to `<lib>_call`.
|
||||
adapterProc*: string ## Identifier of the generated adapter proc.
|
||||
responseTypeName*: string
|
||||
## Nim type name for the response payload
|
||||
## (e.g. "GetStatus"). Foreign-language wrapper codegen consumes this
|
||||
## to emit typed return signatures. Empty if not yet populated by an
|
||||
## older caller path.
|
||||
argFields*: seq[(string, string)]
|
||||
## (paramName, nimType) pairs from
|
||||
## the request signature, in declaration order. Empty for zero-arg
|
||||
## requests. Wrapper codegen turns this into the typed method
|
||||
## signature and the args struct mirroring the synthetic Nim
|
||||
## `<Type>CborArgs` object.
|
||||
returnsInterface*: string
|
||||
## reduced-A: name of the BrokerInterface(API) this request *creates and
|
||||
## returns an instance of* (e.g. "IWidget"), or "" for a normal request.
|
||||
## When set, the wire `ok` value is a bare uint32 (the sub-instance's
|
||||
## BrokerContext); wrapper codegen emits a method returning the typed
|
||||
## sub-wrapper class built from that ctx instead of a decoded payload.
|
||||
|
||||
var gApiCborRequestEntries* {.compileTime.}: seq[CborRequestEntry] = @[]
|
||||
## Accumulated by `RequestBroker(API)` expansions.
|
||||
## `registerBrokerLibrary` drains this list to emit the per-library
|
||||
## `Table[string, CborApiAdapter]` and the `<lib>_call` dispatch.
|
||||
|
||||
type CborEventEntry* = object
|
||||
apiName*: string ## Wire eventName foreign callers pass to `<lib>_subscribe`.
|
||||
typeName*: string ## Nim type identifier for the event payload.
|
||||
|
||||
var gApiCborEventEntries* {.compileTime.}: seq[CborEventEntry] = @[]
|
||||
## Accumulated by `EventBroker(API)` expansions.
|
||||
## `registerBrokerLibrary` reads this list to generate per-event
|
||||
## listener installers and the `<lib>CborIsKnownEvent` predicate. As
|
||||
## with `gApiCborRequestEntries`, this list is read but not reset —
|
||||
## Nim's compile-time VM aliases `let` copies of seqs back to the
|
||||
## source.
|
||||
|
||||
proc registerCborEventEntry*(apiName, typeName: string) {.compileTime.} =
|
||||
## Register an event for the next library's CBOR-mode subscribe surface.
|
||||
for entry in gApiCborEventEntries:
|
||||
if entry.apiName == apiName:
|
||||
let ownerNew = interfaceOwningEventType(typeName)
|
||||
let ownerOld = interfaceOwningEventType(entry.typeName)
|
||||
let ifaceHint =
|
||||
if ownerNew.len > 0 or ownerOld.len > 0:
|
||||
" ('" & typeName & "' in interface " &
|
||||
(if ownerNew.len > 0: ownerNew else: "<library>") & " vs '" & entry.typeName &
|
||||
"' in interface " & (if ownerOld.len > 0: ownerOld else: "<library>") & ")"
|
||||
else:
|
||||
""
|
||||
error(
|
||||
"CBOR FFI: duplicate event apiName '" & apiName & "' (already registered by '" &
|
||||
entry.typeName & "')" & ifaceHint & ". " &
|
||||
"Each EventBroker(API) must have a unique event type name."
|
||||
)
|
||||
gApiCborEventEntries.add(CborEventEntry(apiName: apiName, typeName: typeName))
|
||||
|
||||
proc registerCborRequestEntry*(
|
||||
apiName, adapterProc: string,
|
||||
responseTypeName: string = "",
|
||||
argFields: seq[(string, string)] = @[],
|
||||
returnsInterface: string = "",
|
||||
) {.compileTime.} =
|
||||
## Register a CBOR request adapter for the next library that calls
|
||||
## `registerBrokerLibrary`. Detects duplicate apiNames at compile time
|
||||
## so two requests can't shadow each other on the wire.
|
||||
for entry in gApiCborRequestEntries:
|
||||
if entry.apiName == apiName:
|
||||
# reduced-A: name the owning interfaces when the collision spans two
|
||||
# BrokerInterface(API) declarations (apiNames are globally unique across
|
||||
# the whole library, not per interface).
|
||||
let ownerNew = interfaceOwningRequestType(responseTypeName)
|
||||
let ownerOld = interfaceOwningRequestType(entry.responseTypeName)
|
||||
let ifaceHint =
|
||||
if ownerNew.len > 0 or ownerOld.len > 0:
|
||||
" ('" & responseTypeName & "' in interface " &
|
||||
(if ownerNew.len > 0: ownerNew else: "<library>") & " vs '" &
|
||||
entry.responseTypeName & "' in interface " &
|
||||
(if ownerOld.len > 0: ownerOld else: "<library>") & ")"
|
||||
else:
|
||||
""
|
||||
error(
|
||||
"CBOR FFI: duplicate request apiName '" & apiName & "' (already registered by '" &
|
||||
entry.adapterProc & "')" & ifaceHint & ". " &
|
||||
"Each RequestBroker(API) must have a unique response type name."
|
||||
)
|
||||
gApiCborRequestEntries.add(
|
||||
CborRequestEntry(
|
||||
apiName: apiName,
|
||||
adapterProc: adapterProc,
|
||||
responseTypeName: responseTypeName,
|
||||
argFields: argFields,
|
||||
returnsInterface: returnsInterface,
|
||||
)
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Legacy FFI struct registry bridge
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var gApiFfiStructs* {.compileTime.}: seq[(string, seq[(string, string)])] = @[]
|
||||
## Legacy registry. Kept for backward compatibility with existing ApiType usage.
|
||||
## New code should use `gApiTypeRegistry` from `api_schema` instead.
|
||||
|
||||
proc registerApiFfiStruct*(
|
||||
typeName: string, fields: seq[(string, string)]
|
||||
) {.compileTime.} =
|
||||
## Register a type in both the legacy and new registries.
|
||||
gApiFfiStructs.add((typeName, fields))
|
||||
registerFromFieldTuples(typeName, fields)
|
||||
|
||||
proc lookupFfiStruct*(typeName: string): seq[(string, string)] {.compileTime.} =
|
||||
## Look up type fields. Checks the new type registry first, then falls back
|
||||
## to the legacy registry for backward compatibility.
|
||||
if isTypeRegistered(typeName):
|
||||
return lookupTypeFields(typeName)
|
||||
for (name, fields) in gApiFfiStructs:
|
||||
if name == typeName:
|
||||
return fields
|
||||
error(
|
||||
"Type '" & typeName & "' not registered. " &
|
||||
"Define it as a plain Nim type before the broker macro, " &
|
||||
"or declare it with `ApiType:` for explicit registration."
|
||||
)
|
||||
|
||||
{.pop.}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Runtime memory helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc allocCStringCopy*(s: string): cstring =
|
||||
## Allocates a copy of a Nim string as a shared C string.
|
||||
## The caller frees it via the generated FFI free helpers, which may run on
|
||||
## a different thread than the allocation site under --mm:refc.
|
||||
if s.len == 0:
|
||||
return nil
|
||||
let buf = cast[cstring](allocShared(s.len + 1))
|
||||
copyMem(buf, unsafeAddr s[0], s.len)
|
||||
cast[ptr char](cast[int](buf) + s.len)[] = '\0'
|
||||
buf
|
||||
|
||||
proc freeCString*(s: cstring) =
|
||||
## Frees a C string previously allocated by allocCStringCopy.
|
||||
if not s.isNil:
|
||||
deallocShared(s)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared-memory string helpers for cross-thread event data
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc allocSharedCString*(s: string): cstring =
|
||||
## Allocate a C string copy in shared memory (safe for cross-thread use).
|
||||
allocCStringCopy(s)
|
||||
|
||||
proc freeSharedCString*(s: cstring) =
|
||||
## Free a C string allocated by `allocSharedCString`.
|
||||
freeCString(s)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Foreign thread GC helper — emitted once per compilation unit
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc emitEnsureForeignThreadGc*(): NimNode {.compileTime.} =
|
||||
## Returns the AST for the per-thread foreign thread GC registration helper.
|
||||
## Call this from each broker codegen module; it emits the helper only once
|
||||
## per compilation unit (guarded by `gApiForeignGcHelperEmitted`).
|
||||
if gApiForeignGcHelperEmitted:
|
||||
return newStmtList()
|
||||
|
||||
gApiForeignGcHelperEmitted = true
|
||||
|
||||
let tvGcReg = genSym(nskVar, "gForeignGcRegistered")
|
||||
let ensureIdent = ident("ensureForeignThreadGc")
|
||||
|
||||
result = quote:
|
||||
var `tvGcReg` {.threadvar.}: bool
|
||||
|
||||
proc `ensureIdent`() {.inline.} =
|
||||
when compileOption("app", "lib"):
|
||||
if not `tvGcReg`:
|
||||
when declared(setupForeignThreadGc):
|
||||
# setupForeignThreadGc already registers the thread with the GC
|
||||
# and sets the stack bottom on modern Nim (>= 1.6). Manually
|
||||
# calling nimGC_setStackBottom on top of it can corrupt GC state.
|
||||
setupForeignThreadGc()
|
||||
else:
|
||||
# Fallback for very old Nim versions that lack setupForeignThreadGc.
|
||||
when declared(nimGC_setStackBottom):
|
||||
var locals {.volatile, noinit.}: pointer
|
||||
locals = addr(locals)
|
||||
nimGC_setStackBottom(locals)
|
||||
`tvGcReg` = true
|
||||
@@ -0,0 +1,105 @@
|
||||
## API EventBroker — CBOR mode codegen
|
||||
## ------------------------------------
|
||||
## Generates the CBOR-mode surface for `EventBroker(API)` declarations.
|
||||
##
|
||||
## For each declaration this module emits:
|
||||
##
|
||||
## 1. The underlying multi-thread EventBroker (via `generateMtEventBroker`).
|
||||
## Internal cross-thread emit dispatch stays as typed `Channel[T]`
|
||||
## traffic; CBOR encoding only happens at the moment we hand the event
|
||||
## to a foreign C callback.
|
||||
##
|
||||
## 2. A compile-time entry in `gApiCborEventEntries` so the upcoming
|
||||
## `registerBrokerLibrary` CBOR backend can wire the event into the
|
||||
## library's subscribe surface and emit a per-event listener installer.
|
||||
##
|
||||
## Listener installation is intentionally NOT generated here — installers
|
||||
## need access to the library's subscription map / lock / callback-type,
|
||||
## which only exist at `registerBrokerLibrary` expansion time. We just
|
||||
## record the event's wire name and Nim type identifier; the library macro
|
||||
## materialises the installer with the right captures.
|
||||
##
|
||||
## Wire `eventName` is the snake_case form of the event's Nim type
|
||||
## identifier — e.g. `DeviceUpdated` becomes `device_updated`.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[macros, strutils]
|
||||
import ./helper/broker_utils, ./mt_event_broker, ./mt_config, ./api_common, ./api_schema
|
||||
import ./api_request_broker_cbor # for registerCborObjectType
|
||||
import ./api_type_resolver
|
||||
import ./broker_debug
|
||||
|
||||
# `api_type_resolver` re-export: see note in `api_request_broker_cbor.nim`
|
||||
# — `autoRegisterApiType` is emitted into user code by broker macros and
|
||||
# must resolve at the user-library expansion site post-Part-A retirement
|
||||
# of the native `api_event_broker` re-export chain.
|
||||
export mt_event_broker, mt_config, api_common, api_type_resolver
|
||||
|
||||
proc generateApiCborEventBrokerImpl(body: NimNode, cfg: MtEvtCfg): NimNode =
|
||||
result = newStmtList()
|
||||
|
||||
# 1. Emit the underlying MT event broker (single-thread emit/listen API
|
||||
# visible to user code, MT-aware cross-thread dispatch under the hood).
|
||||
# The capacity config flows in from the outer EventBroker(API, ...)
|
||||
# kwargs — same knobs as EventBroker(mt).
|
||||
result.add(generateMtEventBroker(copyNimTree(body), cfg))
|
||||
|
||||
# 2. Parse the event type identifier and register the entry. Capture
|
||||
# field info so wrapper codegen can emit typed structs for the
|
||||
# payload.
|
||||
let parsed = parseSingleTypeDef(
|
||||
body, "EventBroker", allowRefToNonObject = true, collectFieldInfo = true
|
||||
)
|
||||
let typeIdent = parsed.typeIdent
|
||||
let typeName = sanitizeIdentName(typeIdent)
|
||||
let apiName = toSnakeCase(typeName)
|
||||
if parsed.hasInlineFields:
|
||||
registerCborObjectType(typeName, parsed.fieldNames, parsed.fieldTypes)
|
||||
elif parsed.isVoid:
|
||||
# `void` → a zero-field object: a payload-less event notification.
|
||||
registerCborObjectType(typeName, @[], @[])
|
||||
else:
|
||||
registerCborPrimitiveType(typeName, parsed)
|
||||
registerCborEventEntry(apiName, typeName)
|
||||
|
||||
when defined(brokerDebug):
|
||||
writeBrokerDebug(
|
||||
"EventBrokerApi", typeName, result, header = "eventName='" & apiName & "'"
|
||||
)
|
||||
when defined(brokerDebugStdout):
|
||||
echo "[brokers/cbor] EventBroker(API) for '" & typeName & "' (eventName='" &
|
||||
apiName & "')"
|
||||
echo result.repr
|
||||
|
||||
{.pop.}
|
||||
|
||||
macro generateApiCborEventBrokerDeferred*(args: varargs[untyped]): untyped =
|
||||
## Typed-phase deferred entry point; populates the registry first.
|
||||
## Args layout: [body, kw0, kw1, ...] — kwargs are forwarded as raw
|
||||
## `nnkExprEqExpr` nodes from `generateApiCborEventBroker` so we
|
||||
## re-parse them here into an MtEvtCfg.
|
||||
if args.len == 0:
|
||||
error("generateApiCborEventBrokerDeferred requires a body", args)
|
||||
let body = args[0]
|
||||
var kwargs: seq[NimNode]
|
||||
for i in 1 ..< args.len:
|
||||
kwargs.add(args[i])
|
||||
let cfg = parseMtEvtKwargs(kwargs)
|
||||
generateApiCborEventBrokerImpl(body, cfg)
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
proc generateApiCborEventBroker*(body: NimNode, kwargs: seq[NimNode]): NimNode =
|
||||
result = newStmtList()
|
||||
|
||||
let externalIdents = discoverExternalTypes(body)
|
||||
if externalIdents.len > 0:
|
||||
result.add(emitAutoRegistrations(externalIdents))
|
||||
|
||||
let deferred = newCall(ident("generateApiCborEventBrokerDeferred"), copyNimTree(body))
|
||||
for kw in kwargs:
|
||||
deferred.add(copyNimTree(kw))
|
||||
result.add(deferred)
|
||||
|
||||
{.pop.}
|
||||
@@ -0,0 +1,38 @@
|
||||
## api_outdir
|
||||
## ----------
|
||||
## Tiny compile-time helper for ensuring the generated-output directory
|
||||
## exists. Extracted from the (now-retired) native `api_codegen_c.nim` so
|
||||
## the CBOR codegen and the CMake package emitter can share it without
|
||||
## pulling in any native-codegen module.
|
||||
|
||||
import std/[os, macros, compilesettings]
|
||||
|
||||
proc detectOutputDir*(overrideOutDir = ""): string {.compileTime.} =
|
||||
## Resolves the compiler output directory for generated artifacts. Returns
|
||||
## the override if supplied, otherwise consults `outDir` / `outFile`
|
||||
## query settings, falling back to the empty string.
|
||||
if overrideOutDir.len > 0:
|
||||
return overrideOutDir
|
||||
|
||||
let configuredOutDir = querySetting(SingleValueSetting.outDir)
|
||||
if configuredOutDir.len > 0:
|
||||
return configuredOutDir
|
||||
|
||||
let configuredOutFile = querySetting(SingleValueSetting.outFile)
|
||||
if configuredOutFile.len > 0:
|
||||
let candidateDir = splitFile(configuredOutFile).dir
|
||||
if candidateDir.len > 0:
|
||||
return candidateDir
|
||||
|
||||
return ""
|
||||
|
||||
proc ensureGeneratedOutputDir*(outDir: string) {.compileTime, raises: [].} =
|
||||
if outDir.len == 0 or dirExists(outDir):
|
||||
return
|
||||
try:
|
||||
createDir(outDir)
|
||||
except CatchableError:
|
||||
error(
|
||||
"Failed to create generated output directory '" & outDir & "': " &
|
||||
getCurrentExceptionMsg()
|
||||
)
|
||||
@@ -0,0 +1,628 @@
|
||||
## API RequestBroker — CBOR mode codegen
|
||||
## --------------------------------------
|
||||
## Generates the CBOR-mode surface for `RequestBroker(API)` declarations.
|
||||
##
|
||||
## For each declaration this module emits:
|
||||
##
|
||||
## 1. The underlying multi-thread RequestBroker, exactly as the native path
|
||||
## does — providers register and run on the processing thread the same
|
||||
## way regardless of FFI mode. Internal cross-thread dispatch stays as
|
||||
## typed `Channel[T]` traffic; CBOR encoding only happens at the C ABI
|
||||
## boundary.
|
||||
##
|
||||
## 2. (When the signature has arguments) a synthetic per-request CBOR args
|
||||
## object that mirrors the parameter list field-by-field. Decoding the
|
||||
## foreign request buffer into this object gives us individual local
|
||||
## variables to forward into the broker's `request` call.
|
||||
##
|
||||
## 3. A CBOR adapter proc with the canonical signature
|
||||
##
|
||||
## proc <Type>CborAdapter*(ctx: BrokerContext, reqBuf: seq[byte]):
|
||||
## Future[seq[byte]] {.async: (raises: []).}
|
||||
##
|
||||
## The adapter decodes the request buffer (or ignores it for zero-arg
|
||||
## requests), `await`s the typed broker call, and encodes the resulting
|
||||
## `Result[T, string]` as a CBOR response envelope.
|
||||
##
|
||||
## 4. A compile-time entry in `gApiCborRequestEntries` so the upcoming
|
||||
## `registerBrokerLibrary` CBOR backend can wire the adapter into the
|
||||
## library's dispatch table.
|
||||
##
|
||||
## The wire `apiName` is the snake_case form of the response type name —
|
||||
## e.g. `InitializeRequest` becomes `initialize_request`. Foreign wrappers
|
||||
## are generated to use the same name so the C entry point sees a stable
|
||||
## identifier per broker.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[macros, strutils]
|
||||
import
|
||||
./helper/broker_utils,
|
||||
./mt_request_broker,
|
||||
./mt_config,
|
||||
./api_common,
|
||||
./api_cbor_codec,
|
||||
./api_schema,
|
||||
./api_type_resolver,
|
||||
./broker_debug
|
||||
|
||||
# `api_type_resolver` re-export: `autoRegisterApiType` is emitted into the
|
||||
# user-library AST by the broker macros and must resolve at the user's
|
||||
# expansion site. Previously this came in transitively via the native
|
||||
# `api_request_broker` re-export chain (retired in Part A); re-export it
|
||||
# explicitly here so user code never needs a direct
|
||||
# `import brokers/internal/api_type_resolver`.
|
||||
export mt_request_broker, mt_config, api_common, api_cbor_codec, api_type_resolver
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Schema registration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc registerCborObjectType*(
|
||||
typeName: string, fieldNames, fieldTypes: seq[NimNode]
|
||||
) {.compileTime.} =
|
||||
## Register a parsed object type in `gApiTypeRegistry` so the C++ /
|
||||
## Python / etc. wrapper codegen can emit typed structs for it.
|
||||
## Idempotent — subsequent calls for the same type are a no-op so a
|
||||
## type that ends up registered through both the auto-resolver and a
|
||||
## broker macro doesn't double-list.
|
||||
if isTypeRegistered(typeName):
|
||||
return
|
||||
var entry = ApiTypeEntry(name: typeName, kind: atkObject)
|
||||
for i in 0 ..< fieldNames.len:
|
||||
var fname = $fieldNames[i]
|
||||
# `fieldNames` from parseSingleTypeDef carry the original AST,
|
||||
# which for inline `object` types is a plain Ident (export marker
|
||||
# already lifted by the parser). Strip a trailing '*' defensively.
|
||||
if fname.endsWith("*"):
|
||||
fname.setLen(fname.len - 1)
|
||||
let ftype = fieldTypes[i].repr.strip()
|
||||
entry.fields.add(ApiFieldDef(name: fname, nimType: ftype))
|
||||
registerTypeEntry(entry)
|
||||
|
||||
proc registerCborPrimitiveType*(
|
||||
typeName: string, parsed: ParsedBrokerType
|
||||
) {.compileTime.} =
|
||||
## Register a primitive (non-object) broker type — `type X = int32` — as a
|
||||
## distinct alias of its underlying primitive. Wrapper codegen then emits a
|
||||
## `using X = <prim>` alias and treats X as an emittable scalar payload (the
|
||||
## CBOR wire value is a bare scalar, not a map). A no-op for non-primitive
|
||||
## non-object types, which stay TODO-stubbed in the wrappers.
|
||||
if isTypeRegistered(typeName):
|
||||
return
|
||||
if parsed.objectDef.kind == nnkDistinctTy and parsed.objectDef.len == 1 and
|
||||
parsed.objectDef[0].kind == nnkIdent and isNimPrimitive($parsed.objectDef[0]) and
|
||||
($parsed.objectDef[0]).toLowerAscii() notin ["cstring"]:
|
||||
# `string` is allowed (maps to the wrapper's native string type) so a POD /
|
||||
# option-B `string`-payload request is emittable; `cstring` stays excluded
|
||||
# (unsafe to marshal across the FFI/CBOR boundary).
|
||||
registerTypeEntry(makeAliasEntry(typeName, $parsed.objectDef[0], atkDistinct))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Adapter proc type — exposed so registerBrokerLibrary (CBOR mode) can
|
||||
# materialise a uniform table of dispatchers.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
type CborApiAdapter* = proc(ctx: BrokerContext, reqBuf: seq[byte]): Future[seq[byte]] {.
|
||||
async: (raises: []), gcsafe
|
||||
.}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc collectSignatures(
|
||||
body: NimNode
|
||||
): tuple[
|
||||
zeroArg: NimNode,
|
||||
argSig: NimNode,
|
||||
argParams: seq[NimNode],
|
||||
zeroArgName: string,
|
||||
argSigName: string,
|
||||
] {.compileTime.} =
|
||||
## Walk the macro body and split the (at most two) `signature*` proc
|
||||
## declarations into the zero-arg and arg-based slots, mirroring
|
||||
## `mt_request_broker` and the native path's handling.
|
||||
result.zeroArg = nil
|
||||
result.argSig = nil
|
||||
result.argParams = @[]
|
||||
result.zeroArgName = ""
|
||||
result.argSigName = ""
|
||||
|
||||
for stmt in body:
|
||||
if stmt.kind != nnkProcDef:
|
||||
continue
|
||||
let procName = stmt[0]
|
||||
let procNameIdent =
|
||||
case procName.kind
|
||||
of nnkIdent:
|
||||
procName
|
||||
of nnkPostfix:
|
||||
procName[1]
|
||||
else:
|
||||
procName
|
||||
if not ($procNameIdent).startsWith("signature"):
|
||||
error("Signature proc names must start with `signature`", procName)
|
||||
|
||||
let params = stmt.params
|
||||
let paramCount = params.len - 1
|
||||
if paramCount == 0:
|
||||
result.zeroArg = stmt
|
||||
result.zeroArgName = $procNameIdent
|
||||
elif paramCount >= 1:
|
||||
result.argSig = stmt
|
||||
result.argSigName = $procNameIdent
|
||||
for idx in 1 ..< params.len:
|
||||
result.argParams.add(copyNimTree(params[idx]))
|
||||
|
||||
proc snakeApiName(typeIdent: NimNode): string {.compileTime.} =
|
||||
## Wire `apiName` for a request: snake_case form of the response type
|
||||
## identifier. e.g. `InitializeRequest` -> `initialize_request`.
|
||||
toSnakeCase(sanitizeIdentName(typeIdent))
|
||||
|
||||
proc emitArgsType(
|
||||
argsTypeIdent: NimNode, argParams: seq[NimNode]
|
||||
): NimNode {.compileTime.} =
|
||||
## Build `type <argsTypeIdent>* = object\n field1*: T1\n field2*: T2`
|
||||
## from the arg-based signature's parameter nodes.
|
||||
##
|
||||
## Each `argParams[i]` is an `nnkIdentDefs` node carrying one or more
|
||||
## names plus a type. We expand each name into its own field so an arg
|
||||
## like `(a, b: int32)` produces two separate object fields.
|
||||
var recList = newNimNode(nnkRecList)
|
||||
for paramDefs in argParams:
|
||||
let lastIdx = paramDefs.len - 1
|
||||
let typeNode = paramDefs[lastIdx - 1]
|
||||
for nameIdx in 0 ..< lastIdx - 1:
|
||||
let nameNode = paramDefs[nameIdx]
|
||||
let fieldIdent =
|
||||
case nameNode.kind
|
||||
of nnkIdent, nnkSym:
|
||||
ident($nameNode)
|
||||
of nnkPostfix:
|
||||
ident($nameNode[1])
|
||||
of nnkPragmaExpr:
|
||||
ident($nameNode[0])
|
||||
else:
|
||||
ident($nameNode)
|
||||
recList.add(
|
||||
newTree(
|
||||
nnkIdentDefs, postfix(fieldIdent, "*"), copyNimTree(typeNode), newEmptyNode()
|
||||
)
|
||||
)
|
||||
|
||||
newTree(
|
||||
nnkTypeSection,
|
||||
newTree(
|
||||
nnkTypeDef,
|
||||
postfix(argsTypeIdent, "*"),
|
||||
newEmptyNode(),
|
||||
newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), recList),
|
||||
),
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Adapter emission
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc emitZeroArgAdapter(
|
||||
typeIdent: NimNode, payloadType: NimNode, adapterIdent: NimNode, isVoid: bool
|
||||
): NimNode {.compileTime.} =
|
||||
## Adapter for a zero-argument request: ignore the input buffer, await
|
||||
## the broker call, encode the response envelope. `typeIdent` is the
|
||||
## dispatch tag; `payloadType` is the (decoupled) value type the request
|
||||
## resolves to and the envelope carries.
|
||||
##
|
||||
## For a `void` payload the public broker resolves to `Result[void, string]`,
|
||||
## which has no `Option[void]`-encodable envelope. We bridge it to the wire
|
||||
## unit type `CborUnit` (a zero-field map `{}`), matching the legacy
|
||||
## `type X = void` form bit-for-bit.
|
||||
if isVoid:
|
||||
quote:
|
||||
proc `adapterIdent`*(
|
||||
ctx: BrokerContext, reqBuf: seq[byte]
|
||||
): Future[seq[byte]] {.async: (raises: []), gcsafe.} =
|
||||
discard reqBuf
|
||||
let r = await `typeIdent`.request(ctx)
|
||||
let unitR =
|
||||
if r.isOk:
|
||||
Result[CborUnit, string].ok(CborUnit())
|
||||
else:
|
||||
Result[CborUnit, string].err(r.error)
|
||||
let envBytes = cborEncodeResultEnvelope(unitR)
|
||||
if envBytes.isOk:
|
||||
return envBytes.value
|
||||
let errEnv = cborEncodeResultEnvelope(
|
||||
Result[CborUnit, string].err("response encode failed: " & envBytes.error)
|
||||
)
|
||||
if errEnv.isOk:
|
||||
return errEnv.value
|
||||
return @[]
|
||||
|
||||
else:
|
||||
quote:
|
||||
proc `adapterIdent`*(
|
||||
ctx: BrokerContext, reqBuf: seq[byte]
|
||||
): Future[seq[byte]] {.async: (raises: []), gcsafe.} =
|
||||
discard reqBuf
|
||||
let r = await `typeIdent`.request(ctx)
|
||||
let envBytes = cborEncodeResultEnvelope(r)
|
||||
if envBytes.isOk:
|
||||
return envBytes.value
|
||||
let errEnv = cborEncodeResultEnvelope(
|
||||
Result[`payloadType`, string].err("response encode failed: " & envBytes.error)
|
||||
)
|
||||
if errEnv.isOk:
|
||||
return errEnv.value
|
||||
return @[]
|
||||
|
||||
proc emitArgAdapter(
|
||||
typeIdent: NimNode,
|
||||
payloadType: NimNode,
|
||||
adapterIdent: NimNode,
|
||||
argsTypeIdent: NimNode,
|
||||
argParams: seq[NimNode],
|
||||
isVoid: bool,
|
||||
): NimNode {.compileTime, raises: [ValueError].} =
|
||||
## Adapter for an arg-based request. Decodes the request buffer into the
|
||||
## synthesised `argsTypeIdent`, awaits the broker call with each field
|
||||
## unpacked positionally, and encodes the resulting envelope.
|
||||
##
|
||||
## The proc body is rendered as a Nim source string and parsed back via
|
||||
## `parseStmt`. This sidesteps the awkward interaction between `quote
|
||||
## do:`'s gensym'd proc parameters and pre-built call nodes — every
|
||||
## identifier in the rendered string lives in the same local scope, so
|
||||
## name resolution is straightforward.
|
||||
var fieldNames: seq[string] = @[]
|
||||
for paramDefs in argParams:
|
||||
let lastIdx = paramDefs.len - 1
|
||||
for nameIdx in 0 ..< lastIdx - 1:
|
||||
let nameNode = paramDefs[nameIdx]
|
||||
let nameStr =
|
||||
case nameNode.kind
|
||||
of nnkIdent, nnkSym:
|
||||
$nameNode
|
||||
of nnkPostfix:
|
||||
$nameNode[1]
|
||||
of nnkPragmaExpr:
|
||||
$nameNode[0]
|
||||
else:
|
||||
$nameNode
|
||||
fieldNames.add(nameStr)
|
||||
|
||||
var argList = ""
|
||||
for f in fieldNames:
|
||||
argList.add(", decoded." & f)
|
||||
|
||||
let typeIdentName = $typeIdent
|
||||
# A `void` payload resolves to `Result[void, string]` (no encodable
|
||||
# envelope); bridge it to the wire unit type `CborUnit`, matching the
|
||||
# legacy `type X = void` form. Every envelope on this path then carries
|
||||
# `CborUnit`, and the awaited result is converted before encoding.
|
||||
let envTypeName =
|
||||
if isVoid:
|
||||
"CborUnit"
|
||||
else:
|
||||
payloadType.repr.strip()
|
||||
let argsTypeIdentName = $argsTypeIdent
|
||||
let adapterIdentName = $adapterIdent
|
||||
|
||||
let encodeRespSrc =
|
||||
if isVoid:
|
||||
" let unitR =\n" & " if r.isOk: Result[CborUnit, string].ok(CborUnit())\n" &
|
||||
" else: Result[CborUnit, string].err(r.error)\n" &
|
||||
" let envBytes = cborEncodeResultEnvelope(unitR)\n"
|
||||
else:
|
||||
" let envBytes = cborEncodeResultEnvelope(r)\n"
|
||||
|
||||
let src =
|
||||
"proc " & adapterIdentName & "*(\n" & " ctx: BrokerContext, reqBuf: seq[byte]\n" &
|
||||
"): Future[seq[byte]] {.async: (raises: []), gcsafe.} =\n" &
|
||||
" let decRes = cborDecode(reqBuf, " & argsTypeIdentName & ")\n" &
|
||||
" if decRes.isErr:\n" & " let errEnv = cborEncodeResultEnvelope(\n" &
|
||||
" Result[" & envTypeName &
|
||||
", string].err(\"request decode failed: \" & decRes.error)\n" & " )\n" &
|
||||
" if errEnv.isOk:\n" & " return errEnv.value\n" & " return @[]\n" &
|
||||
" let decoded = decRes.value\n" & " let r = await " & typeIdentName &
|
||||
".request(ctx" & argList & ")\n" & encodeRespSrc & " if envBytes.isOk:\n" &
|
||||
" return envBytes.value\n" & " let errEnv = cborEncodeResultEnvelope(\n" &
|
||||
" Result[" & envTypeName &
|
||||
", string].err(\"response encode failed: \" & envBytes.error)\n" & " )\n" &
|
||||
" if errEnv.isOk:\n" & " return errEnv.value\n" & " return @[]\n"
|
||||
|
||||
parseStmt(src)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# reduced-A: create-instance adapters. When a request's Ok payload type is a
|
||||
# registered BrokerInterface(API), the provider builds and returns a sub-
|
||||
# interface ref. We do NOT CBOR-encode the ref; instead the adapter extracts
|
||||
# the sub-instance's BrokerContext and encodes it as a bare `uint32` (the
|
||||
# routing handle). The foreign wrapper decodes that ctx and constructs the
|
||||
# typed sub-wrapper class. Adapter + provider both run on the processing
|
||||
# thread (same-thread direct dispatch), so the ref never crosses a channel —
|
||||
# safe under both --mm:refc and --mm:orc.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc emitZeroArgInstanceAdapter(
|
||||
typeIdent, adapterIdent: NimNode
|
||||
): NimNode {.compileTime, raises: [ValueError].} =
|
||||
let src =
|
||||
"proc " & $adapterIdent & "*(\n" & " ctx: BrokerContext, reqBuf: seq[byte]\n" &
|
||||
"): Future[seq[byte]] {.async: (raises: []), gcsafe.} =\n" & " discard reqBuf\n" &
|
||||
" let r = await " & $typeIdent & ".request(ctx)\n" & " if r.isOk:\n" &
|
||||
" installApiListenersForCtx(r.value.brokerCtx)\n" & " let mapped =\n" &
|
||||
" if r.isOk: Result[uint32, string].ok(uint32(r.value.brokerCtx))\n" &
|
||||
" else: Result[uint32, string].err(r.error)\n" &
|
||||
" let envBytes = cborEncodeResultEnvelope(mapped)\n" & " if envBytes.isOk:\n" &
|
||||
" return envBytes.value\n" & " return @[]\n"
|
||||
parseStmt(src)
|
||||
|
||||
proc emitArgInstanceAdapter(
|
||||
typeIdent, adapterIdent, argsTypeIdent: NimNode, argParams: seq[NimNode]
|
||||
): NimNode {.compileTime, raises: [ValueError].} =
|
||||
var fieldNames: seq[string] = @[]
|
||||
for paramDefs in argParams:
|
||||
let lastIdx = paramDefs.len - 1
|
||||
for nameIdx in 0 ..< lastIdx - 1:
|
||||
let nameNode = paramDefs[nameIdx]
|
||||
let nameStr =
|
||||
case nameNode.kind
|
||||
of nnkIdent, nnkSym:
|
||||
$nameNode
|
||||
of nnkPostfix:
|
||||
$nameNode[1]
|
||||
of nnkPragmaExpr:
|
||||
$nameNode[0]
|
||||
else:
|
||||
$nameNode
|
||||
fieldNames.add(nameStr)
|
||||
var argList = ""
|
||||
for f in fieldNames:
|
||||
argList.add(", decoded." & f)
|
||||
let src =
|
||||
"proc " & $adapterIdent & "*(\n" & " ctx: BrokerContext, reqBuf: seq[byte]\n" &
|
||||
"): Future[seq[byte]] {.async: (raises: []), gcsafe.} =\n" &
|
||||
" let decRes = cborDecode(reqBuf, " & $argsTypeIdent & ")\n" &
|
||||
" if decRes.isErr:\n" & " let errEnv = cborEncodeResultEnvelope(\n" &
|
||||
" Result[uint32, string].err(\"request decode failed: \" & decRes.error))\n" &
|
||||
" if errEnv.isOk:\n" & " return errEnv.value\n" & " return @[]\n" &
|
||||
" let decoded = decRes.value\n" & " let r = await " & $typeIdent & ".request(ctx" &
|
||||
argList & ")\n" & " if r.isOk:\n" &
|
||||
" installApiListenersForCtx(r.value.brokerCtx)\n" & " let mapped =\n" &
|
||||
" if r.isOk: Result[uint32, string].ok(uint32(r.value.brokerCtx))\n" &
|
||||
" else: Result[uint32, string].err(r.error)\n" &
|
||||
" let envBytes = cborEncodeResultEnvelope(mapped)\n" & " if envBytes.isOk:\n" &
|
||||
" return envBytes.value\n" & " return @[]\n"
|
||||
parseStmt(src)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc generateApiCborRequestBrokerImpl(
|
||||
body: NimNode, cfg: MtReqCfg
|
||||
): NimNode {.raises: [ValueError].} =
|
||||
## Deferred-phase codegen for `RequestBroker(API)` under CBOR mode.
|
||||
## Runs after the typed-phase `autoRegisterApiType` calls have populated
|
||||
## `gApiTypeRegistry` for any external types referenced in the broker
|
||||
## response object or signature parameters (enums, distinct types,
|
||||
## nested objects). Wrapper codegen consumes that registry to emit
|
||||
## typed dataclasses / encoders / decoders.
|
||||
result = newStmtList()
|
||||
|
||||
# 1. Emit the underlying MT broker (typed Nim<->Nim dispatch on the
|
||||
# processing thread, identical to the native path). Capacity
|
||||
# config flows in from the outer RequestBroker(API, ...) kwargs —
|
||||
# same knobs as RequestBroker(mt).
|
||||
result.add(generateMtRequestBroker(copyNimTree(body), cfg))
|
||||
|
||||
# 2. Determine dispatch tag, payload, signatures, and the schema parse,
|
||||
# supporting both the legacy `signature*` form and the proc-sugar.
|
||||
var hasSignatureProc = false
|
||||
var hasOtherProc = false
|
||||
for stmt in body:
|
||||
if stmt.kind == nnkProcDef:
|
||||
let nm = stmt[0]
|
||||
let nmId = (if nm.kind == nnkPostfix: nm[1] else: nm)
|
||||
if ($nmId).startsWith("signature"):
|
||||
hasSignatureProc = true
|
||||
else:
|
||||
hasOtherProc = true
|
||||
let isSugar = hasOtherProc and not hasSignatureProc
|
||||
|
||||
var typeIdent: NimNode = nil
|
||||
var payloadType: NimNode = nil
|
||||
var parsed: ParsedBrokerType
|
||||
var zeroArgPresent = false
|
||||
var argPresent = false
|
||||
var argParams: seq[NimNode] = @[]
|
||||
# Wire apiName suffixes. Legacy form keeps its descriptive
|
||||
# `signature<Suffix>` mechanism (backward-compatible). The new proc-sugar
|
||||
# uses the finalized rule: zero-arg stays bare, arg-based gets `_arg`.
|
||||
var zeroApiSuffix = ""
|
||||
var argApiSuffix = ""
|
||||
|
||||
proc legacySuffix(sigName: string): string =
|
||||
if sigName.len <= "signature".len:
|
||||
return ""
|
||||
toSnakeCase(sigName["signature".len .. ^1])
|
||||
|
||||
if not isSugar:
|
||||
parsed = parseSingleTypeDef(
|
||||
body, "RequestBroker", allowRefToNonObject = true, collectFieldInfo = true
|
||||
)
|
||||
typeIdent = parsed.typeIdent
|
||||
payloadType = copyNimTree(typeIdent)
|
||||
let sigs = collectSignatures(body)
|
||||
zeroArgPresent = not sigs.zeroArg.isNil
|
||||
argPresent = not sigs.argSig.isNil
|
||||
argParams = sigs.argParams
|
||||
if zeroArgPresent and argPresent:
|
||||
let zs = legacySuffix(sigs.zeroArgName)
|
||||
zeroApiSuffix = (if zs.len > 0: "_" & zs else: "_zero")
|
||||
let asfx = legacySuffix(sigs.argSigName)
|
||||
argApiSuffix = (if asfx.len > 0: "_" & asfx else: "_args")
|
||||
else:
|
||||
let sg = parseRequestSugar(body, "RequestBroker", async = true)
|
||||
typeIdent = sg.typeIdent
|
||||
payloadType = sg.payloadType
|
||||
parsed = sg.parsed
|
||||
zeroArgPresent = not sg.zeroArgProc.isNil
|
||||
argPresent = not sg.argProc.isNil
|
||||
argParams = sg.argParams
|
||||
if zeroArgPresent and argPresent:
|
||||
argApiSuffix = "_arg" # zero-arg stays bare
|
||||
|
||||
let typeName = sanitizeIdentName(typeIdent)
|
||||
let apiName = snakeApiName(typeIdent)
|
||||
|
||||
# reduced-A: does this request CREATE AND RETURN a sub-interface instance?
|
||||
# (Its Ok payload type is a registered BrokerInterface(API).) If so the wire
|
||||
# carries the sub-instance's ctx as a bare uint32 — we skip type registration
|
||||
# (the interface ref is never CBOR-encoded) and emit instance adapters below.
|
||||
let payloadName = payloadType.repr.strip()
|
||||
let returnsIface = (if isApiInterface(payloadName): payloadName else: "")
|
||||
|
||||
# Register the payload type in the schema so wrapper codegen can emit
|
||||
# typed structs / aliases. For the proc-sugar POD form this mirrors the
|
||||
# legacy `type X = <prim>` registration exactly (wire-identical).
|
||||
if returnsIface.len > 0:
|
||||
discard # instance-returning request: no payload type to register.
|
||||
elif parsed.hasInlineFields:
|
||||
registerCborObjectType(typeName, parsed.fieldNames, parsed.fieldTypes)
|
||||
elif parsed.isVoid:
|
||||
# `void` → a zero-field object: payload-less request, the response
|
||||
# envelope carries only the ok/err signal.
|
||||
registerCborObjectType(typeName, @[], @[])
|
||||
else:
|
||||
registerCborPrimitiveType(typeName, parsed)
|
||||
|
||||
# Materialise (paramName, nimType) pairs from the arg-based signature so
|
||||
# foreign-language wrapper codegen can emit a typed call signature.
|
||||
proc paramFields(argParams: seq[NimNode]): seq[(string, string)] {.compileTime.} =
|
||||
for paramDefs in argParams:
|
||||
let lastIdx = paramDefs.len - 1
|
||||
let typeNode = paramDefs[lastIdx - 1]
|
||||
let typeStr = typeNode.repr.strip()
|
||||
for nameIdx in 0 ..< lastIdx - 1:
|
||||
let nameNode = paramDefs[nameIdx]
|
||||
let nameStr =
|
||||
case nameNode.kind
|
||||
of nnkIdent, nnkSym:
|
||||
$nameNode
|
||||
of nnkPostfix:
|
||||
$nameNode[1]
|
||||
of nnkPragmaExpr:
|
||||
$nameNode[0]
|
||||
else:
|
||||
$nameNode
|
||||
result.add((nameStr, typeStr))
|
||||
|
||||
# 3. Emit adapters + register descriptors. Naming rule (replaces the old
|
||||
# `_zero`/`_args`): single signature → bare apiName; both slots present →
|
||||
# the zero-arg keeps the bare name, the arg-based gets the `_arg` suffix
|
||||
# (`<broker>Arg` in the foreign wrappers).
|
||||
if not zeroArgPresent and not argPresent:
|
||||
# No explicit signature — treat as zero-arg, matching the native default.
|
||||
let adapterIdent = ident(typeName & "CborAdapter")
|
||||
if returnsIface.len > 0:
|
||||
result.add(emitZeroArgInstanceAdapter(typeIdent, adapterIdent))
|
||||
else:
|
||||
result.add(
|
||||
emitZeroArgAdapter(typeIdent, payloadType, adapterIdent, parsed.isVoid)
|
||||
)
|
||||
registerCborRequestEntry(
|
||||
apiName, $adapterIdent, typeName, @[], returnsInterface = returnsIface
|
||||
)
|
||||
return
|
||||
|
||||
if zeroArgPresent:
|
||||
let zeroAdapterTag = if argPresent: "Zero" else: ""
|
||||
let adapterIdent = ident(typeName & "CborAdapter" & zeroAdapterTag)
|
||||
if returnsIface.len > 0:
|
||||
result.add(emitZeroArgInstanceAdapter(typeIdent, adapterIdent))
|
||||
else:
|
||||
result.add(
|
||||
emitZeroArgAdapter(typeIdent, payloadType, adapterIdent, parsed.isVoid)
|
||||
)
|
||||
registerCborRequestEntry(
|
||||
apiName & zeroApiSuffix,
|
||||
$adapterIdent,
|
||||
typeName,
|
||||
@[],
|
||||
returnsInterface = returnsIface,
|
||||
)
|
||||
|
||||
if argPresent:
|
||||
let argAdapterTag = if zeroArgPresent: "Args" else: ""
|
||||
let adapterIdent = ident(typeName & "CborAdapter" & argAdapterTag)
|
||||
let argsTypeIdent = ident(typeName & "CborArgs" & argAdapterTag)
|
||||
result.add(emitArgsType(argsTypeIdent, argParams))
|
||||
if returnsIface.len > 0:
|
||||
result.add(
|
||||
emitArgInstanceAdapter(typeIdent, adapterIdent, argsTypeIdent, argParams)
|
||||
)
|
||||
else:
|
||||
result.add(
|
||||
emitArgAdapter(
|
||||
typeIdent, payloadType, adapterIdent, argsTypeIdent, argParams, parsed.isVoid
|
||||
)
|
||||
)
|
||||
let fields = paramFields(argParams)
|
||||
registerCborRequestEntry(
|
||||
apiName & argApiSuffix,
|
||||
$adapterIdent,
|
||||
typeName,
|
||||
fields,
|
||||
returnsInterface = returnsIface,
|
||||
)
|
||||
|
||||
when defined(brokerDebug):
|
||||
writeBrokerDebug(
|
||||
"RequestBrokerApi", typeName, result, header = "apiName='" & apiName & "'"
|
||||
)
|
||||
when defined(brokerDebugStdout):
|
||||
echo "[brokers/cbor] RequestBroker(API) for '" & typeName & "' (apiName='" &
|
||||
apiName & "')"
|
||||
echo result.repr
|
||||
|
||||
{.pop.}
|
||||
|
||||
macro generateApiCborRequestBrokerDeferred*(args: varargs[untyped]): untyped =
|
||||
## Typed-phase deferred codegen entry point. By the time this expands,
|
||||
## any preceding `autoRegisterApiType` calls have already populated
|
||||
## `gApiTypeRegistry`, so wrapper codegen can introspect external
|
||||
## enum / distinct / object types without falling back to TODO stubs.
|
||||
##
|
||||
## Args layout: [body, kw0, kw1, ...]. Kwargs are forwarded as raw
|
||||
## `nnkExprEqExpr` nodes from `generateApiCborRequestBroker` and
|
||||
## re-parsed here into an MtReqCfg.
|
||||
if args.len == 0:
|
||||
error("generateApiCborRequestBrokerDeferred requires a body", args)
|
||||
let body = args[0]
|
||||
var kwargs: seq[NimNode]
|
||||
for i in 1 ..< args.len:
|
||||
kwargs.add(args[i])
|
||||
let cfg = parseMtReqKwargs(kwargs)
|
||||
generateApiCborRequestBrokerImpl(body, cfg)
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
proc generateApiCborRequestBroker*(body: NimNode, kwargs: seq[NimNode]): NimNode =
|
||||
## Two-phase entry point — mirrors the native
|
||||
## `generateApiRequestBroker` pattern. Kwargs are passed through the
|
||||
## deferred macro call as raw nodes so the typed-phase expansion sees
|
||||
## the original literal values for `parseMtReqKwargs`.
|
||||
result = newStmtList()
|
||||
|
||||
let externalIdents = discoverExternalTypes(body)
|
||||
if externalIdents.len > 0:
|
||||
result.add(emitAutoRegistrations(externalIdents))
|
||||
|
||||
let deferred =
|
||||
newCall(ident("generateApiCborRequestBrokerDeferred"), copyNimTree(body))
|
||||
for kw in kwargs:
|
||||
deferred.add(copyNimTree(kw))
|
||||
result.add(deferred)
|
||||
|
||||
{.pop.}
|
||||
@@ -0,0 +1,232 @@
|
||||
## api_schema
|
||||
## ----------
|
||||
## Compile-time type registry for FFI API code generation.
|
||||
##
|
||||
## This module provides a language-neutral schema that broker macros populate
|
||||
## and codegen modules consume. It supports objects, enums, type aliases,
|
||||
## and distinct types as first-class citizens.
|
||||
##
|
||||
## The registry stores type information for types used across the FFI boundary,
|
||||
## needed for encoding/decoding, C/C++ header generation, and nested type
|
||||
## marshalling.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[macros, strutils]
|
||||
|
||||
type
|
||||
ApiTypeKind* = enum ## Discriminator for registered types.
|
||||
atkObject ## Plain or ref object with fields
|
||||
atkEnum ## Nim enum type
|
||||
atkAlias ## Type alias (e.g. `type Timestamp = int64`)
|
||||
atkDistinct ## Distinct type (e.g. `type MyId = distinct int32`)
|
||||
|
||||
ApiEnumValue* = object ## A single value in an enum type.
|
||||
name*: string
|
||||
ordinal*: int
|
||||
|
||||
ApiFieldDef* = object ## A single field in a type definition.
|
||||
name*: string
|
||||
nimType*: string ## "int64", "string", "bool", "seq[DeviceInfo]", etc.
|
||||
isSeq*: bool ## true when nimType starts with "seq["
|
||||
seqElementType*: string ## e.g. "DeviceInfo" when isSeq
|
||||
isArray*: bool ## true when nimType is "array[N, T]"
|
||||
arraySize*: int ## e.g. 3 for array[3, int32]
|
||||
arrayElementType*: string ## e.g. "int32" for array[3, int32]
|
||||
isCustomObject*: bool ## true when type resolves to an object (not primitive)
|
||||
|
||||
ApiTypeEntry* = object ## A registered type in the FFI schema.
|
||||
name*: string ## "DeviceInfo"
|
||||
kind*: ApiTypeKind ## What kind of type this is
|
||||
fields*: seq[ApiFieldDef] ## field definitions (for atkObject)
|
||||
enumValues*: seq[ApiEnumValue] ## enum values (for atkEnum)
|
||||
underlyingType*: string ## base type (for atkAlias/atkDistinct)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compile-time type registry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var gApiTypeRegistry* {.compileTime.}: seq[ApiTypeEntry] = @[]
|
||||
## All types registered for FFI code generation.
|
||||
## Populated by auto-resolution (api_type_resolver) or legacy ApiType macro.
|
||||
## Consumed by codegen modules when processing seq[T] fields.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Primitive type detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const nimPrimitiveTypes* = [
|
||||
"string", "cstring", "char", "bool", "int", "int8", "int16", "int32", "int64", "uint",
|
||||
"uint8", "uint16", "uint32", "uint64", "float", "float32", "float64", "byte",
|
||||
]
|
||||
|
||||
proc isNimPrimitive*(typeName: string): bool {.compileTime.} =
|
||||
## Returns true if `typeName` is a built-in Nim primitive type.
|
||||
typeName.toLowerAscii() in nimPrimitiveTypes
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Registry operations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc isTypeRegistered*(name: string): bool {.compileTime.} =
|
||||
## Check if a type is already in the registry.
|
||||
for entry in gApiTypeRegistry:
|
||||
if entry.name == name:
|
||||
return true
|
||||
false
|
||||
|
||||
proc lookupTypeEntry*(name: string): ApiTypeEntry {.compileTime.} =
|
||||
## Lookup a type entry by name. Returns the entry or triggers a compile error.
|
||||
for entry in gApiTypeRegistry:
|
||||
if entry.name == name:
|
||||
return entry
|
||||
error(
|
||||
"Type '" & name & "' not registered in FFI schema. " &
|
||||
"Define it as a plain Nim type before using it in a broker macro, " &
|
||||
"or declare it with `ApiType:` for explicit registration."
|
||||
)
|
||||
|
||||
proc lookupTypeFields*(name: string): seq[(string, string)] {.compileTime.} =
|
||||
## Backward-compatible lookup returning (fieldName, nimTypeName) tuples.
|
||||
## This is the drop-in replacement for the old `lookupFfiStruct()`.
|
||||
let entry = lookupTypeEntry(name)
|
||||
for field in entry.fields:
|
||||
result.add((field.name, field.nimType))
|
||||
|
||||
proc registerTypeEntry*(entry: ApiTypeEntry) {.compileTime.} =
|
||||
## Register a type in the schema. Skips if already registered.
|
||||
if not isTypeRegistered(entry.name):
|
||||
gApiTypeRegistry.add(entry)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Query helpers for type kinds
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc isEnumRegistered*(name: string): bool {.compileTime.} =
|
||||
## Returns true if the name is registered as an enum type.
|
||||
for entry in gApiTypeRegistry:
|
||||
if entry.name == name and entry.kind == atkEnum:
|
||||
return true
|
||||
false
|
||||
|
||||
proc isAliasOrDistinctRegistered*(name: string): bool {.compileTime.} =
|
||||
## Returns true if the name is registered as an alias or distinct type.
|
||||
for entry in gApiTypeRegistry:
|
||||
if entry.name == name and entry.kind in {atkAlias, atkDistinct}:
|
||||
return true
|
||||
false
|
||||
|
||||
proc resolveUnderlyingType*(name: string): string {.compileTime.} =
|
||||
## Follows alias/distinct chains to the final underlying type name.
|
||||
## Returns the name itself if not registered as alias/distinct.
|
||||
var current = name
|
||||
var depth = 0
|
||||
while depth < 20: # safety limit
|
||||
var found = false
|
||||
for entry in gApiTypeRegistry:
|
||||
if entry.name == current and entry.kind in {atkAlias, atkDistinct}:
|
||||
current = entry.underlyingType
|
||||
found = true
|
||||
break
|
||||
if not found:
|
||||
break
|
||||
inc depth
|
||||
current
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Type node inspection helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc isSeqOfPrimitive*(nimType: NimNode): bool {.compileTime.} =
|
||||
## Returns true when nimType is `seq[T]` and T is a primitive type.
|
||||
if nimType.kind == nnkBracketExpr and nimType.len == 2 and
|
||||
($nimType[0]).toLowerAscii() == "seq":
|
||||
let elemName = $nimType[1]
|
||||
return isNimPrimitive(elemName)
|
||||
false
|
||||
|
||||
proc isArrayType*(nimType: NimNode): bool {.compileTime.} =
|
||||
## Returns true if the type node represents `array[N, T]`.
|
||||
nimType.kind == nnkBracketExpr and nimType.len == 3 and
|
||||
($nimType[0]).toLowerAscii() == "array"
|
||||
|
||||
proc arraySize*(nimType: NimNode): int {.compileTime.} =
|
||||
## Extracts N from `array[N, T]`. Expects an int literal.
|
||||
assert isArrayType(nimType)
|
||||
if nimType[1].kind == nnkIntLit:
|
||||
int(nimType[1].intVal)
|
||||
else:
|
||||
error("array size must be an integer literal for FFI codegen", nimType[1])
|
||||
|
||||
proc arrayElemTypeName*(nimType: NimNode): string {.compileTime.} =
|
||||
## Extracts the element type name from `array[N, T]`.
|
||||
assert isArrayType(nimType)
|
||||
$nimType[2]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Field construction helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc makeFieldDef*(name, nimType: string): ApiFieldDef {.compileTime.} =
|
||||
## Construct an ApiFieldDef from name and type strings.
|
||||
result.name = name
|
||||
result.nimType = nimType
|
||||
let lower = nimType.toLowerAscii()
|
||||
if lower.startsWith("seq[") and lower.endsWith("]"):
|
||||
result.isSeq = true
|
||||
result.seqElementType = nimType[4 ..^ 2] # strip "seq[" and "]"
|
||||
elif lower.startsWith("array["):
|
||||
# Parse "array[N, T]" format
|
||||
let inner = nimType[6 ..^ 2] # strip "array[" and "]"
|
||||
let commaPos = inner.find(',')
|
||||
if commaPos >= 0:
|
||||
result.isArray = true
|
||||
try:
|
||||
result.arraySize = parseInt(inner[0 ..< commaPos].strip())
|
||||
except ValueError:
|
||||
result.arraySize = 0
|
||||
result.arrayElementType = inner[commaPos + 1 .. ^1].strip()
|
||||
if not isNimPrimitive(nimType) and not result.isSeq and not result.isArray:
|
||||
result.isCustomObject = true
|
||||
|
||||
proc makeTypeEntry*(
|
||||
name: string, fields: seq[ApiFieldDef], kind: ApiTypeKind = atkObject
|
||||
): ApiTypeEntry {.compileTime.} =
|
||||
## Construct an ApiTypeEntry for an object type.
|
||||
result.name = name
|
||||
result.kind = kind
|
||||
result.fields = fields
|
||||
|
||||
proc makeEnumEntry*(
|
||||
name: string, values: seq[ApiEnumValue]
|
||||
): ApiTypeEntry {.compileTime.} =
|
||||
## Construct an ApiTypeEntry for an enum type.
|
||||
result.name = name
|
||||
result.kind = atkEnum
|
||||
result.enumValues = values
|
||||
|
||||
proc makeAliasEntry*(
|
||||
name: string, underlyingType: string, kind: ApiTypeKind = atkAlias
|
||||
): ApiTypeEntry {.compileTime.} =
|
||||
## Construct an ApiTypeEntry for an alias or distinct type.
|
||||
result.name = name
|
||||
result.kind = kind
|
||||
result.underlyingType = underlyingType
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Backward compatibility: bridge to old registration format
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc registerFromFieldTuples*(
|
||||
typeName: string, fields: seq[(string, string)]
|
||||
) {.compileTime.} =
|
||||
## Register a type from (fieldName, nimTypeName) tuples.
|
||||
## Used by the legacy ApiType macro and during migration.
|
||||
if isTypeRegistered(typeName):
|
||||
return
|
||||
var fieldDefs: seq[ApiFieldDef] = @[]
|
||||
for (fname, ftype) in fields:
|
||||
fieldDefs.add(makeFieldDef(fname, ftype))
|
||||
registerTypeEntry(makeTypeEntry(typeName, fieldDefs))
|
||||
|
||||
{.pop.}
|
||||
@@ -0,0 +1,455 @@
|
||||
## api_type_resolver
|
||||
## -----------------
|
||||
## Two-phase external type introspection for FFI API broker macros.
|
||||
##
|
||||
## When a broker macro encounters a reference to an external type (e.g.
|
||||
## `seq[DeviceInfo]` where `DeviceInfo` is a plain Nim type defined outside
|
||||
## the macro body), this module resolves its fields at compile time and
|
||||
## registers it in the API schema.
|
||||
##
|
||||
## ## Mechanism
|
||||
##
|
||||
## Phase 1 (called from an `untyped` broker macro):
|
||||
## `discoverExternalTypes(body)` scans the raw AST for type identifiers
|
||||
## that are not Nim primitives. Returns ident nodes.
|
||||
##
|
||||
## Phase 2 (typed macro expansion):
|
||||
## `autoRegisterApiType(T: typed)` receives a resolved type symbol,
|
||||
## calls `getTypeImpl()` to extract its fields, recursively resolves
|
||||
## nested object types, and registers everything in `gApiTypeRegistry`.
|
||||
##
|
||||
## ## Supported type kinds
|
||||
##
|
||||
## - `object` types — field introspection and CItem generation
|
||||
## - `enum` types — value extraction and C enum generation
|
||||
## - `distinct` types — base type resolution and C typedef generation
|
||||
## - Type aliases — base type resolution and C typedef generation
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[macros, strutils]
|
||||
import ./api_schema
|
||||
|
||||
export api_schema
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 2: Typed macro that resolves a single external type
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc resolveActualSym(T: NimNode): NimNode {.compileTime.} =
|
||||
## Get the actual type symbol regardless of how T was passed.
|
||||
## Handles both typedesc[X] (from typed parameter) and direct symbols
|
||||
## (from recursive calls within the typed phase).
|
||||
let impl = getTypeImpl(T)
|
||||
case impl.kind
|
||||
of nnkBracketExpr:
|
||||
# typedesc[X] -> return X
|
||||
if impl.len >= 2:
|
||||
impl[1]
|
||||
else:
|
||||
nil
|
||||
of nnkObjectTy:
|
||||
# Already resolved; T itself is the symbol
|
||||
T
|
||||
of nnkEnumTy:
|
||||
T
|
||||
of nnkDistinctTy:
|
||||
T
|
||||
of nnkSym:
|
||||
T
|
||||
else:
|
||||
nil
|
||||
|
||||
proc extractFieldsFromSym(sym: NimNode): seq[(string, string)] {.compileTime.} =
|
||||
## Extract (fieldName, fieldTypeName) from a resolved type symbol.
|
||||
let typeImpl = getTypeImpl(sym)
|
||||
let obj =
|
||||
if typeImpl.kind == nnkObjectTy:
|
||||
typeImpl
|
||||
elif typeImpl.kind == nnkBracketExpr and typeImpl.len >= 2:
|
||||
getTypeImpl(typeImpl[1])
|
||||
else:
|
||||
nil
|
||||
|
||||
if obj.isNil or obj.kind != nnkObjectTy:
|
||||
return @[]
|
||||
|
||||
let recList = obj[2]
|
||||
if recList.kind != nnkRecList:
|
||||
return @[]
|
||||
|
||||
for field in recList:
|
||||
if field.kind != nnkIdentDefs:
|
||||
continue
|
||||
let fieldType = field[field.len - 2]
|
||||
if fieldType.kind == nnkEmpty:
|
||||
continue
|
||||
for i in 0 ..< field.len - 2:
|
||||
if field[i].kind == nnkEmpty:
|
||||
continue
|
||||
result.add(($field[i], repr(fieldType)))
|
||||
|
||||
proc extractEnumValues(sym: NimNode): seq[(string, int)] {.compileTime.} =
|
||||
## Walk nnkEnumTy children to get (name, ordinal) pairs.
|
||||
let typeImpl = getTypeImpl(sym)
|
||||
let enumTy =
|
||||
if typeImpl.kind == nnkEnumTy:
|
||||
typeImpl
|
||||
elif typeImpl.kind == nnkBracketExpr and typeImpl.len >= 2:
|
||||
getTypeImpl(typeImpl[1])
|
||||
else:
|
||||
nil
|
||||
|
||||
if enumTy.isNil or enumTy.kind != nnkEnumTy:
|
||||
return @[]
|
||||
|
||||
var ordinal = 0
|
||||
for i in 1 ..< enumTy.len: # skip first child (empty node)
|
||||
let child = enumTy[i]
|
||||
case child.kind
|
||||
of nnkSym:
|
||||
result.add(($child, ordinal))
|
||||
inc ordinal
|
||||
of nnkEnumFieldDef:
|
||||
let fieldName = $child[0]
|
||||
let fieldVal = int(child[1].intVal)
|
||||
result.add((fieldName, fieldVal))
|
||||
ordinal = fieldVal + 1
|
||||
else:
|
||||
discard
|
||||
|
||||
const tuplePositionalNames* =
|
||||
["first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth", "ninth"]
|
||||
## Synthesised field names for unnamed positional tuple elements.
|
||||
## Tuples with more than 9 positional elements are rejected by the FFI
|
||||
## generator — wrap them in a named `object` instead.
|
||||
|
||||
proc extractFieldsFromTupleSym(sym: NimNode): seq[(string, string)] {.compileTime.} =
|
||||
## Extract `(fieldName, fieldTypeName)` pairs from a resolved tuple type
|
||||
## symbol. Named tuples like `tuple[key: Key, payload: seq[byte]]` use the
|
||||
## declared field names verbatim. Unnamed positional tuples up to 9
|
||||
## elements receive synthesised names from `tuplePositionalNames`.
|
||||
let typeImpl = getTypeImpl(sym)
|
||||
let tupleTy = if typeImpl.kind == nnkTupleTy: typeImpl else: nil
|
||||
if tupleTy.isNil:
|
||||
return @[]
|
||||
|
||||
var posIdx = 0
|
||||
for child in tupleTy:
|
||||
if child.kind == nnkIdentDefs:
|
||||
let typeNode = child[child.len - 2]
|
||||
for i in 0 ..< child.len - 2:
|
||||
let rawName = $child[i]
|
||||
result.add((rawName, typeNode.repr.strip()))
|
||||
else:
|
||||
if posIdx >= tuplePositionalNames.len:
|
||||
error(
|
||||
"FFI tuple support is limited to 9 positional fields; got element " &
|
||||
$(posIdx + 1) & " of tuple " & $sym & ". Wrap in a named object instead.",
|
||||
sym,
|
||||
)
|
||||
result.add((tuplePositionalNames[posIdx], child.repr.strip()))
|
||||
inc posIdx
|
||||
|
||||
proc collectNestedTypeNodesFromTuple(sym: NimNode): seq[NimNode] {.compileTime.} =
|
||||
## Tuple-shaped analogue of `collectNestedTypeNodes` — walks a resolved
|
||||
## tuple type's fields and returns NimNodes for any nested custom types
|
||||
## (object / enum / distinct / alias / seq[Custom] / array[N, Custom])
|
||||
## that need recursive registration.
|
||||
let typeImpl = getTypeImpl(sym)
|
||||
let tupleTy = if typeImpl.kind == nnkTupleTy: typeImpl else: nil
|
||||
if tupleTy.isNil:
|
||||
return @[]
|
||||
|
||||
proc handleFieldType(fieldType: NimNode, acc: var seq[NimNode]) =
|
||||
if fieldType.kind == nnkSym and not isNimPrimitive($fieldType):
|
||||
let innerImpl = getTypeImpl(fieldType)
|
||||
if innerImpl.kind in {nnkObjectTy, nnkEnumTy, nnkDistinctTy, nnkTupleTy}:
|
||||
acc.add(fieldType)
|
||||
else:
|
||||
let instName = $getTypeInst(fieldType)
|
||||
if instName != $fieldType and not isNimPrimitive(instName):
|
||||
acc.add(fieldType)
|
||||
elif fieldType.kind == nnkBracketExpr and fieldType.len >= 2 and
|
||||
$fieldType[0] == "seq":
|
||||
let elemSym = fieldType[1]
|
||||
if elemSym.kind == nnkSym and not isNimPrimitive($elemSym):
|
||||
let elemImpl = getTypeImpl(elemSym)
|
||||
if elemImpl.kind in {nnkObjectTy, nnkEnumTy, nnkTupleTy, nnkDistinctTy}:
|
||||
acc.add(elemSym)
|
||||
elif fieldType.kind == nnkBracketExpr and fieldType.len == 3 and
|
||||
$fieldType[0] == "array":
|
||||
let elemSym = fieldType[2]
|
||||
if elemSym.kind == nnkSym and not isNimPrimitive($elemSym):
|
||||
let elemImpl = getTypeImpl(elemSym)
|
||||
if elemImpl.kind in {nnkObjectTy, nnkEnumTy, nnkTupleTy, nnkDistinctTy}:
|
||||
acc.add(elemSym)
|
||||
|
||||
for child in tupleTy:
|
||||
if child.kind == nnkIdentDefs:
|
||||
let typeNode = child[child.len - 2]
|
||||
handleFieldType(typeNode, result)
|
||||
else:
|
||||
handleFieldType(child, result)
|
||||
|
||||
proc resolveAliasBase(sym: NimNode): string {.compileTime.} =
|
||||
## Follows alias/distinct chains to the underlying primitive name.
|
||||
let typeImpl = getTypeImpl(sym)
|
||||
if typeImpl.kind == nnkDistinctTy:
|
||||
let base = typeImpl[0]
|
||||
# `$` panics on non-symbol nodes (e.g. nnkBracketExpr for
|
||||
# `distinct seq[byte]`); `repr` accepts any AST shape and yields
|
||||
# the same printable form for symbols.
|
||||
return base.repr.strip()
|
||||
# For aliases, getTypeInst gives us the target
|
||||
let typeInst = getTypeInst(sym)
|
||||
if typeInst.kind == nnkBracketExpr and typeInst.len >= 2:
|
||||
return typeInst[1].repr.strip()
|
||||
if typeInst.kind == nnkSym:
|
||||
return $typeInst
|
||||
return sym.repr.strip()
|
||||
|
||||
proc collectNestedTypeNodes(sym: NimNode): seq[NimNode] {.compileTime.} =
|
||||
## Walk the fields of a resolved type symbol and return NimNodes for
|
||||
## any nested custom object types or seq[T] element types that need
|
||||
## recursive registration.
|
||||
let typeImpl = getTypeImpl(sym)
|
||||
let obj =
|
||||
if typeImpl.kind == nnkObjectTy:
|
||||
typeImpl
|
||||
elif typeImpl.kind == nnkBracketExpr and typeImpl.len >= 2:
|
||||
getTypeImpl(typeImpl[1])
|
||||
else:
|
||||
nil
|
||||
|
||||
if obj.isNil or obj.kind != nnkObjectTy:
|
||||
return @[]
|
||||
|
||||
let recList = obj[2]
|
||||
if recList.kind != nnkRecList:
|
||||
return @[]
|
||||
|
||||
for field in recList:
|
||||
if field.kind != nnkIdentDefs:
|
||||
continue
|
||||
let fieldType = field[field.len - 2]
|
||||
if fieldType.kind == nnkEmpty:
|
||||
continue
|
||||
|
||||
# Direct custom object field (e.g. `address: Address`)
|
||||
if fieldType.kind == nnkSym and not isNimPrimitive($fieldType):
|
||||
let innerImpl = getTypeImpl(fieldType)
|
||||
if innerImpl.kind == nnkObjectTy:
|
||||
result.add(fieldType)
|
||||
elif innerImpl.kind == nnkEnumTy:
|
||||
result.add(fieldType)
|
||||
elif innerImpl.kind == nnkDistinctTy:
|
||||
result.add(fieldType)
|
||||
elif innerImpl.kind == nnkTupleTy:
|
||||
result.add(fieldType)
|
||||
else:
|
||||
# Could be an alias — check if it resolves to something different
|
||||
let instName = $getTypeInst(fieldType)
|
||||
if instName != $fieldType and not isNimPrimitive(instName):
|
||||
result.add(fieldType)
|
||||
|
||||
# seq[T] where T is a custom type (e.g. `devices: seq[DeviceInfo]`)
|
||||
elif fieldType.kind == nnkBracketExpr and fieldType.len >= 2 and
|
||||
$fieldType[0] == "seq":
|
||||
let elemSym = fieldType[1]
|
||||
if elemSym.kind == nnkSym and not isNimPrimitive($elemSym):
|
||||
let elemImpl = getTypeImpl(elemSym)
|
||||
if elemImpl.kind in {nnkObjectTy, nnkEnumTy, nnkTupleTy, nnkDistinctTy}:
|
||||
result.add(elemSym)
|
||||
|
||||
# array[N, T] where T is a custom type
|
||||
elif fieldType.kind == nnkBracketExpr and fieldType.len == 3 and
|
||||
$fieldType[0] == "array":
|
||||
let elemSym = fieldType[2]
|
||||
if elemSym.kind == nnkSym and not isNimPrimitive($elemSym):
|
||||
let elemImpl = getTypeImpl(elemSym)
|
||||
if elemImpl.kind in {nnkObjectTy, nnkEnumTy, nnkTupleTy, nnkDistinctTy}:
|
||||
result.add(elemSym)
|
||||
|
||||
macro autoRegisterApiType*(T: typed): untyped =
|
||||
## Phase 2: Receives a resolved type symbol, extracts fields,
|
||||
## recursively processes nested types, registers in the schema,
|
||||
## and generates CItem type + encode proc + C/C++/Python codegen.
|
||||
##
|
||||
## Handles object types (full CItem generation), enum types (C enum
|
||||
## generation), and alias/distinct types (C typedef generation).
|
||||
result = newStmtList()
|
||||
|
||||
let actualSym = resolveActualSym(T)
|
||||
if actualSym.isNil:
|
||||
return result
|
||||
|
||||
let typeName = $actualSym
|
||||
if isTypeRegistered(typeName) or isNimPrimitive(typeName):
|
||||
return result
|
||||
|
||||
let typeImpl = getTypeImpl(actualSym)
|
||||
|
||||
# Check for enum types
|
||||
block checkEnum:
|
||||
let enumTy =
|
||||
if typeImpl.kind == nnkEnumTy:
|
||||
typeImpl
|
||||
elif typeImpl.kind == nnkBracketExpr and typeImpl.len >= 2:
|
||||
let inner = getTypeImpl(typeImpl[1])
|
||||
if inner.kind == nnkEnumTy: inner else: nil
|
||||
else:
|
||||
nil
|
||||
if not enumTy.isNil:
|
||||
let values = extractEnumValues(actualSym)
|
||||
var apiValues: seq[ApiEnumValue] = @[]
|
||||
for (name, ordinal) in values:
|
||||
apiValues.add(ApiEnumValue(name: name, ordinal: ordinal))
|
||||
registerTypeEntry(makeEnumEntry(typeName, apiValues))
|
||||
return result
|
||||
|
||||
# Check for distinct types
|
||||
if typeImpl.kind == nnkDistinctTy:
|
||||
let baseName = resolveAliasBase(actualSym)
|
||||
registerTypeEntry(makeAliasEntry(typeName, baseName, atkDistinct))
|
||||
return result
|
||||
|
||||
# Check for alias types (sym that resolves to another sym/primitive)
|
||||
block checkAlias:
|
||||
let typeInst = getTypeInst(actualSym)
|
||||
if typeInst.kind == nnkBracketExpr and typeInst.len >= 2:
|
||||
let targetName = $typeInst[1]
|
||||
if targetName != typeName:
|
||||
registerTypeEntry(makeAliasEntry(typeName, targetName, atkAlias))
|
||||
return result
|
||||
|
||||
# Tuple types — register as a synthesised object so the CBOR codegen
|
||||
# modules (which iterate `gApiTypeRegistry` for `atkObject` entries)
|
||||
# pick the tuple up and emit struct definitions. Named tuples keep
|
||||
# their declared field names; unnamed positional tuples up to 9
|
||||
# elements receive `first`..`ninth`.
|
||||
#
|
||||
# Note: we deliberately DO NOT call `generateApiType` here. That path
|
||||
# emits a fixed-layout `<Name>CItem` for the native ABI which has no
|
||||
# count-companion field for `seq[T]` members — so a tuple like
|
||||
# `tuple[a: Key, b: seq[byte]]` cannot fit. Native-ABI tuple support
|
||||
# belongs to a follow-up task; for now the CBOR-mode codegen runs off
|
||||
# the schema entry alone, and the native codegen sees an object with
|
||||
# a missing CItem and falls back to its own TODO emission for
|
||||
# downstream wrappers (which is what existing native-uncovered shapes
|
||||
# like `seq[Object<seq>]` already do).
|
||||
if typeImpl.kind == nnkTupleTy:
|
||||
let tupleFields = extractFieldsFromTupleSym(actualSym)
|
||||
if tupleFields.len == 0:
|
||||
return result
|
||||
let nestedNodesT = collectNestedTypeNodesFromTuple(actualSym)
|
||||
for nestedSym in nestedNodesT:
|
||||
let nestedName = $nestedSym
|
||||
if not isTypeRegistered(nestedName) and not isNimPrimitive(nestedName):
|
||||
result.add(newCall(ident("autoRegisterApiType"), nestedSym))
|
||||
registerFromFieldTuples(typeName, tupleFields)
|
||||
# Bind a map-shaped CBOR encoder/decoder so the wire matches the
|
||||
# named struct that wrappers emit for the same tuple type. The
|
||||
# default `write[T: tuple]` in cbor_serialization writes positional
|
||||
# CBOR arrays which decode wrappers reject as "expected map".
|
||||
result.add(newCall(ident("bindCborTupleMap"), actualSym))
|
||||
return result
|
||||
|
||||
# Object types — existing behavior
|
||||
let fields = extractFieldsFromSym(actualSym)
|
||||
if fields.len == 0:
|
||||
return result
|
||||
|
||||
# Emit recursive calls for nested types (depth-first: dependencies first)
|
||||
let nestedNodes = collectNestedTypeNodes(actualSym)
|
||||
for nestedSym in nestedNodes:
|
||||
let nestedName = $nestedSym
|
||||
if not isTypeRegistered(nestedName) and not isNimPrimitive(nestedName):
|
||||
result.add(newCall(ident("autoRegisterApiType"), nestedSym))
|
||||
|
||||
# Register this type in the schema; CBOR codegen reads gApiTypeRegistry.
|
||||
registerFromFieldTuples(typeName, fields)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 1: Scan untyped AST for external type references
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc scanTypeNode(ft: NimNode, result: var seq[NimNode]) {.compileTime.} =
|
||||
## Check a single type node for external type references.
|
||||
## Adds ident nodes for `seq[T]`, `array[N, T]`, and plain custom types.
|
||||
if ft.kind == nnkEmpty:
|
||||
return
|
||||
# seq[T]
|
||||
if ft.kind == nnkBracketExpr and ft.len >= 2 and $ft[0] == "seq":
|
||||
let elemName = $ft[1]
|
||||
if not isNimPrimitive(elemName):
|
||||
result.add(ft[1])
|
||||
# array[N, T]
|
||||
elif ft.kind == nnkBracketExpr and ft.len == 3 and $ft[0] == "array":
|
||||
let elemName = $ft[2]
|
||||
if not isNimPrimitive(elemName):
|
||||
result.add(ft[2])
|
||||
# Plain custom type
|
||||
elif ft.kind == nnkIdent and not isNimPrimitive($ft):
|
||||
result.add(ft)
|
||||
|
||||
proc discoverExternalTypes*(body: NimNode): seq[NimNode] {.compileTime.} =
|
||||
## Scan an untyped macro body for references to external types.
|
||||
## Returns ident nodes for each type that needs resolution.
|
||||
##
|
||||
## Detects:
|
||||
## - `seq[T]` fields in type definitions where T is not a primitive
|
||||
## - `array[N, T]` fields where T is not a primitive
|
||||
## - Plain custom type fields (`field: CustomType`)
|
||||
## - Type aliases (`type MyEvent = ExternalType`)
|
||||
## - `seq[T]` and custom types in proc signature parameters
|
||||
var seen: seq[string] = @[]
|
||||
|
||||
for stmt in body:
|
||||
if stmt.kind == nnkTypeSection:
|
||||
for def in stmt:
|
||||
if def.kind != nnkTypeDef:
|
||||
continue
|
||||
let rhs = def[2]
|
||||
|
||||
if rhs.kind == nnkObjectTy:
|
||||
# Inline object: scan fields
|
||||
let recList = rhs[2]
|
||||
if recList.kind != nnkRecList:
|
||||
continue
|
||||
for field in recList:
|
||||
if field.kind != nnkIdentDefs:
|
||||
continue
|
||||
let ft = field[field.len - 2]
|
||||
scanTypeNode(ft, result)
|
||||
elif rhs.kind == nnkIdent:
|
||||
# Type alias: `type MyEvent = ExternalType`
|
||||
let aliasTarget = $rhs
|
||||
if not isNimPrimitive(aliasTarget):
|
||||
result.add(rhs)
|
||||
elif stmt.kind == nnkProcDef:
|
||||
# Scan proc signature parameters for external types
|
||||
let params = stmt.params
|
||||
for i in 1 ..< params.len:
|
||||
let paramDef = params[i]
|
||||
if paramDef.kind == nnkIdentDefs:
|
||||
let ft = paramDef[paramDef.len - 2]
|
||||
scanTypeNode(ft, result)
|
||||
|
||||
# Deduplicate (keep first occurrence)
|
||||
var deduped: seq[NimNode] = @[]
|
||||
for node in result:
|
||||
let name = $node
|
||||
if name notin seen:
|
||||
seen.add(name)
|
||||
deduped.add(node)
|
||||
result = deduped
|
||||
|
||||
proc emitAutoRegistrations*(externalIdents: seq[NimNode]): NimNode {.compileTime.} =
|
||||
## Generate `autoRegisterApiType(Ident)` calls for discovered external types.
|
||||
## These compile as typed macro invocations, triggering Phase 2 resolution.
|
||||
result = newStmtList()
|
||||
for typeIdent in externalIdents:
|
||||
result.add(newCall(ident("autoRegisterApiType"), typeIdent))
|
||||
|
||||
{.pop.}
|
||||
@@ -0,0 +1,111 @@
|
||||
## Broker macro debug-dump helper
|
||||
## ===============================
|
||||
## Active when client code compiles with `-d:brokerDebug`. The broker
|
||||
## macros call `writeBrokerDebug(...)` instead of (or in addition to)
|
||||
## `echo result.repr`, dumping the generated Nim AST — rendered back
|
||||
## to Nim source — into per-broker files for offline examination.
|
||||
##
|
||||
## Output layout (default):
|
||||
##
|
||||
## build/broker_debug/
|
||||
## ├── InitializeRequest__RequestBrokerApi.gen.nim
|
||||
## ├── ShutdownRequest__RequestBrokerApi.gen.nim
|
||||
## ├── DeviceStatusChanged__EventBrokerApi.gen.nim
|
||||
## ├── PerfData__RequestBrokerMt.gen.nim
|
||||
## ├── …
|
||||
## └── mylib__BrokerLibrary.gen.nim ← `registerBrokerLibrary`
|
||||
## (FFI C-ABI surface +
|
||||
## courier/lifecycle plumbing)
|
||||
##
|
||||
## Override the directory with `-d:brokerDebugDir=<path>`. The
|
||||
## directory is created on demand. Files are overwritten — stale
|
||||
## entries from prior builds are NOT auto-cleaned (delete the dir
|
||||
## before a build if you want a fresh snapshot).
|
||||
##
|
||||
## To preserve the historical "echo result.repr" behaviour alongside
|
||||
## the file dump, add `-d:brokerDebugStdout`. By default the dump is
|
||||
## file-only so the build log isn't drowned in generated Nim.
|
||||
##
|
||||
## The helper is a `{.compileTime.}` proc — it runs in the Nim VM
|
||||
## during macro expansion, the same way the C++/Python/Rust/Go
|
||||
## wrapper codegens write their output files.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[macros, os, strutils]
|
||||
|
||||
const brokerDebugDirOverride {.strdefine: "brokerDebugDir".}: string = ""
|
||||
|
||||
proc brokerDebugDir*(): string {.compileTime.} =
|
||||
## Directory under which dump files are written. Override via
|
||||
## `-d:brokerDebugDir=<path>`.
|
||||
if brokerDebugDirOverride.len > 0: brokerDebugDirOverride else: "build/broker_debug"
|
||||
|
||||
proc sanitizeFileNamePart(s: string): string {.compileTime.} =
|
||||
## Coerce `s` to a portable filename fragment. Conservative —
|
||||
## anything outside `[A-Za-z0-9_-]` becomes `_`.
|
||||
result = newStringOfCap(s.len)
|
||||
for c in s:
|
||||
if c in {'a' .. 'z', 'A' .. 'Z', '0' .. '9', '_', '-'}:
|
||||
result.add(c)
|
||||
else:
|
||||
result.add('_')
|
||||
|
||||
proc writeBrokerDebug*(
|
||||
role: string, typeName: string, generated: NimNode, header: string = ""
|
||||
) {.compileTime.} =
|
||||
## Dump the macro-generated AST for one broker (or the
|
||||
## `registerBrokerLibrary` output) into a per-broker file under
|
||||
## `brokerDebugDir()`.
|
||||
##
|
||||
## - `role` — e.g. "RequestBrokerApi" / "EventBrokerMt" /
|
||||
## "BrokerLibrary". Used in the filename suffix and
|
||||
## in the file header.
|
||||
## - `typeName` — the broker type name (or library name for
|
||||
## `BrokerLibrary`). Used as the filename stem.
|
||||
## - `generated` — the macro's `result` NimNode; its `.repr` is
|
||||
## written verbatim after a small comment header.
|
||||
## - `header` — optional one-line context note (e.g.
|
||||
## "apiName='initialize_request'").
|
||||
##
|
||||
## Errors are reported via `echo` and the proc returns; we do NOT
|
||||
## raise into the compilation. A failed dump is a diagnostic loss,
|
||||
## not a build failure.
|
||||
let dir = brokerDebugDir()
|
||||
try:
|
||||
createDir(dir)
|
||||
except OSError as e:
|
||||
echo "[brokers/debug] createDir('", dir, "') failed: ", e.msg, " — skipping dump."
|
||||
return
|
||||
except IOError as e:
|
||||
echo "[brokers/debug] createDir('", dir, "') failed: ", e.msg, " — skipping dump."
|
||||
return
|
||||
except CatchableError as e:
|
||||
echo "[brokers/debug] createDir('", dir, "') failed: ", e.msg, " — skipping dump."
|
||||
return
|
||||
|
||||
let safeName = sanitizeFileNamePart(typeName)
|
||||
let safeRole = sanitizeFileNamePart(role)
|
||||
let path = dir & "/" & safeName & "__" & safeRole & ".gen.nim"
|
||||
|
||||
var s = newStringOfCap(4096)
|
||||
s.add("## Auto-generated by nim-brokers macro expansion under -d:brokerDebug.\n")
|
||||
s.add("## DO NOT EDIT — this file reflects the AST the macro emits,\n")
|
||||
s.add("## rendered back to Nim source for offline examination.\n")
|
||||
s.add("##\n")
|
||||
s.add("## Role: " & role & "\n")
|
||||
s.add("## Type: " & typeName & "\n")
|
||||
if header.len > 0:
|
||||
s.add("## Notes: " & header & "\n")
|
||||
s.add("##\n")
|
||||
s.add("## Open in your editor or pipe through `nph` for nicer formatting.\n\n")
|
||||
s.add(generated.repr)
|
||||
if not s.endsWith("\n"):
|
||||
s.add("\n")
|
||||
|
||||
try:
|
||||
writeFile(path, s)
|
||||
except IOError as e:
|
||||
echo "[brokers/debug] writeFile('", path, "') failed: ", e.msg
|
||||
|
||||
{.pop.}
|
||||
@@ -0,0 +1,547 @@
|
||||
import std/[macros, strutils]
|
||||
|
||||
type ParsedBrokerType* = object
|
||||
## Result of parsing the single `type` definition inside a broker macro body.
|
||||
##
|
||||
## - `typeIdent`: base identifier for the declared type name
|
||||
## - `objectDef`: exported type definition RHS (inline object fields exported;
|
||||
## non-object types wrapped in `distinct` unless already distinct)
|
||||
## - `isRefObject`: true only for inline `ref object` definitions
|
||||
## - `hasInlineFields`: true for inline `object` / `ref object`
|
||||
## - `fieldNames`/`fieldTypes`: populated only when `collectFieldInfo = true`
|
||||
typeIdent*: NimNode
|
||||
objectDef*: NimNode
|
||||
isRefObject*: bool
|
||||
hasInlineFields*: bool
|
||||
isVoid*: bool ## true when the declared RHS is the bare `void` type
|
||||
fieldNames*: seq[NimNode]
|
||||
fieldTypes*: seq[NimNode]
|
||||
|
||||
proc toSnakeCase*(name: string): string {.compileTime.} =
|
||||
## Converts PascalCase / camelCase to snake_case. Shared between the
|
||||
## CBOR codegen surface and any kept compile-time helper that needs to
|
||||
## derive a wire name from a Nim identifier.
|
||||
result = ""
|
||||
for i, ch in name:
|
||||
if ch in {'A' .. 'Z'}:
|
||||
if i > 0 and name[i - 1] notin {'A' .. 'Z', '_'}:
|
||||
result.add('_')
|
||||
result.add(chr(ord(ch) + 32))
|
||||
else:
|
||||
result.add(ch)
|
||||
|
||||
proc sanitizeIdentName*(node: NimNode): string =
|
||||
var raw = $node
|
||||
var sanitizedName = newStringOfCap(raw.len)
|
||||
for ch in raw:
|
||||
case ch
|
||||
of 'A' .. 'Z', 'a' .. 'z', '0' .. '9', '_':
|
||||
sanitizedName.add(ch)
|
||||
else:
|
||||
sanitizedName.add('_')
|
||||
sanitizedName
|
||||
|
||||
proc ensureFieldDef*(node: NimNode) =
|
||||
if node.kind != nnkIdentDefs or node.len < 3:
|
||||
error("Expected field definition of the form `name: Type`", node)
|
||||
let typeSlot = node.len - 2
|
||||
if node[typeSlot].kind == nnkEmpty:
|
||||
error("Field `" & $node[0] & "` must declare a type", node)
|
||||
|
||||
proc exportIdentNode*(node: NimNode): NimNode =
|
||||
case node.kind
|
||||
of nnkIdent:
|
||||
postfix(copyNimTree(node), "*")
|
||||
of nnkPostfix:
|
||||
node
|
||||
else:
|
||||
error("Unsupported identifier form in field definition", node)
|
||||
|
||||
proc baseTypeIdent*(defName: NimNode): NimNode =
|
||||
case defName.kind
|
||||
of nnkIdent:
|
||||
defName
|
||||
of nnkAccQuoted:
|
||||
if defName.len != 1:
|
||||
error("Unsupported quoted identifier", defName)
|
||||
defName[0]
|
||||
of nnkPostfix:
|
||||
baseTypeIdent(defName[1])
|
||||
of nnkPragmaExpr:
|
||||
baseTypeIdent(defName[0])
|
||||
else:
|
||||
error("Unsupported type name in broker definition", defName)
|
||||
|
||||
proc ensureDistinctType*(rhs: NimNode): NimNode =
|
||||
## For PODs / aliases / externally-defined types, wrap in `distinct` unless
|
||||
## it's already distinct.
|
||||
if rhs.kind == nnkDistinctTy:
|
||||
return copyNimTree(rhs)
|
||||
newTree(nnkDistinctTy, copyNimTree(rhs))
|
||||
|
||||
proc cloneParams*(params: seq[NimNode]): seq[NimNode] =
|
||||
## Deep copy parameter definitions so they can be inserted in multiple places.
|
||||
result = @[]
|
||||
for param in params:
|
||||
result.add(copyNimTree(param))
|
||||
|
||||
proc collectParamNames*(params: seq[NimNode]): seq[NimNode] =
|
||||
## Extract all identifier symbols declared across IdentDefs nodes.
|
||||
result = @[]
|
||||
for param in params:
|
||||
assert param.kind == nnkIdentDefs
|
||||
for i in 0 ..< param.len - 2:
|
||||
let nameNode = param[i]
|
||||
if nameNode.kind == nnkEmpty:
|
||||
continue
|
||||
result.add(ident($nameNode))
|
||||
|
||||
proc parseOneTypeDef(
|
||||
def: NimNode,
|
||||
macroName: string,
|
||||
allowRefToNonObject = false,
|
||||
collectFieldInfo = false,
|
||||
): ParsedBrokerType =
|
||||
## Parse a single nnkTypeDef node into a ParsedBrokerType.
|
||||
## Internal helper used by both parseSingleTypeDef and parseTypeDefs.
|
||||
var fieldNames: seq[NimNode] = @[]
|
||||
var fieldTypes: seq[NimNode] = @[]
|
||||
|
||||
let typeIdent = baseTypeIdent(def[0])
|
||||
let rhs = def[2]
|
||||
var objectDef: NimNode
|
||||
var isRefObject = false
|
||||
var hasInlineFields = false
|
||||
var isVoid = false
|
||||
|
||||
case rhs.kind
|
||||
of nnkObjectTy:
|
||||
let recList = rhs[2]
|
||||
if recList.kind != nnkRecList:
|
||||
error(macroName & " object must declare a standard field list", rhs)
|
||||
var exportedRecList = newTree(nnkRecList)
|
||||
for field in recList:
|
||||
case field.kind
|
||||
of nnkIdentDefs:
|
||||
ensureFieldDef(field)
|
||||
if collectFieldInfo:
|
||||
let fieldTypeNode = field[field.len - 2]
|
||||
for i in 0 ..< field.len - 2:
|
||||
let baseFieldIdent = baseTypeIdent(field[i])
|
||||
fieldNames.add(copyNimTree(baseFieldIdent))
|
||||
fieldTypes.add(copyNimTree(fieldTypeNode))
|
||||
var cloned = copyNimTree(field)
|
||||
for i in 0 ..< cloned.len - 2:
|
||||
cloned[i] = exportIdentNode(cloned[i])
|
||||
exportedRecList.add(cloned)
|
||||
of nnkEmpty:
|
||||
discard
|
||||
else:
|
||||
error(
|
||||
macroName & " object definition only supports simple field declarations",
|
||||
field,
|
||||
)
|
||||
objectDef =
|
||||
newTree(nnkObjectTy, copyNimTree(rhs[0]), copyNimTree(rhs[1]), exportedRecList)
|
||||
isRefObject = false
|
||||
hasInlineFields = true
|
||||
of nnkRefTy:
|
||||
if rhs.len != 1:
|
||||
error(macroName & " ref type must have a single base", rhs)
|
||||
if rhs[0].kind == nnkObjectTy:
|
||||
let obj = rhs[0]
|
||||
let recList = obj[2]
|
||||
if recList.kind != nnkRecList:
|
||||
error(macroName & " object must declare a standard field list", obj)
|
||||
var exportedRecList = newTree(nnkRecList)
|
||||
for field in recList:
|
||||
case field.kind
|
||||
of nnkIdentDefs:
|
||||
ensureFieldDef(field)
|
||||
if collectFieldInfo:
|
||||
let fieldTypeNode = field[field.len - 2]
|
||||
for i in 0 ..< field.len - 2:
|
||||
let baseFieldIdent = baseTypeIdent(field[i])
|
||||
fieldNames.add(copyNimTree(baseFieldIdent))
|
||||
fieldTypes.add(copyNimTree(fieldTypeNode))
|
||||
var cloned = copyNimTree(field)
|
||||
for i in 0 ..< cloned.len - 2:
|
||||
cloned[i] = exportIdentNode(cloned[i])
|
||||
exportedRecList.add(cloned)
|
||||
of nnkEmpty:
|
||||
discard
|
||||
else:
|
||||
error(
|
||||
macroName & " object definition only supports simple field declarations",
|
||||
field,
|
||||
)
|
||||
let exportedObjectType =
|
||||
newTree(nnkObjectTy, copyNimTree(obj[0]), copyNimTree(obj[1]), exportedRecList)
|
||||
objectDef = newTree(nnkRefTy, exportedObjectType)
|
||||
isRefObject = true
|
||||
hasInlineFields = true
|
||||
elif allowRefToNonObject:
|
||||
## `ref SomeType` (SomeType can be defined elsewhere)
|
||||
objectDef = ensureDistinctType(rhs)
|
||||
isRefObject = false
|
||||
hasInlineFields = false
|
||||
else:
|
||||
error(macroName & " ref object must wrap a concrete object definition", rhs)
|
||||
elif rhs.kind == nnkIdent and rhs.eqIdent("void"):
|
||||
## `void` — a payload-less broker. The bare `void` type cannot name a
|
||||
## broker (every `void` broker would share `typedesc[void]`, colliding
|
||||
## the generated `request` / `setProvider` / `emit` overloads). It is
|
||||
## therefore lowered to a *unique* empty `object` — a unit type — so
|
||||
## each broker keeps a distinct identity. `isVoid` lets broker macros
|
||||
## drop the now-meaningless value parameter from handler / emit
|
||||
## signatures; the request payload is simply the zero-field object.
|
||||
objectDef =
|
||||
newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), newTree(nnkRecList))
|
||||
isRefObject = false
|
||||
hasInlineFields = false
|
||||
isVoid = true
|
||||
else:
|
||||
## Non-object type / alias.
|
||||
objectDef = ensureDistinctType(rhs)
|
||||
isRefObject = false
|
||||
hasInlineFields = false
|
||||
|
||||
result = ParsedBrokerType(
|
||||
typeIdent: typeIdent,
|
||||
objectDef: objectDef,
|
||||
isRefObject: isRefObject,
|
||||
hasInlineFields: hasInlineFields,
|
||||
isVoid: isVoid,
|
||||
fieldNames: fieldNames,
|
||||
fieldTypes: fieldTypes,
|
||||
)
|
||||
|
||||
proc parseTypeDefs*(
|
||||
body: NimNode,
|
||||
macroName: string,
|
||||
allowRefToNonObject = false,
|
||||
collectFieldInfo = false,
|
||||
): seq[ParsedBrokerType] =
|
||||
## Parses all `type` definitions from a broker macro body.
|
||||
## Returns them in declaration order. Supports multiple types in a single
|
||||
## broker block (e.g. supporting types + primary type).
|
||||
##
|
||||
## Callers are responsible for identifying which entry is the "primary" type
|
||||
## (typically the last one, or the one referenced in the signature return type).
|
||||
result = @[]
|
||||
for stmt in body:
|
||||
if stmt.kind != nnkTypeSection:
|
||||
continue
|
||||
for def in stmt:
|
||||
if def.kind != nnkTypeDef:
|
||||
continue
|
||||
result.add(parseOneTypeDef(def, macroName, allowRefToNonObject, collectFieldInfo))
|
||||
|
||||
if result.len == 0:
|
||||
error(macroName & " body must declare at least one type", body)
|
||||
|
||||
proc parseSingleTypeDef*(
|
||||
body: NimNode,
|
||||
macroName: string,
|
||||
allowRefToNonObject = false,
|
||||
collectFieldInfo = false,
|
||||
): ParsedBrokerType =
|
||||
## Parses exactly one `type` definition from a broker macro body.
|
||||
## Backward-compatible wrapper around parseTypeDefs that enforces a single type.
|
||||
##
|
||||
## Supported RHS:
|
||||
## - inline `object` / `ref object` (fields are auto-exported)
|
||||
## - non-object types / aliases / externally-defined types (wrapped in `distinct`)
|
||||
## - optionally: `ref SomeType` when `allowRefToNonObject = true`
|
||||
let defs = parseTypeDefs(body, macroName, allowRefToNonObject, collectFieldInfo)
|
||||
if defs.len > 1:
|
||||
error("Only one type may be declared inside " & macroName, body)
|
||||
result = defs[0]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RequestBroker proc-style sugar (option B — payload decoupled from the
|
||||
# dispatch tag). Shared by the single-thread, multi-thread, and API
|
||||
# RequestBroker generators so the surface stays identical across flavors.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
type ParsedRequestSugar* = object
|
||||
## Result of parsing the new proc-style RequestBroker sugar.
|
||||
## - `typeIdent` : the dispatch-tag type (broker name).
|
||||
## - `objectDef` : RHS to declare for the tag (the object for the object
|
||||
## form, `distinct payload` for the POD form).
|
||||
## - `payloadType`: the value type returned by `request` (decoupled — raw
|
||||
## payload for POD, == typeIdent for the object form).
|
||||
## - `fieldTypes` : object field types (object form) for MT auto-config;
|
||||
## empty for POD.
|
||||
## - `zeroArgProc`/`argProc`: the parsed signature proc defs (nil if absent).
|
||||
## - `argParams` : IdentDefs of the arg-based signature.
|
||||
typeIdent*: NimNode
|
||||
objectDef*: NimNode
|
||||
payloadType*: NimNode
|
||||
fieldTypes*: seq[NimNode]
|
||||
zeroArgProc*: NimNode
|
||||
argProc*: NimNode
|
||||
argParams*: seq[NimNode]
|
||||
verb*: string
|
||||
## The (lowercase) signature verb — the BrokerInterface method name that
|
||||
## `BrokerImplement` overrides (e.g. `getHealth`).
|
||||
parsed*: ParsedBrokerType
|
||||
## Full parse of the dispatch tag over the payload — drives the API/CBOR
|
||||
## schema registration identically to the legacy `type X = ...` path.
|
||||
|
||||
proc extractResultOk*(returnType: NimNode, async: bool): NimNode =
|
||||
## Ok payload type T from `Future[Result[T, string]]` (async) or
|
||||
## `Result[T, string]` (sync). Returns nil if the shape is invalid (error
|
||||
## type must be `string`). FFI/in-process errors are pinned to `string`.
|
||||
if async:
|
||||
if returnType.kind != nnkBracketExpr or returnType.len != 2:
|
||||
return nil
|
||||
if returnType[0].kind != nnkIdent or not returnType[0].eqIdent("Future"):
|
||||
return nil
|
||||
let inner = returnType[1]
|
||||
if inner.kind != nnkBracketExpr or inner.len != 3:
|
||||
return nil
|
||||
if inner[0].kind != nnkIdent or not inner[0].eqIdent("Result"):
|
||||
return nil
|
||||
if not (inner[2].kind == nnkIdent and inner[2].eqIdent("string")):
|
||||
return nil
|
||||
return inner[1]
|
||||
else:
|
||||
if returnType.kind != nnkBracketExpr or returnType.len != 3:
|
||||
return nil
|
||||
if returnType[0].kind != nnkIdent or not returnType[0].eqIdent("Result"):
|
||||
return nil
|
||||
if not (returnType[2].kind == nnkIdent and returnType[2].eqIdent("string")):
|
||||
return nil
|
||||
return returnType[1]
|
||||
|
||||
proc sugarVerbIdent(p: NimNode): NimNode =
|
||||
let nm = p[0]
|
||||
if nm.kind == nnkPostfix:
|
||||
nm[1]
|
||||
else:
|
||||
nm
|
||||
|
||||
proc parseRequestSugar*(
|
||||
body: NimNode, macroName: string, async: bool
|
||||
): ParsedRequestSugar =
|
||||
## Parse the proc-style sugar form of a RequestBroker body (one broker per
|
||||
## block, two signature slots, payload decoupled from the dispatch tag).
|
||||
var typeDecl: NimNode = nil
|
||||
var procs: seq[NimNode] = @[]
|
||||
for stmt in body:
|
||||
case stmt.kind
|
||||
of nnkProcDef:
|
||||
procs.add(stmt)
|
||||
of nnkTypeSection:
|
||||
for d in stmt:
|
||||
if d.kind == nnkTypeDef:
|
||||
if typeDecl != nil:
|
||||
error(macroName & " sugar allows a single payload type", d)
|
||||
typeDecl = d
|
||||
of nnkEmpty:
|
||||
discard
|
||||
else:
|
||||
error("Unsupported statement inside " & macroName & " definition", stmt)
|
||||
if procs.len == 0:
|
||||
error(macroName & " requires at least one signature proc", body)
|
||||
|
||||
var verb = ""
|
||||
for p in procs:
|
||||
if verb.len == 0:
|
||||
verb = $sugarVerbIdent(p)
|
||||
elif not sugarVerbIdent(p).eqIdent(verb):
|
||||
error("All signatures in one " & macroName & " block must share the proc name", p)
|
||||
let brokerName = capitalizeAscii(verb)
|
||||
result.verb = verb
|
||||
|
||||
if typeDecl != nil:
|
||||
let parsedT = parseSingleTypeDef(
|
||||
newTree(nnkStmtList, newTree(nnkTypeSection, typeDecl)),
|
||||
macroName,
|
||||
allowRefToNonObject = true,
|
||||
collectFieldInfo = true,
|
||||
)
|
||||
result.typeIdent = parsedT.typeIdent
|
||||
result.objectDef = parsedT.objectDef
|
||||
result.fieldTypes = parsedT.fieldTypes
|
||||
result.parsed = parsedT
|
||||
if not result.typeIdent.eqIdent(brokerName):
|
||||
error(
|
||||
"Signature `" & verb & "` must pair with type `" & brokerName & "` (got `" &
|
||||
$result.typeIdent & "`)",
|
||||
typeDecl,
|
||||
)
|
||||
result.payloadType = copyNimTree(result.typeIdent)
|
||||
else:
|
||||
# POD form: the broker name is derived solely from the proc verb and is
|
||||
# always Capitalized (it is a Nim type / dispatch tag). Warn when we had to
|
||||
# capitalize a lowercase verb so the `Broker.request(...)` handle name is
|
||||
# not a surprise; writing the proc Capitalized (`proc GetConfig(...)`) is
|
||||
# accepted and silences this.
|
||||
if verb.len > 0 and verb[0] in {'a' .. 'z'}:
|
||||
warning(
|
||||
"RequestBroker: broker name is `" & brokerName & "` (capitalized from proc `" &
|
||||
verb & "`); call it as `" & brokerName & ".request(...)`. Write `proc " &
|
||||
brokerName & "(...)` to name it explicitly and silence this warning.",
|
||||
procs[0],
|
||||
)
|
||||
result.typeIdent = ident(brokerName)
|
||||
|
||||
for p in procs:
|
||||
let params = p.params
|
||||
if params.len == 0:
|
||||
error("Signature must declare a return type", p)
|
||||
let pl = extractResultOk(params[0], async)
|
||||
if pl.isNil:
|
||||
error(
|
||||
"Signature must return " &
|
||||
(if async: "Future[Result[T, string]]" else: "Result[T, string]"),
|
||||
p,
|
||||
)
|
||||
if result.payloadType.isNil:
|
||||
result.payloadType = copyNimTree(pl)
|
||||
elif result.payloadType.repr != pl.repr:
|
||||
error(
|
||||
"All signatures of broker `" & brokerName & "` must return the same payload type",
|
||||
p,
|
||||
)
|
||||
let paramCount = params.len - 1
|
||||
if paramCount == 0:
|
||||
if not result.zeroArgProc.isNil:
|
||||
error("Only one zero-argument signature is allowed", p)
|
||||
result.zeroArgProc = p
|
||||
else:
|
||||
if not result.argProc.isNil:
|
||||
error("Only one argument-based signature is allowed", p)
|
||||
result.argProc = p
|
||||
result.argParams = @[]
|
||||
for idx in 1 ..< params.len:
|
||||
let pd = params[idx]
|
||||
if pd.kind != nnkIdentDefs:
|
||||
error("Signature parameter must be a standard identifier declaration", pd)
|
||||
if pd[pd.len - 2].kind == nnkEmpty:
|
||||
error("Signature parameter must declare a type", pd)
|
||||
result.argParams.add(copyNimTree(pd))
|
||||
|
||||
if typeDecl == nil:
|
||||
# POD: synthesize `type <tag> = <payload>` and parse it through the normal
|
||||
# path so the dispatch-tag classification (primitive / void / distinct) and
|
||||
# the API/CBOR schema registration match the legacy `type X = ...` form.
|
||||
let synth = newTree(
|
||||
nnkStmtList,
|
||||
newTree(
|
||||
nnkTypeSection,
|
||||
newTree(
|
||||
nnkTypeDef,
|
||||
copyNimTree(result.typeIdent),
|
||||
newEmptyNode(),
|
||||
copyNimTree(result.payloadType),
|
||||
),
|
||||
),
|
||||
)
|
||||
result.parsed = parseSingleTypeDef(
|
||||
synth, macroName, allowRefToNonObject = true, collectFieldInfo = true
|
||||
)
|
||||
result.objectDef = result.parsed.objectDef
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compile-time interface -> event-type registry. BrokerInterface records the
|
||||
# event types it declares; BrokerImplement reads them so the generated
|
||||
# `close()` can drop the instance's event listeners (the impl macro otherwise
|
||||
# doesn't know the interface's events).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var gInterfaceEvents {.compileTime.}: seq[(string, seq[string])] = @[]
|
||||
|
||||
proc registerInterfaceEvents*(iface: string, events: seq[string]) {.compileTime.} =
|
||||
for i in 0 ..< gInterfaceEvents.len:
|
||||
if gInterfaceEvents[i][0] == iface:
|
||||
gInterfaceEvents[i][1] = events
|
||||
return
|
||||
gInterfaceEvents.add((iface, events))
|
||||
|
||||
proc interfaceEvents*(iface: string): seq[string] {.compileTime.} =
|
||||
for it in gInterfaceEvents:
|
||||
if it[0] == iface:
|
||||
return it[1]
|
||||
@[]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compile-time registry of interface request verbs.
|
||||
# Records, per interface, the verb name and the associated request type name.
|
||||
# Used by BrokerImplement to validate that all declared requests are overridden.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var gInterfaceVerbs {.compileTime.}: seq[(string, seq[(string, string)])] = @[]
|
||||
|
||||
proc registerInterfaceVerbs*(
|
||||
iface: string, verbs: seq[(string, string)]
|
||||
) {.compileTime.} =
|
||||
for i in 0 ..< gInterfaceVerbs.len:
|
||||
if gInterfaceVerbs[i][0] == iface:
|
||||
gInterfaceVerbs[i] = (iface, verbs)
|
||||
return
|
||||
gInterfaceVerbs.add((iface, verbs))
|
||||
|
||||
proc interfaceRequestVerbs*(iface: string): seq[(string, string)] {.compileTime.} =
|
||||
for it in gInterfaceVerbs:
|
||||
if it[0] == iface:
|
||||
return it[1]
|
||||
@[]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compile-time registry of `BrokerInterface(API)` interfaces (reduced-A, A1).
|
||||
# Records, per interface, the sanitized request *type* names and event *type*
|
||||
# names it owns. The flat CBOR request/event entry registries store these same
|
||||
# type names (CborRequestEntry.responseTypeName / CborEventEntry.typeName), so
|
||||
# wrapper codegen can partition the flat entry lists per interface by matching
|
||||
# on type name — no need to replicate the snake/suffix apiName derivation here.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
type ApiInterfaceEntry* = object
|
||||
name*: string
|
||||
requestTypes*: seq[string] ## sanitized request broker type names
|
||||
eventTypes*: seq[string] ## event payload type names
|
||||
|
||||
var gApiInterfaces {.compileTime.}: seq[ApiInterfaceEntry] = @[]
|
||||
|
||||
proc registerApiInterface*(
|
||||
name: string, requestTypes, eventTypes: seq[string]
|
||||
) {.compileTime.} =
|
||||
for i in 0 ..< gApiInterfaces.len:
|
||||
if gApiInterfaces[i].name == name:
|
||||
gApiInterfaces[i].requestTypes = requestTypes
|
||||
gApiInterfaces[i].eventTypes = eventTypes
|
||||
return
|
||||
gApiInterfaces.add(
|
||||
ApiInterfaceEntry(name: name, requestTypes: requestTypes, eventTypes: eventTypes)
|
||||
)
|
||||
|
||||
proc apiInterfaces*(): seq[ApiInterfaceEntry] {.compileTime.} =
|
||||
gApiInterfaces
|
||||
|
||||
proc isApiInterface*(name: string): bool {.compileTime.} =
|
||||
for it in gApiInterfaces:
|
||||
if it.name == name:
|
||||
return true
|
||||
false
|
||||
|
||||
proc interfaceOwningRequestType*(typeName: string): string {.compileTime.} =
|
||||
## Comma-joined names of every interface that declared the request broker
|
||||
## `typeName` (more than one when two interfaces reuse the same type name —
|
||||
## itself the most common apiName collision), or "" if none.
|
||||
var owners: seq[string] = @[]
|
||||
for it in gApiInterfaces:
|
||||
for rt in it.requestTypes:
|
||||
if rt == typeName:
|
||||
owners.add(it.name)
|
||||
owners.join(", ")
|
||||
|
||||
proc interfaceOwningEventType*(typeName: string): string {.compileTime.} =
|
||||
var owners: seq[string] = @[]
|
||||
for it in gApiInterfaces:
|
||||
for et in it.eventTypes:
|
||||
if et == typeName:
|
||||
owners.add(it.name)
|
||||
owners.join(", ")
|
||||
@@ -0,0 +1,324 @@
|
||||
## Multi-Thread Broker Common
|
||||
## --------------------------
|
||||
## Shared runtime helpers used by both mt_request_broker and mt_event_broker.
|
||||
## These are not generated — they are used directly by generated code.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import chronos, chronos/threadsync
|
||||
import std/atomics
|
||||
import std/[os, locks] # `sleep`; `Lock` for the API listener-installer registry
|
||||
import results
|
||||
import ../broker_context
|
||||
import ./mt_queue
|
||||
export chronos, threadsync, atomics
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# reduced-A: per-classCtx event-listener installer registry.
|
||||
#
|
||||
# An EventBroker(API) event only reaches the foreign event courier if the
|
||||
# library's `installAllListeners` has been called for the *emitting* ctx. The
|
||||
# main library ctx is handled at createContext, but a SUB-INSTANCE (created via
|
||||
# a create-instance request, sharing the library classCtx with a distinct
|
||||
# instanceCtx) needs its listeners installed too. registerBrokerLibrary records
|
||||
# its installer keyed by classCtx here; the create-instance adapter calls
|
||||
# `installApiListenersForCtx(subCtx)` on the processing thread.
|
||||
#
|
||||
# Storage is a fixed POD array (the installer is a bare `nimcall` function
|
||||
# pointer, the key a uint16) so it is safe to share across threads under both
|
||||
# --mm:refc and --mm:orc — no GC'd container crosses the thread boundary.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const maxApiCtxInstallers* = 64
|
||||
|
||||
type ApiCtxListenerInstaller* =
|
||||
proc(ctx: BrokerContext): Result[void, string] {.nimcall.}
|
||||
|
||||
var gApiCtxInstallers:
|
||||
array[maxApiCtxInstallers, tuple[classCtx: uint16, fn: ApiCtxListenerInstaller]]
|
||||
var gApiCtxInstallerCount: int
|
||||
var gApiCtxInstallerLock: Lock
|
||||
var gApiCtxInstallerLockInit: Atomic[int]
|
||||
|
||||
proc ensureApiCtxInstallerLock() {.gcsafe.} =
|
||||
var expected = 0
|
||||
if gApiCtxInstallerLockInit.compareExchange(expected, 1, moAcquire, moRelaxed):
|
||||
{.cast(gcsafe).}:
|
||||
initLock(gApiCtxInstallerLock)
|
||||
gApiCtxInstallerLockInit.store(2, moRelease)
|
||||
else:
|
||||
while gApiCtxInstallerLockInit.load(moAcquire) != 2:
|
||||
sleep(0)
|
||||
|
||||
proc registerApiCtxListenerInstaller*(
|
||||
classCtx: uint16, fn: ApiCtxListenerInstaller
|
||||
) {.gcsafe.} =
|
||||
## Record (or replace) the listener installer for a library, keyed by its
|
||||
## classCtx. Called once per `createContext`.
|
||||
ensureApiCtxInstallerLock()
|
||||
{.cast(gcsafe).}:
|
||||
withLock gApiCtxInstallerLock:
|
||||
for i in 0 ..< gApiCtxInstallerCount:
|
||||
if gApiCtxInstallers[i].classCtx == classCtx:
|
||||
gApiCtxInstallers[i].fn = fn
|
||||
return
|
||||
if gApiCtxInstallerCount < maxApiCtxInstallers:
|
||||
gApiCtxInstallers[gApiCtxInstallerCount] = (classCtx, fn)
|
||||
inc gApiCtxInstallerCount
|
||||
|
||||
proc installApiListenersForCtx*(ctx: BrokerContext) {.gcsafe.} =
|
||||
## Install the owning library's event-courier listeners for a sub-instance
|
||||
## ctx (looked up by classCtx). Best-effort: if no installer is registered
|
||||
## (e.g. a library with no events) or it fails, the sub-instance simply has no
|
||||
## event delivery. Runs on the processing thread.
|
||||
ensureApiCtxInstallerLock()
|
||||
var fn: ApiCtxListenerInstaller = nil
|
||||
let cc = classCtx(ctx)
|
||||
{.cast(gcsafe).}:
|
||||
withLock gApiCtxInstallerLock:
|
||||
for i in 0 ..< gApiCtxInstallerCount:
|
||||
if gApiCtxInstallers[i].classCtx == cc:
|
||||
fn = gApiCtxInstallers[i].fn
|
||||
break
|
||||
if not fn.isNil:
|
||||
try:
|
||||
{.cast(gcsafe).}:
|
||||
discard fn(ctx)
|
||||
except Exception:
|
||||
discard
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Thread identity
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var mtThreadIdMarker* {.threadvar.}: bool
|
||||
## Each thread gets its own copy; `addr mtThreadIdMarker` is a unique thread id.
|
||||
|
||||
template currentMtThreadId*(): pointer =
|
||||
addr mtThreadIdMarker
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Thread generation — monotonically increasing, unique per thread incarnation.
|
||||
# Under refc, threadvar addresses can be reused when threads exit and new
|
||||
# ones are created. The generation counter disambiguates reused addresses.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
var gMtThreadGenCounter: Atomic[uint64]
|
||||
|
||||
var mtThreadGen* {.threadvar.}: uint64
|
||||
var mtThreadGenInitialized {.threadvar.}: bool
|
||||
|
||||
proc currentMtThreadGen*(): uint64 =
|
||||
if not mtThreadGenInitialized:
|
||||
mtThreadGen = gMtThreadGenCounter.fetchAdd(1, moRelaxed)
|
||||
mtThreadGenInitialized = true
|
||||
mtThreadGen
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Blocking await for {.thread.} procs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
template blockingAwait*[T](f: Future[T]): T =
|
||||
## Blocking await for use inside non-async `{.thread.}` procs.
|
||||
## Use this instead of `await` (which conflicts with chronos's async-only
|
||||
## `await`) or call `waitFor` directly.
|
||||
waitFor(f)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-thread shared signal + dispatcher
|
||||
# ---------------------------------------------------------------------------
|
||||
# Instead of one ThreadSignalPtr (2 fds on macOS, 1 on Linux) per broker
|
||||
# type per thread, every broker type on the same thread shares a single
|
||||
# ThreadSignalPtr. Fd count drops from O(broker_types × threads) to
|
||||
# O(threads).
|
||||
#
|
||||
# Each broker type registers a poll proc (ThreadDispatchPollFn). The
|
||||
# shared brokerDispatchLoop coroutine fires whenever ANY channel on this
|
||||
# thread has a new message, then drains all registered poll procs.
|
||||
# Poll proc return values:
|
||||
# 0 — nothing to process; keep registered
|
||||
# 1 — message processed; keep registered
|
||||
# 2 — done (shutdown or one-shot complete); remove from dispatcher
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
type ThreadDispatchPollFn* = proc(): int {.gcsafe, raises: [].}
|
||||
|
||||
var gBrokerThreadSignal* {.threadvar.}: ThreadSignalPtr
|
||||
var gBrokerThreadPollers* {.threadvar.}: seq[ThreadDispatchPollFn]
|
||||
var gBrokerDispatchStarted* {.threadvar.}: bool
|
||||
var gBrokerDispatchStopRequested* {.threadvar.}: bool
|
||||
## Set by stopBrokerDispatchHere() to ask the loop to exit on its next
|
||||
## drain pass. Used by FFI entry points so that transient foreign threads
|
||||
## (e.g. the C++ caller of <lib>_request_*/<lib>_shutdown) don't accumulate
|
||||
## a persistent suspended coroutine and its associated chronos/GC state
|
||||
## across calls. The flag is cleared by stopBrokerDispatchHere() after the
|
||||
## loop confirms exit.
|
||||
|
||||
proc getOrInitBrokerSignal*(): ThreadSignalPtr =
|
||||
## Get (or lazily create) the per-thread signal shared by all broker types.
|
||||
if gBrokerThreadSignal.isNil:
|
||||
let res = ThreadSignalPtr.new()
|
||||
if res.isErr():
|
||||
raiseAssert "BrokerDispatcher: failed to create thread signal: " & res.error
|
||||
gBrokerThreadSignal = res.get()
|
||||
gBrokerThreadSignal
|
||||
|
||||
proc fireBrokerSignal*(signal: ThreadSignalPtr) {.gcsafe, raises: [].} =
|
||||
## Wake the target thread's broker dispatcher. Safe to call from any thread.
|
||||
discard signal.fireSync()
|
||||
|
||||
proc registerBrokerPoller*(fn: ThreadDispatchPollFn) =
|
||||
## Register a poll function with this thread's dispatcher.
|
||||
## Must be called from the owning thread.
|
||||
gBrokerThreadPollers.add(fn)
|
||||
|
||||
proc brokerDispatchLoop*(signal: ThreadSignalPtr) {.async: (raises: []).} =
|
||||
## Single dispatch loop per chronos thread. Drains all registered broker
|
||||
## channel pollers whenever the shared signal fires.
|
||||
while true:
|
||||
# Drain: keep polling until every channel is empty.
|
||||
var anyWork = true
|
||||
while anyWork:
|
||||
anyWork = false
|
||||
var i = 0
|
||||
while i < gBrokerThreadPollers.len:
|
||||
let r = gBrokerThreadPollers[i]()
|
||||
case r
|
||||
of 2:
|
||||
# Poller is done — remove it.
|
||||
gBrokerThreadPollers.del(i)
|
||||
of 1:
|
||||
anyWork = true
|
||||
inc i
|
||||
else:
|
||||
inc i
|
||||
# FFI-caller teardown hook: an external caller (stopBrokerDispatchHere)
|
||||
# asked the loop to exit. Drain pass is complete, exit cleanly.
|
||||
if gBrokerDispatchStopRequested:
|
||||
break
|
||||
# Wait for next signal.
|
||||
let waitRes = catch:
|
||||
await signal.wait()
|
||||
if waitRes.isErr():
|
||||
break
|
||||
if gBrokerDispatchStopRequested:
|
||||
break
|
||||
# Dispatcher is exiting (e.g. thread shutting down). Close the per-thread
|
||||
# signal so its OS handle (eventfd on Linux, pipe pair on macOS) is reclaimed
|
||||
# instead of leaking on every createContext/processing-thread cycle. Reset
|
||||
# the threadvar state so a future ensureBrokerDispatchStarted() on a reused
|
||||
# threadvar address (refc) starts fresh.
|
||||
let sig = gBrokerThreadSignal
|
||||
gBrokerThreadSignal = nil
|
||||
gBrokerDispatchStarted = false
|
||||
if not sig.isNil:
|
||||
let closeRes = sig.close()
|
||||
if closeRes.isErr():
|
||||
discard
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pending-ring-free registry — synchronous deferred cleanup at thread exit
|
||||
# ---------------------------------------------------------------------------
|
||||
# When clearProvider(ctx) closes a request broker's ring on the provider
|
||||
# thread, the corresponding poll fn (registered via brokerDispatchLoop's
|
||||
# pollers seq) detects `ring.isClosed()` on its next iteration and needs to
|
||||
# free the shared-memory (ring, slab, pool) triple — but only after a grace
|
||||
# window long enough for any cross-thread sender that snapshotted those
|
||||
# pointers under the previous globalLock state to finish its enqueue.
|
||||
#
|
||||
# Previous design: `asyncSpawn deferredFreeReqRing(...)` — start an async
|
||||
# proc that does `await sleepAsync(50ms)` then frees. Two problems:
|
||||
#
|
||||
# 1. Allocating the sleepAsync Future inside an asyncSpawn started during
|
||||
# `cleanupAllRequestsIdent` runs the refc allocator at a moment where
|
||||
# the thread's gch state is fragile from teardown churn. Observed as a
|
||||
# hard SEGV in rawAlloc on Linux refc + ASAN (PR #13).
|
||||
#
|
||||
# 2. drainAsyncOps only polls chronos for 1ms — the 50ms sleepAsync would
|
||||
# never fire before the processing thread exits, so the buffers either
|
||||
# leak or are freed by an orphaned coroutine racing thread teardown.
|
||||
#
|
||||
# Current design: the poll fn instead records the triple in a thread-local
|
||||
# seq; the processing-thread proc drains the seq AFTER drainAsyncOps via a
|
||||
# single synchronous `sleep(50)` followed by direct free calls. No chronos
|
||||
# involvement in the cleanup path; the grace window applies once for the
|
||||
# whole ctx instead of once per broker.
|
||||
type PendingRingFree* = object
|
||||
ring*: ptr VyukovMpscRing[uint32]
|
||||
slab*: ptr PayloadSlab
|
||||
pool*: ptr ResponseSlotPool
|
||||
|
||||
var gPendingRingFrees* {.threadvar.}: seq[PendingRingFree]
|
||||
|
||||
proc enqueuePendingRingFree*(
|
||||
ring: ptr VyukovMpscRing[uint32], slab: ptr PayloadSlab, pool: ptr ResponseSlotPool
|
||||
) {.gcsafe.} =
|
||||
## Called from a broker poll fn on the provider thread when its ring has
|
||||
## been closed by clearProvider(). The (ring, slab, pool) triple will be
|
||||
## freed by `drainPendingRingFrees()` at thread shutdown.
|
||||
{.cast(gcsafe).}:
|
||||
gPendingRingFrees.add(PendingRingFree(ring: ring, slab: slab, pool: pool))
|
||||
|
||||
proc drainPendingRingFrees*() {.gcsafe.} =
|
||||
## Drain the per-thread pending-ring-free registry synchronously.
|
||||
## Sleeps once for a 50ms grace window covering all queued frees, then
|
||||
## releases each (ring, slab, pool) triple. Must be called from the owning
|
||||
## thread AFTER any chronos work that may still touch the buffers has
|
||||
## completed (i.e. after `drainAsyncOps` in the processing-thread proc).
|
||||
if gPendingRingFrees.len == 0:
|
||||
return
|
||||
# Single grace window: 50ms is enough for any sender that snapshotted
|
||||
# pool/slab/ring pointers before clearProvider closed the ring to either
|
||||
# complete its enqueue (which then fails on isClosed()) or abort. Without
|
||||
# this, a stale sender deref'ing the about-to-be-freed slab/pool crashes.
|
||||
sleep(50)
|
||||
for entry in gPendingRingFrees:
|
||||
if not entry.ring.isNil:
|
||||
freeVyukovMpscRing(entry.ring)
|
||||
if not entry.slab.isNil:
|
||||
deinitPayloadSlab(entry.slab[])
|
||||
deallocShared(entry.slab)
|
||||
if not entry.pool.isNil:
|
||||
deinitResponseSlotPool(entry.pool[])
|
||||
deallocShared(entry.pool)
|
||||
gPendingRingFrees.setLen(0)
|
||||
|
||||
proc ensureBrokerDispatchStarted*() =
|
||||
## Start the per-thread dispatch loop if not already running.
|
||||
## Must be called from within a chronos async context.
|
||||
if not gBrokerDispatchStarted:
|
||||
gBrokerDispatchStarted = true
|
||||
asyncSpawn brokerDispatchLoop(getOrInitBrokerSignal())
|
||||
|
||||
proc stopBrokerDispatchHere*() =
|
||||
## Tear down the per-thread brokerDispatchLoop on the calling thread.
|
||||
##
|
||||
## Intended for **FFI entry points** (procs exported with `cdecl, dynlib`
|
||||
## that run on a foreign caller's thread). The dispatch loop was designed
|
||||
## for chronos-loop-owning threads (processing/delivery threads), which
|
||||
## are torn down via joinThread. An FFI caller's thread instead lives for
|
||||
## the entire process and re-enters Nim per call; without teardown its
|
||||
## suspended `await signal.wait()` future, registered pollers seq, and
|
||||
## chronos pending-callback list accumulate across calls and eventually
|
||||
## drag the thread's refc ZCT/heap into corruption (PR #13).
|
||||
##
|
||||
## Safe to call from sync context (after `waitFor` returns). No-op if the
|
||||
## loop was never started on this thread. Drives chronos via an internal
|
||||
## `waitFor` until the loop's coroutine actually exits.
|
||||
if not gBrokerDispatchStarted:
|
||||
return
|
||||
gBrokerDispatchStopRequested = true
|
||||
let sig = gBrokerThreadSignal
|
||||
if not sig.isNil:
|
||||
discard sig.fireSync()
|
||||
|
||||
proc awaitLoopExit() {.async: (raises: []).} =
|
||||
let deadline = Moment.now() + chronos.seconds(2)
|
||||
while gBrokerDispatchStarted and Moment.now() < deadline:
|
||||
let sleepRes = catch:
|
||||
await sleepAsync(milliseconds(1))
|
||||
if sleepRes.isErr():
|
||||
break
|
||||
|
||||
waitFor awaitLoopExit()
|
||||
gBrokerDispatchStopRequested = false
|
||||
@@ -0,0 +1,307 @@
|
||||
## Runtime marshal / unmarshal helpers for (mt) broker payloads.
|
||||
##
|
||||
## The broker macro emits two thin per-type wrappers
|
||||
## (`<TypeName>MtMarshal` / `<TypeName>MtUnmarshal`) that call into the
|
||||
## generic `mtMarshalValue` / `mtUnmarshalValue` defined here. Those
|
||||
## generics use `when supportsCopyMem(T):` + Nim's `fieldPairs` to walk
|
||||
## arbitrary payload types at compile time, recursing into:
|
||||
##
|
||||
## - **POD types** (scalars, enums, distinct-of-POD, fixed POD arrays,
|
||||
## objects whose fields are all POD): single `copyMem(sizeof(T))`.
|
||||
## `supportsCopyMem` correctly classifies all of these.
|
||||
## - **`string`**: 4-byte little-endian length + bytes.
|
||||
## - **`seq[U]`**: 4-byte length + per-element recursive marshal.
|
||||
## - **`array[N, U]` where U is non-POD**: per-element recursive marshal.
|
||||
## - **objects with non-POD fields**: `fieldPairs` walks each field
|
||||
## recursively.
|
||||
##
|
||||
## Forbidden (caught at the call site by a compile-time `{.error.}`):
|
||||
## `ref T`, `ptr T`, `pointer`, `cstring`, proc-typed fields.
|
||||
##
|
||||
## Strings and seqs allocate on the *consumer thread's GC heap* during
|
||||
## unmarshal — no thread-local pointer ever crosses a broker boundary,
|
||||
## which is the §2.6 fix in practice.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[macros, typetraits]
|
||||
|
||||
# Generic recursive primitives. The `pos` parameter is updated in place;
|
||||
# the bool return is false on overflow / truncation / malformed input.
|
||||
|
||||
proc mtMarshalValue*[T](
|
||||
buf: ptr UncheckedArray[byte], cap: int, value: T, pos: var int
|
||||
): bool {.gcsafe.}
|
||||
|
||||
proc mtUnmarshalValue*[T](
|
||||
buf: ptr UncheckedArray[byte], len: int, value: var T, pos: var int
|
||||
): bool {.gcsafe.}
|
||||
|
||||
# Sequence specialization — separate generic so the element type `U`
|
||||
# is statically known (we need `newSeq[U]` on the unmarshal side).
|
||||
|
||||
proc mtMarshalSeq*[U](
|
||||
buf: ptr UncheckedArray[byte], cap: int, value: openArray[U], pos: var int
|
||||
): bool {.gcsafe.} =
|
||||
mixin mtMarshalValue # allow user overloads for element type
|
||||
if pos + 4 > cap:
|
||||
return false
|
||||
let sLen = uint32(value.len)
|
||||
copyMem(addr buf[pos], unsafeAddr sLen, 4)
|
||||
pos += 4
|
||||
when supportsCopyMem(U):
|
||||
let totalBytes = int(sLen) * sizeof(U)
|
||||
if pos + totalBytes > cap:
|
||||
return false
|
||||
if sLen > 0'u32:
|
||||
copyMem(addr buf[pos], unsafeAddr value[0], totalBytes)
|
||||
pos += totalBytes
|
||||
return true
|
||||
else:
|
||||
for e in value:
|
||||
if not mtMarshalValue(buf, cap, e, pos):
|
||||
return false
|
||||
return true
|
||||
|
||||
proc mtUnmarshalSeq*[U](
|
||||
buf: ptr UncheckedArray[byte], len: int, value: var seq[U], pos: var int
|
||||
): bool {.gcsafe.} =
|
||||
mixin mtUnmarshalValue # allow user overloads for element type
|
||||
if pos + 4 > len:
|
||||
return false
|
||||
var sLen: uint32
|
||||
copyMem(addr sLen, addr buf[pos], 4)
|
||||
pos += 4
|
||||
when supportsCopyMem(U):
|
||||
let totalBytes = int(sLen) * sizeof(U)
|
||||
if pos + totalBytes > len:
|
||||
return false
|
||||
value = newSeq[U](int(sLen))
|
||||
if sLen > 0'u32:
|
||||
copyMem(addr value[0], addr buf[pos], totalBytes)
|
||||
pos += totalBytes
|
||||
return true
|
||||
else:
|
||||
value = newSeq[U](int(sLen))
|
||||
for i in 0 ..< int(sLen):
|
||||
if not mtUnmarshalValue(buf, len, value[i], pos):
|
||||
return false
|
||||
return true
|
||||
|
||||
proc mtMarshalValue*[T](
|
||||
buf: ptr UncheckedArray[byte], cap: int, value: T, pos: var int
|
||||
): bool {.gcsafe.} =
|
||||
mixin mtMarshalValue # allow user overloads for field types
|
||||
when T is ref:
|
||||
when compiles(value.brokerCtx):
|
||||
# reduced-A: a BrokerInterface ref is a same-thread routing handle (it
|
||||
# carries a `brokerCtx`). Create-instance dispatch is same-thread (adapter
|
||||
# + provider both on the processing thread), so the ref never actually
|
||||
# travels between threads — we marshal its pointer bytewise purely to
|
||||
# satisfy the response codec's instantiation. This is NOT general
|
||||
# cross-thread ref support; arbitrary refs still hard-error below.
|
||||
if pos + sizeof(pointer) > cap:
|
||||
return false
|
||||
copyMem(addr buf[pos], unsafeAddr value, sizeof(pointer))
|
||||
pos += sizeof(pointer)
|
||||
return true
|
||||
else:
|
||||
{.error: "mt broker payload field type is unsupported (ref T): " & $T.}
|
||||
# ptr / pointer / cstring fall through to the `supportsCopyMem` branch
|
||||
# below and are marshaled bytewise. Caller is responsible for the
|
||||
# lifetime of what they point to — typically used for shared structures
|
||||
# like chronos' ThreadSignalPtr.
|
||||
elif supportsCopyMem(T):
|
||||
if pos + sizeof(T) > cap:
|
||||
return false
|
||||
copyMem(addr buf[pos], unsafeAddr value, sizeof(T))
|
||||
pos += sizeof(T)
|
||||
return true
|
||||
elif T is string:
|
||||
let sLen = uint32(value.len)
|
||||
if pos + 4 + int(sLen) > cap:
|
||||
return false
|
||||
copyMem(addr buf[pos], unsafeAddr sLen, 4)
|
||||
pos += 4
|
||||
if sLen > 0'u32:
|
||||
copyMem(addr buf[pos], unsafeAddr value[0], int(sLen))
|
||||
pos += int(sLen)
|
||||
return true
|
||||
elif T is seq:
|
||||
return mtMarshalSeq(buf, cap, value, pos)
|
||||
elif T is array:
|
||||
# Non-POD array (e.g. array[N, string]); iterate per element.
|
||||
for i in 0 ..< value.len:
|
||||
if not mtMarshalValue(buf, cap, value[i], pos):
|
||||
return false
|
||||
return true
|
||||
elif T is (object or tuple):
|
||||
for _, fval in fieldPairs(value):
|
||||
if not mtMarshalValue(buf, cap, fval, pos):
|
||||
return false
|
||||
return true
|
||||
elif T is distinct:
|
||||
# Unwrap to the underlying base and recurse. POD distincts (e.g.
|
||||
# `distinct int32`) are caught by the `supportsCopyMem` branch above;
|
||||
# this branch handles distincts whose base needs structural marshaling
|
||||
# such as `distinct seq[byte]` or `distinct string`.
|
||||
var base = distinctBase(value)
|
||||
return mtMarshalValue(buf, cap, base, pos)
|
||||
else:
|
||||
{.error: "mt broker payload field type is unsupported by mtMarshalValue: " & $T.}
|
||||
|
||||
proc mtUnmarshalValue*[T](
|
||||
buf: ptr UncheckedArray[byte], len: int, value: var T, pos: var int
|
||||
): bool {.gcsafe.} =
|
||||
mixin mtUnmarshalValue # allow user overloads for field types
|
||||
when T is ref:
|
||||
when compiles(value.brokerCtx):
|
||||
# reduced-A: BrokerInterface ref — same-thread routing handle, see the
|
||||
# marshal counterpart. Reads the pointer bytes back. Bypasses GC refcount
|
||||
# (the instance stays pinned by its provider closures), valid only because
|
||||
# the create-instance path is same-thread and transient.
|
||||
if pos + sizeof(pointer) > len:
|
||||
return false
|
||||
copyMem(unsafeAddr value, addr buf[pos], sizeof(pointer))
|
||||
pos += sizeof(pointer)
|
||||
return true
|
||||
else:
|
||||
{.error: "mt broker payload field type is unsupported (ref T): " & $T.}
|
||||
# ptr / pointer / cstring fall through to the `supportsCopyMem` branch
|
||||
# below and are marshaled bytewise. Caller is responsible for the
|
||||
# lifetime of what they point to — typically used for shared structures
|
||||
# like chronos' ThreadSignalPtr.
|
||||
elif supportsCopyMem(T):
|
||||
if pos + sizeof(T) > len:
|
||||
return false
|
||||
copyMem(unsafeAddr value, addr buf[pos], sizeof(T))
|
||||
pos += sizeof(T)
|
||||
return true
|
||||
elif T is string:
|
||||
if pos + 4 > len:
|
||||
return false
|
||||
var sLen: uint32
|
||||
copyMem(addr sLen, addr buf[pos], 4)
|
||||
pos += 4
|
||||
if pos + int(sLen) > len:
|
||||
return false
|
||||
value = newString(int(sLen))
|
||||
if sLen > 0'u32:
|
||||
copyMem(addr value[0], addr buf[pos], int(sLen))
|
||||
pos += int(sLen)
|
||||
return true
|
||||
elif T is seq:
|
||||
return mtUnmarshalSeq(buf, len, value, pos)
|
||||
elif T is array:
|
||||
for i in 0 ..< value.len:
|
||||
if not mtUnmarshalValue(buf, len, value[i], pos):
|
||||
return false
|
||||
return true
|
||||
elif T is (object or tuple):
|
||||
for _, fval in fieldPairs(value):
|
||||
if not mtUnmarshalValue(buf, len, fval, pos):
|
||||
return false
|
||||
return true
|
||||
elif T is distinct:
|
||||
type Base = distinctBase(T)
|
||||
var base: Base
|
||||
if not mtUnmarshalValue(buf, len, base, pos):
|
||||
return false
|
||||
value = T(base)
|
||||
return true
|
||||
else:
|
||||
{.error: "mt broker payload field type is unsupported by mtUnmarshalValue: " & $T.}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Marshal-size companion — pure byte-count walk, no writes.
|
||||
#
|
||||
# Mirrors mtMarshalValue exactly so the heap-spill path (flexible-mt-dispatch
|
||||
# Part 2) can size an exact `allocShared0` buffer in one pass when a payload
|
||||
# overflows the fixed slab cell. MUST stay structurally in lockstep with
|
||||
# mtMarshalValue: every branch that advances `pos` there adds the same count
|
||||
# here. Returns the marshaled byte length.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc mtMarshalSizeValue*[T](value: T): int {.gcsafe.}
|
||||
|
||||
proc mtMarshalSizeSeq*[U](value: openArray[U]): int {.gcsafe.} =
|
||||
mixin mtMarshalSizeValue
|
||||
result = 4 # length prefix
|
||||
when supportsCopyMem(U):
|
||||
result += value.len * sizeof(U)
|
||||
else:
|
||||
for e in value:
|
||||
result += mtMarshalSizeValue(e)
|
||||
|
||||
proc mtMarshalSizeValue*[T](value: T): int {.gcsafe.} =
|
||||
mixin mtMarshalSizeValue
|
||||
when T is ref:
|
||||
when compiles(value.brokerCtx):
|
||||
return sizeof(pointer)
|
||||
else:
|
||||
{.error: "mt broker payload field type is unsupported (ref T): " & $T.}
|
||||
elif supportsCopyMem(T):
|
||||
return sizeof(T)
|
||||
elif T is string:
|
||||
return 4 + value.len
|
||||
elif T is seq:
|
||||
return mtMarshalSizeSeq(value)
|
||||
elif T is array:
|
||||
result = 0
|
||||
for i in 0 ..< value.len:
|
||||
result += mtMarshalSizeValue(value[i])
|
||||
elif T is (object or tuple):
|
||||
result = 0
|
||||
for _, fval in fieldPairs(value):
|
||||
result += mtMarshalSizeValue(fval)
|
||||
elif T is distinct:
|
||||
var base = distinctBase(value)
|
||||
return mtMarshalSizeValue(base)
|
||||
else:
|
||||
{.
|
||||
error: "mt broker payload field type is unsupported by mtMarshalSizeValue: " & $T
|
||||
.}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Per-type wrapper proc generation (called from broker macros)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc genMtCodecProcs*(
|
||||
marshalIdent, unmarshalIdent: NimNode, typeIdent: NimNode
|
||||
): seq[NimNode] =
|
||||
## Emits per-type marshal/unmarshal/size wrappers that bottom out to the
|
||||
## generic primitives above. Three procs returned:
|
||||
## [marshalProc, unmarshalProc, sizeProc]. The size proc is named
|
||||
## `<marshalIdent>Size` and returns the exact marshaled byte length (used by
|
||||
## the heap-spill path to size an allocShared0 buffer in one pass).
|
||||
let bufIdent = ident("buf")
|
||||
let capIdent = ident("cap")
|
||||
let lenIdent = ident("len")
|
||||
let valueIdent = ident("value")
|
||||
let dstIdent = ident("dst")
|
||||
let posIdent = ident("pos")
|
||||
let sizeIdent = ident($marshalIdent & "Size")
|
||||
|
||||
let marshalProc = quote:
|
||||
proc `marshalIdent`(
|
||||
`bufIdent`: ptr UncheckedArray[byte], `capIdent`: int, `valueIdent`: `typeIdent`
|
||||
): int {.gcsafe, raises: [].} =
|
||||
var `posIdent` = 0
|
||||
if mtMarshalValue(`bufIdent`, `capIdent`, `valueIdent`, `posIdent`):
|
||||
return `posIdent`
|
||||
return -1
|
||||
|
||||
let unmarshalProc = quote:
|
||||
proc `unmarshalIdent`(
|
||||
`bufIdent`: ptr UncheckedArray[byte],
|
||||
`lenIdent`: int,
|
||||
`dstIdent`: var `typeIdent`,
|
||||
): bool {.gcsafe, raises: [].} =
|
||||
var `posIdent` = 0
|
||||
return mtUnmarshalValue(`bufIdent`, `lenIdent`, `dstIdent`, `posIdent`)
|
||||
|
||||
let sizeProc = quote:
|
||||
proc `sizeIdent`(`valueIdent`: `typeIdent`): int {.gcsafe, raises: [].} =
|
||||
mtMarshalSizeValue(`valueIdent`)
|
||||
|
||||
@[marshalProc, unmarshalProc, sizeProc]
|
||||
@@ -0,0 +1,596 @@
|
||||
## Multi-thread broker configuration
|
||||
## ---------------------------------
|
||||
## Compile-time config records and macro-argument parsing for the
|
||||
## multi-thread Event / Request brokers.
|
||||
##
|
||||
## The macro entry points accept optional kwargs:
|
||||
##
|
||||
## EventBroker(mt, queueDepth = 1024, slabCapacity = 4096): ...
|
||||
## RequestBroker(mt, responseSlots = 64, maxResponseBytes = 4096): ...
|
||||
##
|
||||
## When no kwargs are supplied the existing module-level defaults in
|
||||
## `mt_event_broker.nim` / `mt_request_broker.nim` are used unchanged.
|
||||
|
||||
{.push raises: [].}
|
||||
{.push warning[UnreachableCode]: off.}
|
||||
|
||||
import std/[macros, strutils]
|
||||
|
||||
type
|
||||
MtEvtCfg* = object ## Resolved EventBroker(mt) capacity config.
|
||||
queueDepth*: int ## ring slots per listener bucket (power-of-2)
|
||||
slabCapacity*: int ## global slab cell count
|
||||
maxPayloadBytes*: int ## per-cell payload bytes
|
||||
maxDynamicPayloadBytes*: int
|
||||
## ceiling for an auto-spilled (heap) payload that exceeds the fixed cell.
|
||||
## Spill is always-on; this is a dev-chosen sanity cap, default high(uint32)
|
||||
## (effectively unbounded). A payload above it is dropped (OOM/DoS backstop).
|
||||
freeListShards*: int ## sharded free-list partitions
|
||||
# Provenance — for the compile-time printout. "default" / "kwarg" /
|
||||
# "preset:<name>" / "auto:<reason>".
|
||||
queueDepthOrigin*: string
|
||||
slabCapacityOrigin*: string
|
||||
maxPayloadBytesOrigin*: string
|
||||
maxDynamicPayloadBytesOrigin*: string
|
||||
freeListShardsOrigin*: string
|
||||
|
||||
MtReqCfg* = object ## Resolved RequestBroker(mt) capacity config.
|
||||
queueDepth*: int
|
||||
slabCapacity*: int
|
||||
maxPayloadBytes*: int
|
||||
maxDynamicPayloadBytes*: int
|
||||
## ceiling for an auto-spilled request OR response payload. See MtEvtCfg.
|
||||
responseSlots*: int
|
||||
maxResponseBytes*: int
|
||||
freeListShards*: int
|
||||
queueDepthOrigin*: string
|
||||
slabCapacityOrigin*: string
|
||||
maxPayloadBytesOrigin*: string
|
||||
maxDynamicPayloadBytesOrigin*: string
|
||||
responseSlotsOrigin*: string
|
||||
maxResponseBytesOrigin*: string
|
||||
freeListShardsOrigin*: string
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Defaults
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const
|
||||
# Default ceiling for heap-spilled payloads. high(uint32) ≈ 4 GiB — also the
|
||||
# intrinsic cap, since the cell/slot spill-length fields are uint32. Spill is
|
||||
# always-on; this only bounds how large a single spill may grow.
|
||||
DefaultMtMaxDynamicPayloadBytes* = int(high(uint32))
|
||||
|
||||
DefaultMtEvtQueueDepth* = 256
|
||||
DefaultMtEvtSlabCapacity* = 1024
|
||||
DefaultMtEvtMaxPayloadBytes* = 1024
|
||||
DefaultMtEvtFreeListShards* = 4
|
||||
|
||||
DefaultMtReqQueueDepth* = 256
|
||||
DefaultMtReqSlabCapacity* = 64
|
||||
DefaultMtReqMaxPayloadBytes* = 1024
|
||||
DefaultMtReqResponseSlots* = 256
|
||||
DefaultMtReqMaxResponseBytes* = 64 * 1024
|
||||
DefaultMtReqFreeListShards* = 2
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Built-in presets
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Named shorthand for capacity profiles. Recognised in the macro as
|
||||
# `preset = <name>`.
|
||||
#
|
||||
# defaultBalanced same as omitting `preset`
|
||||
# fastBurst bursty emit/request, small payload — wide ring/slab
|
||||
# largePayload infrequent traffic with big payloads
|
||||
# tinyFootprint rare traffic, embedded / memory-constrained
|
||||
#
|
||||
# Individual kwargs supplied alongside `preset =` override the preset's
|
||||
# values (so you can pick a profile and tweak one field).
|
||||
|
||||
type BuiltinPreset* = enum
|
||||
bpDefaultBalanced = "defaultBalanced"
|
||||
bpFastBurst = "fastBurst"
|
||||
bpLargePayload = "largePayload"
|
||||
bpTinyFootprint = "tinyFootprint"
|
||||
|
||||
proc parseBuiltinPreset(name: string, n: NimNode): BuiltinPreset =
|
||||
case name
|
||||
of "defaultBalanced":
|
||||
bpDefaultBalanced
|
||||
of "fastBurst":
|
||||
bpFastBurst
|
||||
of "largePayload":
|
||||
bpLargePayload
|
||||
of "tinyFootprint":
|
||||
bpTinyFootprint
|
||||
else:
|
||||
error(
|
||||
"Unknown preset '" & name &
|
||||
"'. Built-in presets: defaultBalanced, fastBurst, largePayload, " &
|
||||
"tinyFootprint. (User-defined presets are not yet supported.)",
|
||||
n,
|
||||
)
|
||||
bpDefaultBalanced
|
||||
|
||||
proc applyEvtPreset(cfg: var MtEvtCfg, p: BuiltinPreset) =
|
||||
let tag = "preset:" & $p
|
||||
case p
|
||||
of bpDefaultBalanced:
|
||||
discard # already the default
|
||||
of bpFastBurst:
|
||||
cfg.queueDepth = 4096
|
||||
cfg.slabCapacity = 8192
|
||||
cfg.maxPayloadBytes = 256
|
||||
cfg.freeListShards = 8
|
||||
of bpLargePayload:
|
||||
cfg.queueDepth = 64
|
||||
cfg.slabCapacity = 128
|
||||
cfg.maxPayloadBytes = 64 * 1024
|
||||
cfg.freeListShards = 2
|
||||
of bpTinyFootprint:
|
||||
cfg.queueDepth = 32
|
||||
cfg.slabCapacity = 32
|
||||
cfg.maxPayloadBytes = 256
|
||||
cfg.freeListShards = 1
|
||||
cfg.queueDepthOrigin = tag
|
||||
cfg.slabCapacityOrigin = tag
|
||||
cfg.maxPayloadBytesOrigin = tag
|
||||
cfg.freeListShardsOrigin = tag
|
||||
|
||||
proc applyReqPreset(cfg: var MtReqCfg, p: BuiltinPreset) =
|
||||
let tag = "preset:" & $p
|
||||
case p
|
||||
of bpDefaultBalanced:
|
||||
discard
|
||||
of bpFastBurst:
|
||||
cfg.queueDepth = 4096
|
||||
cfg.slabCapacity = 256
|
||||
cfg.maxPayloadBytes = 256
|
||||
cfg.responseSlots = 1024
|
||||
cfg.maxResponseBytes = 4 * 1024
|
||||
cfg.freeListShards = 4
|
||||
of bpLargePayload:
|
||||
cfg.queueDepth = 64
|
||||
cfg.slabCapacity = 32
|
||||
cfg.maxPayloadBytes = 64 * 1024
|
||||
cfg.responseSlots = 64
|
||||
cfg.maxResponseBytes = 256 * 1024
|
||||
cfg.freeListShards = 2
|
||||
of bpTinyFootprint:
|
||||
cfg.queueDepth = 16
|
||||
cfg.slabCapacity = 8
|
||||
cfg.maxPayloadBytes = 256
|
||||
cfg.responseSlots = 16
|
||||
cfg.maxResponseBytes = 1024
|
||||
cfg.freeListShards = 1
|
||||
cfg.queueDepthOrigin = tag
|
||||
cfg.slabCapacityOrigin = tag
|
||||
cfg.maxPayloadBytesOrigin = tag
|
||||
cfg.responseSlotsOrigin = tag
|
||||
cfg.maxResponseBytesOrigin = tag
|
||||
cfg.freeListShardsOrigin = tag
|
||||
|
||||
proc defaultMtEvtCfg*(): MtEvtCfg =
|
||||
MtEvtCfg(
|
||||
queueDepth: DefaultMtEvtQueueDepth,
|
||||
slabCapacity: DefaultMtEvtSlabCapacity,
|
||||
maxPayloadBytes: DefaultMtEvtMaxPayloadBytes,
|
||||
maxDynamicPayloadBytes: DefaultMtMaxDynamicPayloadBytes,
|
||||
freeListShards: DefaultMtEvtFreeListShards,
|
||||
queueDepthOrigin: "default",
|
||||
slabCapacityOrigin: "default",
|
||||
maxPayloadBytesOrigin: "default",
|
||||
maxDynamicPayloadBytesOrigin: "default",
|
||||
freeListShardsOrigin: "default",
|
||||
)
|
||||
|
||||
proc defaultMtReqCfg*(): MtReqCfg =
|
||||
MtReqCfg(
|
||||
queueDepth: DefaultMtReqQueueDepth,
|
||||
slabCapacity: DefaultMtReqSlabCapacity,
|
||||
maxPayloadBytes: DefaultMtReqMaxPayloadBytes,
|
||||
maxDynamicPayloadBytes: DefaultMtMaxDynamicPayloadBytes,
|
||||
responseSlots: DefaultMtReqResponseSlots,
|
||||
maxResponseBytes: DefaultMtReqMaxResponseBytes,
|
||||
freeListShards: DefaultMtReqFreeListShards,
|
||||
queueDepthOrigin: "default",
|
||||
slabCapacityOrigin: "default",
|
||||
maxPayloadBytesOrigin: "default",
|
||||
maxDynamicPayloadBytesOrigin: "default",
|
||||
responseSlotsOrigin: "default",
|
||||
maxResponseBytesOrigin: "default",
|
||||
freeListShardsOrigin: "default",
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc isPow2(n: int): bool {.inline.} =
|
||||
n > 0 and (n and (n - 1)) == 0
|
||||
|
||||
proc intValOrFail(n: NimNode, kw: string): int =
|
||||
## Extract a compile-time int from a kwarg RHS. Errors clearly on
|
||||
## non-int input.
|
||||
case n.kind
|
||||
of nnkIntLit, nnkInt8Lit, nnkInt16Lit, nnkInt32Lit, nnkInt64Lit, nnkUIntLit,
|
||||
nnkUInt8Lit, nnkUInt16Lit, nnkUInt32Lit, nnkUInt64Lit:
|
||||
int(n.intVal)
|
||||
else:
|
||||
error("broker kwarg '" & kw & "' expects an integer literal, got " & $n.kind, n)
|
||||
0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kwarg parsing — EventBroker(mt)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const ValidEvtKwargs = [
|
||||
"queueDepth", "slabCapacity", "maxPayloadBytes", "maxDynamicPayloadBytes",
|
||||
"freeListShards",
|
||||
]
|
||||
|
||||
proc applyEvtKwarg(cfg: var MtEvtCfg, kw: string, n: NimNode) =
|
||||
case kw
|
||||
of "queueDepth":
|
||||
let v = intValOrFail(n, kw)
|
||||
if not isPow2(v):
|
||||
error("EventBroker kwarg 'queueDepth' must be power-of-2, got " & $v, n)
|
||||
cfg.queueDepth = v
|
||||
cfg.queueDepthOrigin = "kwarg"
|
||||
of "slabCapacity":
|
||||
let v = intValOrFail(n, kw)
|
||||
if v <= 0:
|
||||
error("EventBroker kwarg 'slabCapacity' must be > 0, got " & $v, n)
|
||||
cfg.slabCapacity = v
|
||||
cfg.slabCapacityOrigin = "kwarg"
|
||||
of "maxPayloadBytes":
|
||||
let v = intValOrFail(n, kw)
|
||||
if v <= 0:
|
||||
error("EventBroker kwarg 'maxPayloadBytes' must be > 0, got " & $v, n)
|
||||
cfg.maxPayloadBytes = v
|
||||
cfg.maxPayloadBytesOrigin = "kwarg"
|
||||
of "maxDynamicPayloadBytes":
|
||||
let v = intValOrFail(n, kw)
|
||||
if v <= 0 or v > int(high(uint32)):
|
||||
error(
|
||||
"EventBroker kwarg 'maxDynamicPayloadBytes' must be in 1..high(uint32), got " &
|
||||
$v,
|
||||
n,
|
||||
)
|
||||
cfg.maxDynamicPayloadBytes = v
|
||||
cfg.maxDynamicPayloadBytesOrigin = "kwarg"
|
||||
of "freeListShards":
|
||||
let v = intValOrFail(n, kw)
|
||||
if v <= 0 or v > 64:
|
||||
error("EventBroker kwarg 'freeListShards' must be in 1..64, got " & $v, n)
|
||||
cfg.freeListShards = v
|
||||
cfg.freeListShardsOrigin = "kwarg"
|
||||
else:
|
||||
error(
|
||||
"Unknown EventBroker(mt) kwarg '" & kw & "'. Valid: " & ValidEvtKwargs.join(", "),
|
||||
n,
|
||||
)
|
||||
|
||||
proc presetFromKwargRhs(rhs: NimNode): BuiltinPreset =
|
||||
## Extracts a built-in preset name from a kwarg RHS. Accepts identifier
|
||||
## form (`preset = fastBurst`).
|
||||
if rhs.kind != nnkIdent:
|
||||
error(
|
||||
"preset value must be one of the built-in preset names " &
|
||||
"(defaultBalanced, fastBurst, largePayload, tinyFootprint), got " & $rhs.kind &
|
||||
" — " & rhs.repr,
|
||||
rhs,
|
||||
)
|
||||
parseBuiltinPreset($rhs, rhs)
|
||||
|
||||
proc parseMtEvtKwargs*(kwargs: openArray[NimNode]): MtEvtCfg =
|
||||
## Parses kwarg nodes (everything between `mt` and the trailing body).
|
||||
## Each node must be of shape `nnkExprEqExpr` (`name = value`).
|
||||
##
|
||||
## Order of application:
|
||||
## 1. defaultMtEvtCfg()
|
||||
## 2. `preset = <name>` if present (overrides defaults)
|
||||
## 3. individual kwargs (override the preset)
|
||||
result = defaultMtEvtCfg()
|
||||
for n in kwargs:
|
||||
if n.kind != nnkExprEqExpr:
|
||||
error(
|
||||
"EventBroker(mt) expects kwargs of the form 'name = value', got " & $n.kind &
|
||||
" — " & n.repr,
|
||||
n,
|
||||
)
|
||||
let nameNode = n[0]
|
||||
if nameNode.kind != nnkIdent:
|
||||
error("EventBroker(mt) kwarg name must be an identifier", nameNode)
|
||||
if $nameNode == "preset":
|
||||
applyEvtPreset(result, presetFromKwargRhs(n[1]))
|
||||
for n in kwargs:
|
||||
let name = $n[0]
|
||||
if name == "preset":
|
||||
continue
|
||||
applyEvtKwarg(result, name, n[1])
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kwarg parsing — RequestBroker(mt)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const ValidReqKwargs = [
|
||||
"queueDepth", "slabCapacity", "maxPayloadBytes", "maxDynamicPayloadBytes",
|
||||
"responseSlots", "maxResponseBytes", "freeListShards",
|
||||
]
|
||||
|
||||
proc applyReqKwarg(cfg: var MtReqCfg, kw: string, n: NimNode) =
|
||||
case kw
|
||||
of "queueDepth":
|
||||
let v = intValOrFail(n, kw)
|
||||
if not isPow2(v):
|
||||
error("RequestBroker kwarg 'queueDepth' must be power-of-2, got " & $v, n)
|
||||
cfg.queueDepth = v
|
||||
cfg.queueDepthOrigin = "kwarg"
|
||||
of "slabCapacity":
|
||||
let v = intValOrFail(n, kw)
|
||||
if v <= 0:
|
||||
error("RequestBroker kwarg 'slabCapacity' must be > 0, got " & $v, n)
|
||||
cfg.slabCapacity = v
|
||||
cfg.slabCapacityOrigin = "kwarg"
|
||||
of "maxPayloadBytes":
|
||||
let v = intValOrFail(n, kw)
|
||||
if v <= 0:
|
||||
error("RequestBroker kwarg 'maxPayloadBytes' must be > 0, got " & $v, n)
|
||||
cfg.maxPayloadBytes = v
|
||||
cfg.maxPayloadBytesOrigin = "kwarg"
|
||||
of "maxDynamicPayloadBytes":
|
||||
let v = intValOrFail(n, kw)
|
||||
if v <= 0 or v > int(high(uint32)):
|
||||
error(
|
||||
"RequestBroker kwarg 'maxDynamicPayloadBytes' must be in 1..high(uint32), got " &
|
||||
$v,
|
||||
n,
|
||||
)
|
||||
cfg.maxDynamicPayloadBytes = v
|
||||
cfg.maxDynamicPayloadBytesOrigin = "kwarg"
|
||||
of "responseSlots":
|
||||
let v = intValOrFail(n, kw)
|
||||
if v <= 0:
|
||||
error("RequestBroker kwarg 'responseSlots' must be > 0, got " & $v, n)
|
||||
cfg.responseSlots = v
|
||||
cfg.responseSlotsOrigin = "kwarg"
|
||||
of "maxResponseBytes":
|
||||
let v = intValOrFail(n, kw)
|
||||
if v <= 0:
|
||||
error("RequestBroker kwarg 'maxResponseBytes' must be > 0, got " & $v, n)
|
||||
cfg.maxResponseBytes = v
|
||||
cfg.maxResponseBytesOrigin = "kwarg"
|
||||
of "freeListShards":
|
||||
let v = intValOrFail(n, kw)
|
||||
if v <= 0 or v > 64:
|
||||
error("RequestBroker kwarg 'freeListShards' must be in 1..64, got " & $v, n)
|
||||
cfg.freeListShards = v
|
||||
cfg.freeListShardsOrigin = "kwarg"
|
||||
else:
|
||||
error(
|
||||
"Unknown RequestBroker(mt) kwarg '" & kw & "'. Valid: " & ValidReqKwargs.join(
|
||||
", "
|
||||
),
|
||||
n,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Type-driven default sizing
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Walks a Nim type AST at macro time and recommends a cell payload size.
|
||||
# Triggered when the user did NOT provide an explicit `maxPayloadBytes`
|
||||
# / `maxResponseBytes` kwarg, AND a preset did not set those fields.
|
||||
#
|
||||
# Sizing table (matches doc/MT_BROKER_REFACTOR_RETROSPECTIVE.md §8):
|
||||
#
|
||||
# scalar (bool/intN/uintN/floatN/byte/char/enum/distinct of scalar) 64 B
|
||||
# string (or object whose largest field is string) 4 KB
|
||||
# seq[string] / object containing seq[string] 16 KB
|
||||
# seq[byte] / object containing seq[byte] 64 KB
|
||||
# anything else (alias / external type / unknown ident) 8 KB + warning
|
||||
|
||||
const
|
||||
ScalarBytes* = 64
|
||||
StringBytes* = 4 * 1024
|
||||
SeqStringBytes* = 16 * 1024
|
||||
SeqByteBytes* = 64 * 1024
|
||||
UnclassifiableBytes* = 8 * 1024
|
||||
|
||||
proc classifyTypeSize*(t: NimNode): tuple[bytes: int, reason: string] =
|
||||
## Classifies a type AST into a recommended payload-cell size.
|
||||
## Caller decides what to do with "unclassifiable" (typically: use
|
||||
## the value + emit a warning so the user knows to override).
|
||||
if t.kind == nnkIdent:
|
||||
let name = $t
|
||||
case name
|
||||
of "bool", "char", "byte", "uint", "int", "uint8", "int8", "uint16", "int16",
|
||||
"uint32", "int32", "uint64", "int64", "float", "float32", "float64":
|
||||
(ScalarBytes, "scalar:" & name)
|
||||
of "string":
|
||||
(StringBytes, "string")
|
||||
else:
|
||||
# enum / distinct / alias / external object — can't tell at macro
|
||||
# time without resolving the symbol. Fall back to safe size.
|
||||
(UnclassifiableBytes, "unclassifiable:" & name)
|
||||
elif t.kind == nnkBracketExpr and t.len >= 2 and
|
||||
(t[0].kind == nnkIdent or t[0].kind == nnkDotExpr):
|
||||
# Accept both bare (`Option[T]`) and qualified
|
||||
# (`options.Option[T]`) outer names. For the dotted form we treat
|
||||
# the rightmost ident as the bracket name, while also retaining
|
||||
# the fully qualified form so the existing `options.Option` arm
|
||||
# below still matches when the user writes the full path.
|
||||
let outer =
|
||||
if t[0].kind == nnkIdent:
|
||||
$t[0]
|
||||
else:
|
||||
# nnkDotExpr: lhs.rhs — use rhs as the primary name.
|
||||
if t[0].len >= 2 and t[0][1].kind == nnkIdent:
|
||||
$t[0][1]
|
||||
else:
|
||||
t[0].repr
|
||||
if outer == "seq":
|
||||
let inner = t[1]
|
||||
if inner.kind == nnkIdent:
|
||||
let n = $inner
|
||||
if n == "byte" or n == "uint8":
|
||||
(SeqByteBytes, "seq[byte]")
|
||||
elif n == "string":
|
||||
(SeqStringBytes, "seq[string]")
|
||||
else:
|
||||
# seq[<other>] — assume short list of small items.
|
||||
(StringBytes, "seq[" & n & "]")
|
||||
else:
|
||||
(UnclassifiableBytes, "unclassifiable:" & t.repr)
|
||||
elif outer == "array":
|
||||
# array[N, T] — bounded; treat as the underlying T classification.
|
||||
if t.len >= 3:
|
||||
classifyTypeSize(t[2])
|
||||
else:
|
||||
(UnclassifiableBytes, "unclassifiable:" & t.repr)
|
||||
elif outer == "Option":
|
||||
# Option[T] — wire size is bounded by T plus a one-byte CBOR
|
||||
# tag (null marker vs concrete value). Recurse into the inner
|
||||
# type and reuse its classification verbatim; the +1 byte sits
|
||||
# comfortably inside whatever bucket T lands in. Without this
|
||||
# special case Option[seq[byte]] would silently under-allocate
|
||||
# (8 KB fallback < 64 KB seq[byte]), while Option[int64] would
|
||||
# noisily over-allocate at 8 KB.
|
||||
let inner = classifyTypeSize(t[1])
|
||||
(inner.bytes, "Option[" & inner.reason & "]")
|
||||
else:
|
||||
(UnclassifiableBytes, "unclassifiable:" & outer)
|
||||
else:
|
||||
(UnclassifiableBytes, "unclassifiable:" & $t.kind)
|
||||
|
||||
proc classifyFieldsMax*(
|
||||
fieldTypes: openArray[NimNode]
|
||||
): tuple[bytes: int, reason: string] =
|
||||
## Returns the maximum-size classification across a collection of
|
||||
## field-type ASTs. Used to size the cell for an inline object.
|
||||
var bestBytes = ScalarBytes
|
||||
var bestReason = "scalar"
|
||||
for ft in fieldTypes:
|
||||
let c = classifyTypeSize(ft)
|
||||
if c.bytes > bestBytes:
|
||||
bestBytes = c.bytes
|
||||
bestReason = c.reason
|
||||
(bestBytes, bestReason)
|
||||
|
||||
proc peelFutureResult*(t: NimNode): NimNode =
|
||||
## Walks `Future[Result[T, E]]` and returns T. Returns nil if the
|
||||
## shape doesn't match.
|
||||
var cur = t
|
||||
if cur.kind == nnkBracketExpr and cur.len >= 2 and cur[0].kind == nnkIdent and
|
||||
$cur[0] == "Future":
|
||||
cur = cur[1]
|
||||
if cur.kind == nnkBracketExpr and cur.len >= 2 and cur[0].kind == nnkIdent and
|
||||
($cur[0] == "Result" or $cur[0] == "results.Result"):
|
||||
return cur[1]
|
||||
nil
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Compile-time summary formatting
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc fmtBytes(n: int): string =
|
||||
if n >= 1024 * 1024:
|
||||
$(n div (1024 * 1024)) & "." &
|
||||
align($((n mod (1024 * 1024)) div (102 * 1024)), 1, '0') & " MB"
|
||||
elif n >= 1024:
|
||||
$(n div 1024) & "." & align($((n mod 1024) div 102), 1, '0') & " KB"
|
||||
else:
|
||||
$n & " B"
|
||||
|
||||
# Approximate per-element bytes — match the runtime layouts in mt_queue.nim.
|
||||
# Exact figures don't matter; this is a sizing-guidance number for the user.
|
||||
const
|
||||
RingSlotBytes = 24 # Slot[uint32] = idx u32 + seq u64 + pad
|
||||
CellHeaderBytes = 32 # CellHeader (refcount, length, prev/next idx)
|
||||
RespSlotHeaderBytes = 48 # ResponseSlot header
|
||||
|
||||
proc alignUp8(n: int): int {.inline.} =
|
||||
(n + 7) and (not 7)
|
||||
|
||||
proc estEvtIdleBytes(cfg: MtEvtCfg): tuple[ring, slab, total: int] =
|
||||
let ring = cfg.queueDepth * RingSlotBytes
|
||||
let cellStride = alignUp8(CellHeaderBytes + cfg.maxPayloadBytes)
|
||||
let slab = cfg.slabCapacity * cellStride
|
||||
(ring, slab, ring + slab)
|
||||
|
||||
proc estReqIdleBytes(cfg: MtReqCfg): tuple[ring, slab, respPool, total: int] =
|
||||
let ring = cfg.queueDepth * RingSlotBytes
|
||||
let cellStride = alignUp8(CellHeaderBytes + cfg.maxPayloadBytes)
|
||||
let slab = cfg.slabCapacity * cellStride
|
||||
let slotStride = alignUp8(RespSlotHeaderBytes + cfg.maxResponseBytes)
|
||||
let respPool = cfg.responseSlots * slotStride
|
||||
(ring, slab, respPool, ring + slab + respPool)
|
||||
|
||||
proc fmtEvtCfgSummary*(typeName: string, cfg: MtEvtCfg): string =
|
||||
let est = estEvtIdleBytes(cfg)
|
||||
"[brokers] EventBroker(" & typeName & "): " & "queueDepth=" & $cfg.queueDepth & " [" &
|
||||
cfg.queueDepthOrigin & "], " & "slabCapacity=" & $cfg.slabCapacity & " [" &
|
||||
cfg.slabCapacityOrigin & "], " & "maxPayloadBytes=" & $cfg.maxPayloadBytes & " [" &
|
||||
cfg.maxPayloadBytesOrigin & "], freeListShards=" & $cfg.freeListShards & " [" &
|
||||
cfg.freeListShardsOrigin & "] — idle RAM: ring≈" & fmtBytes(est.ring) &
|
||||
", slab≈" & fmtBytes(est.slab) & ", total≈" & fmtBytes(est.total)
|
||||
|
||||
proc fmtReqCfgSummary*(typeName: string, cfg: MtReqCfg): string =
|
||||
let est = estReqIdleBytes(cfg)
|
||||
"[brokers] RequestBroker(" & typeName & "): " & "queueDepth=" & $cfg.queueDepth & " [" &
|
||||
cfg.queueDepthOrigin & "], " & "slabCapacity=" & $cfg.slabCapacity & " [" &
|
||||
cfg.slabCapacityOrigin & "], " & "maxPayloadBytes=" & $cfg.maxPayloadBytes & " [" &
|
||||
cfg.maxPayloadBytesOrigin & "], responseSlots=" & $cfg.responseSlots & " [" &
|
||||
cfg.responseSlotsOrigin & "], maxResponseBytes=" & $cfg.maxResponseBytes & " [" &
|
||||
cfg.maxResponseBytesOrigin & "], freeListShards=" & $cfg.freeListShards & " [" &
|
||||
cfg.freeListShardsOrigin & "] — idle RAM: ring≈" & fmtBytes(est.ring) &
|
||||
", slab≈" & fmtBytes(est.slab) & ", respPool≈" & fmtBytes(est.respPool) &
|
||||
", total≈" & fmtBytes(est.total)
|
||||
|
||||
proc parseMtReqKwargs*(kwargs: openArray[NimNode]): MtReqCfg =
|
||||
## See `parseMtEvtKwargs` for order-of-application rules.
|
||||
result = defaultMtReqCfg()
|
||||
for n in kwargs:
|
||||
if n.kind != nnkExprEqExpr:
|
||||
error(
|
||||
"RequestBroker(mt) expects kwargs of the form 'name = value', got " & $n.kind &
|
||||
" — " & n.repr,
|
||||
n,
|
||||
)
|
||||
let nameNode = n[0]
|
||||
if nameNode.kind != nnkIdent:
|
||||
error("RequestBroker(mt) kwarg name must be an identifier", nameNode)
|
||||
if $nameNode == "preset":
|
||||
applyReqPreset(result, presetFromKwargRhs(n[1]))
|
||||
for n in kwargs:
|
||||
let name = $n[0]
|
||||
if name == "preset":
|
||||
continue
|
||||
applyReqKwarg(result, name, n[1])
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Splitting varargs into kwargs + body
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc splitMtArgs*(
|
||||
args: NimNode, what: string
|
||||
): tuple[kwargs: seq[NimNode], body: NimNode] =
|
||||
## Splits a `varargs[untyped]` macro arg list into (kwarg nodes, body
|
||||
## stmt-list). The body is always the last element. Errors if no body.
|
||||
if args.len == 0:
|
||||
error(what & " requires a body block", args)
|
||||
let bodyNode = args[args.len - 1]
|
||||
if bodyNode.kind notin {nnkStmtList, nnkTypeDef, nnkTypeSection}:
|
||||
error(
|
||||
what & " body must be a `:` block of type definitions (got " & $bodyNode.kind & ")",
|
||||
bodyNode,
|
||||
)
|
||||
var kw = newSeqOfCap[NimNode](args.len - 1)
|
||||
for i in 0 ..< args.len - 1:
|
||||
kw.add(args[i])
|
||||
(kw, bodyNode)
|
||||
|
||||
{.pop.} # warning[UnreachableCode]
|
||||
{.pop.} # raises: []
|
||||
@@ -0,0 +1,935 @@
|
||||
## Multi-Thread EventBroker
|
||||
## ------------------------
|
||||
## Generates a multi-thread capable EventBroker where listeners can be
|
||||
## registered on any thread and events can be emitted from any thread.
|
||||
## Events are delivered to all registered listeners across all threads
|
||||
## (broadcast fan-out).
|
||||
##
|
||||
## Same-thread emit→listener dispatch bypasses the ring and is delivered
|
||||
## directly via `asyncSpawn`. Cross-thread delivery uses a lock-free
|
||||
## Vyukov MPSC ring + a global per-broker-type slab with refcounted
|
||||
## payload cells (so one emit shares one cell across N listener threads
|
||||
## via atomic refcount, rather than N deep-copies).
|
||||
##
|
||||
## See `doc/REFACTOR_MT_QUEUE.md` for the full design; this file is the
|
||||
## EventBroker integration of Phase 2+3 of that plan.
|
||||
##
|
||||
## §2.6 safety contract honored by construction (Invariant I0):
|
||||
## - The bucket-owning thread (the listener thread) allocates its ring
|
||||
## via `createShared` and frees it via `shutdown(ctx)` on the same
|
||||
## thread.
|
||||
## - The global event slab is allocated lazily, by whichever thread
|
||||
## first calls `listen()` or `emit()`. That thread MUST outlive the
|
||||
## slab.
|
||||
## - Sender threads only ever touch atomics + memcpy + signal-fire —
|
||||
## never the Nim allocator on the hot path.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[macros, strutils, locks, tables, atomics]
|
||||
import chronos, chronicles
|
||||
import results
|
||||
import
|
||||
./helper/broker_utils,
|
||||
../broker_context,
|
||||
./mt_broker_common,
|
||||
./mt_queue,
|
||||
./mt_codec,
|
||||
./mt_config,
|
||||
./broker_debug
|
||||
|
||||
export results, chronos, broker_context, chronicles, mt_broker_common, mt_config
|
||||
|
||||
# Ring-slot sentinel: a slot's payload `uint32` is normally a slab cell
|
||||
# index, but this reserved value carries a "clear local tvHandlers"
|
||||
# control signal instead. Shutdown is communicated via the ring's
|
||||
# `closed` flag, not a sentinel, because `tryEnqueue` rejects when
|
||||
# closed and we don't want shutdown to compete with that.
|
||||
#
|
||||
# The sentinel lives in the same namespace as cell indices and MUST be
|
||||
# larger than any legal slab capacity (bounded by uint32 in practice).
|
||||
const CtrlClearListeners*: uint32 = high(uint32) - 1
|
||||
|
||||
# Capacity defaults moved to `mt_config.nim`; they remain re-exported via
|
||||
# the `mt_config` module so external code referencing
|
||||
# `DefaultMtEvtQueueDepth` etc. still resolves.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Macro code generator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc generateMtEventBroker*(
|
||||
body: NimNode, cfgIn: MtEvtCfg = defaultMtEvtCfg()
|
||||
): NimNode =
|
||||
when defined(brokerDebug):
|
||||
echo body.treeRepr
|
||||
echo "EventBroker mode: mt"
|
||||
|
||||
let parsed = parseSingleTypeDef(body, "EventBroker", collectFieldInfo = true)
|
||||
let typeIdent = parsed.typeIdent
|
||||
let objectDef = parsed.objectDef
|
||||
let fieldNames = parsed.fieldNames
|
||||
let fieldTypes = parsed.fieldTypes
|
||||
let hasInlineFields = parsed.hasInlineFields
|
||||
|
||||
let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*")
|
||||
let typeDisplayName = sanitizeIdentName(typeIdent)
|
||||
let typeNameLit = newLit(typeDisplayName)
|
||||
|
||||
# Apply type-driven default for maxPayloadBytes when neither a kwarg
|
||||
# nor a preset set it. Warn if the type is unclassifiable so the user
|
||||
# knows to provide an explicit override. Void / zero-field bodies
|
||||
# collapse to the scalar bucket: a payload-less notification only
|
||||
# ships the CBOR envelope (a handful of bytes), and the conservative
|
||||
# 1 KB default would otherwise pin a full megabyte slab per event
|
||||
# type for no reason.
|
||||
var cfg = cfgIn
|
||||
if cfg.maxPayloadBytesOrigin == "default":
|
||||
if fieldTypes.len > 0:
|
||||
let cls = classifyFieldsMax(fieldTypes)
|
||||
cfg.maxPayloadBytes = cls.bytes
|
||||
cfg.maxPayloadBytesOrigin = "auto:" & cls.reason
|
||||
if cls.reason.startsWith("unclassifiable"):
|
||||
warning(
|
||||
"[brokers] EventBroker(" & typeDisplayName & ") could not auto-size payload (" &
|
||||
cls.reason & "); falling back to " & $cls.bytes &
|
||||
" B. Override with `maxPayloadBytes = N`."
|
||||
)
|
||||
else:
|
||||
cfg.maxPayloadBytes = ScalarBytes
|
||||
cfg.maxPayloadBytesOrigin = "auto:void"
|
||||
|
||||
when not defined(brokerConfigSilent):
|
||||
hint(fmtEvtCfgSummary(typeDisplayName, cfg))
|
||||
|
||||
# ── Identifier setup ──────────────────────────────────────────────────
|
||||
let handlerProcIdent = ident(typeDisplayName & "ListenerProc")
|
||||
let listenerHandleIdent = ident(typeDisplayName & "Listener")
|
||||
let exportedHandlerProcIdent = postfix(copyNimTree(handlerProcIdent), "*")
|
||||
let exportedListenerHandleIdent = postfix(copyNimTree(listenerHandleIdent), "*")
|
||||
|
||||
let bucketName = ident(typeDisplayName & "MtEventBucket")
|
||||
|
||||
let globalBucketsIdent = ident("g" & typeDisplayName & "MtBuckets")
|
||||
let globalBucketCountIdent = ident("g" & typeDisplayName & "MtBucketCount")
|
||||
let globalBucketCapIdent = ident("g" & typeDisplayName & "MtBucketCap")
|
||||
let globalLockIdent = ident("g" & typeDisplayName & "MtLock")
|
||||
let globalInitIdent = ident("g" & typeDisplayName & "MtInit")
|
||||
|
||||
let globalSlabIdent = ident("g" & typeDisplayName & "MtSlab")
|
||||
let globalSlabInitIdent = ident("g" & typeDisplayName & "MtSlabInit")
|
||||
|
||||
let initProcIdent = ident("ensureInit" & typeDisplayName & "MtBroker")
|
||||
let initSlabProcIdent = ident("ensureSlab" & typeDisplayName & "MtBroker")
|
||||
let growProcIdent = ident("grow" & typeDisplayName & "MtBuckets")
|
||||
let listenerTaskIdent = ident("notify" & typeDisplayName & "Listener")
|
||||
let pollFnMakerIdent = ident("makePollFn" & typeDisplayName)
|
||||
let clearListenersIdent = ident("clearListeners" & typeDisplayName)
|
||||
let releaseCellIdent = ident("releaseCell" & typeDisplayName)
|
||||
let shardHintIdent = ident("shardHint" & typeDisplayName)
|
||||
|
||||
let marshalIdent = ident(typeDisplayName & "MtMarshal")
|
||||
let unmarshalIdent = ident(typeDisplayName & "MtUnmarshal")
|
||||
let marshalSizeIdent = ident(typeDisplayName & "MtMarshalSize")
|
||||
|
||||
let tvListenerCtxIdent = ident("g" & typeDisplayName & "TvListenerCtxs")
|
||||
let tvListenerHandlersIdent = ident("g" & typeDisplayName & "TvListenerHandlers")
|
||||
let tvNextIdsIdent = ident("g" & typeDisplayName & "TvNextIds")
|
||||
let tvListenerFutsIdent = ident("g" & typeDisplayName & "TvListenerFuts")
|
||||
let tvShutdownFutsIdent = ident("g" & typeDisplayName & "TvShutdownFuts")
|
||||
|
||||
let listenImplIdent = ident("listen" & typeDisplayName & "MtImpl")
|
||||
let emitImplIdent = ident("emit" & typeDisplayName & "MtImpl")
|
||||
let dropListenerImplIdent = ident("drop" & typeDisplayName & "MtListenerImpl")
|
||||
let dropAllListenersImplIdent = ident("dropAll" & typeDisplayName & "MtListenersImpl")
|
||||
# Part D-3: optional companion hook fired by `dropAllListenersImpl`
|
||||
# after listener clearing completes. Used by the CBOR FFI library
|
||||
# (`api_library.nim`) to clear the foreign-subscriber registry +
|
||||
# reset the per-event atomic counter in lock-step with Nim-side
|
||||
# listener drops. Single slot per type — the only intended user is
|
||||
# the per-event installer registered at `_createContext` time.
|
||||
let dropAllHookProcTypeIdent = ident(typeDisplayName & "MtDropAllHook")
|
||||
let dropAllHookIdent = ident("g" & typeDisplayName & "MtDropAllHook")
|
||||
let dropAllHookLockIdent = ident("g" & typeDisplayName & "MtDropAllHookLock")
|
||||
let dropAllHookInitIdent = ident("g" & typeDisplayName & "MtDropAllHookInit")
|
||||
let setDropAllHookIdent = ident("setDropAll" & typeDisplayName & "Hook")
|
||||
let shutdownProcessLoopsForCtxIdent =
|
||||
ident("shutdownProcessLoopsForCtx" & typeDisplayName)
|
||||
|
||||
let queueDepthLit = newLit(cfg.queueDepth)
|
||||
let slabCapacityLit = newLit(cfg.slabCapacity)
|
||||
let payloadBytesLit = newLit(cfg.maxPayloadBytes)
|
||||
let maxDynPayloadLit = newLit(cfg.maxDynamicPayloadBytes)
|
||||
let freeListShardsLit = newLit(uint32(cfg.freeListShards))
|
||||
|
||||
result = newStmtList()
|
||||
|
||||
# ── Type section ──────────────────────────────────────────────────────
|
||||
result.add(
|
||||
quote do:
|
||||
type
|
||||
`exportedTypeIdent` = `objectDef`
|
||||
`exportedListenerHandleIdent` = object
|
||||
id*: uint64
|
||||
threadId*: pointer ## Thread that registered this listener.
|
||||
|
||||
`exportedHandlerProcIdent` =
|
||||
proc(event: `typeIdent`): Future[void] {.async: (raises: []), gcsafe.}
|
||||
|
||||
`dropAllHookProcTypeIdent` =
|
||||
proc(brokerCtx: BrokerContext) {.gcsafe, raises: [].}
|
||||
|
||||
`bucketName` = object
|
||||
brokerCtx: BrokerContext
|
||||
ring: ptr VyukovMpscRing[uint32]
|
||||
listenerSignal: ThreadSignalPtr
|
||||
threadId: pointer
|
||||
threadGen: uint64 ## disambiguates reused threadvar addresses
|
||||
active: bool
|
||||
hasListeners: bool
|
||||
|
||||
)
|
||||
|
||||
# ── Codec procs (marshal / unmarshal) ─────────────────────────────────
|
||||
for procNode in genMtCodecProcs(marshalIdent, unmarshalIdent, typeIdent):
|
||||
result.add(procNode)
|
||||
|
||||
# ── Global shared state ───────────────────────────────────────────────
|
||||
result.add(
|
||||
quote do:
|
||||
var `globalBucketsIdent`: ptr UncheckedArray[`bucketName`]
|
||||
var `globalBucketCountIdent`: int
|
||||
var `globalBucketCapIdent`: int
|
||||
var `globalLockIdent`: Lock
|
||||
var `globalInitIdent`: Atomic[int]
|
||||
## 0 = uninitialised, 1 = initialising, 2 = ready. CAS(0→1) wins;
|
||||
## losers spin until 2.
|
||||
var `globalSlabIdent`: PayloadSlab
|
||||
var `globalSlabInitIdent`: Atomic[int]
|
||||
# Part D-3 dropAllListeners hook. Single slot per event type;
|
||||
# `dropAllHookIdent` is `nil` when no hook is registered.
|
||||
var `dropAllHookIdent`: `dropAllHookProcTypeIdent`
|
||||
var `dropAllHookLockIdent`: Lock
|
||||
var `dropAllHookInitIdent`: Atomic[int]
|
||||
## same protocol as `globalInitIdent`, gating the global slab.
|
||||
)
|
||||
|
||||
# ── Init helpers ──────────────────────────────────────────────────────
|
||||
result.add(
|
||||
quote do:
|
||||
proc `initSlabProcIdent`() =
|
||||
## Lazy-init the global event slab on first listen() or emit().
|
||||
## The caller's thread becomes the slab's owner (must outlive it).
|
||||
if `globalSlabInitIdent`.load(moRelaxed) == 2:
|
||||
return
|
||||
var expected = 0
|
||||
if `globalSlabInitIdent`.compareExchange(expected, 1, moAcquire, moRelaxed):
|
||||
initPayloadSlab(
|
||||
`globalSlabIdent`,
|
||||
capacity = uint32(`slabCapacityLit`),
|
||||
payloadBytes = uint32(`payloadBytesLit`),
|
||||
nShards = `freeListShardsLit`,
|
||||
)
|
||||
`globalSlabInitIdent`.store(2, moRelease)
|
||||
else:
|
||||
while `globalSlabInitIdent`.load(moAcquire) != 2:
|
||||
discard
|
||||
|
||||
proc `initProcIdent`() =
|
||||
if `globalInitIdent`.load(moRelaxed) == 2:
|
||||
`initSlabProcIdent`()
|
||||
return
|
||||
var expected = 0
|
||||
if `globalInitIdent`.compareExchange(expected, 1, moAcquire, moRelaxed):
|
||||
initLock(`globalLockIdent`)
|
||||
`globalBucketCapIdent` = 4
|
||||
`globalBucketsIdent` = cast[ptr UncheckedArray[`bucketName`]](createShared(
|
||||
`bucketName`, `globalBucketCapIdent`
|
||||
))
|
||||
`globalBucketCountIdent` = 0
|
||||
# Part D-3 dropAllListeners hook storage init. Same one-shot
|
||||
# CAS-init protocol as the main globals so concurrent callers
|
||||
# see an initialised lock before any reader/writer touches it.
|
||||
var hookExpected = 0
|
||||
if `dropAllHookInitIdent`.compareExchange(
|
||||
hookExpected, 1, moAcquire, moRelaxed
|
||||
):
|
||||
initLock(`dropAllHookLockIdent`)
|
||||
`dropAllHookInitIdent`.store(2, moRelease)
|
||||
else:
|
||||
while `dropAllHookInitIdent`.load(moAcquire) != 2:
|
||||
discard
|
||||
`globalInitIdent`.store(2, moRelease)
|
||||
else:
|
||||
while `globalInitIdent`.load(moAcquire) != 2:
|
||||
discard
|
||||
`initSlabProcIdent`()
|
||||
|
||||
)
|
||||
|
||||
# ── Grow helper ───────────────────────────────────────────────────────
|
||||
result.add(
|
||||
quote do:
|
||||
proc `growProcIdent`() =
|
||||
## Must be called under lock.
|
||||
let newCap = `globalBucketCapIdent` * 2
|
||||
let newBuf =
|
||||
cast[ptr UncheckedArray[`bucketName`]](createShared(`bucketName`, newCap))
|
||||
for i in 0 ..< `globalBucketCountIdent`:
|
||||
newBuf[i] = `globalBucketsIdent`[i]
|
||||
# Intentional leak of the old buffer: see mt_request_broker.nim.
|
||||
`globalBucketsIdent` = newBuf
|
||||
`globalBucketCapIdent` = newCap
|
||||
|
||||
)
|
||||
|
||||
# ── Threadvar listener storage ────────────────────────────────────────
|
||||
result.add(
|
||||
quote do:
|
||||
var `tvListenerCtxIdent` {.threadvar.}: seq[BrokerContext]
|
||||
var `tvListenerHandlersIdent` {.threadvar.}:
|
||||
seq[Table[uint64, `handlerProcIdent`]]
|
||||
var `tvNextIdsIdent` {.threadvar.}: seq[uint64]
|
||||
var `tvListenerFutsIdent` {.threadvar.}: seq[(BrokerContext, Future[void])]
|
||||
var `tvShutdownFutsIdent` {.threadvar.}: seq[(BrokerContext, Future[void])]
|
||||
)
|
||||
|
||||
# ── Listener task ─────────────────────────────────────────────────────
|
||||
result.add(
|
||||
quote do:
|
||||
proc `listenerTaskIdent`(
|
||||
callback: `handlerProcIdent`, event: `typeIdent`
|
||||
): Future[void] {.async: (raises: []).} =
|
||||
if callback.isNil():
|
||||
return
|
||||
try:
|
||||
await callback(event)
|
||||
except CatchableError:
|
||||
error "Failed to execute event listener",
|
||||
eventType = `typeNameLit`, error = getCurrentExceptionMsg()
|
||||
|
||||
)
|
||||
|
||||
# ── Local helpers used by both same-thread emit and cross-thread poll
|
||||
result.add(
|
||||
quote do:
|
||||
proc `shardHintIdent`(): uint32 {.inline.} =
|
||||
## Hash of the calling thread's TLS marker → free-list shard.
|
||||
cast[uint32](cast[uint](currentMtThreadId()) shr 4)
|
||||
|
||||
proc `clearListenersIdent`(loopCtx: BrokerContext) {.gcsafe, raises: [].} =
|
||||
{.cast(gcsafe).}:
|
||||
for i in 0 ..< `tvListenerCtxIdent`.len:
|
||||
if `tvListenerCtxIdent`[i] == loopCtx:
|
||||
`tvListenerHandlersIdent`[i].clear()
|
||||
`tvListenerCtxIdent`.del(i)
|
||||
`tvListenerHandlersIdent`.del(i)
|
||||
`tvNextIdsIdent`.del(i)
|
||||
break
|
||||
|
||||
proc `releaseCellIdent`(cellIdx: uint32) {.inline, gcsafe.} =
|
||||
if `globalSlabIdent`.decRefAndCheck(cellIdx):
|
||||
`globalSlabIdent`.release(cellIdx, `shardHintIdent`())
|
||||
|
||||
)
|
||||
|
||||
# ── Poll fn maker ─────────────────────────────────────────────────────
|
||||
result.add(
|
||||
quote do:
|
||||
proc `pollFnMakerIdent`(
|
||||
ring: ptr VyukovMpscRing[uint32],
|
||||
loopCtx: BrokerContext,
|
||||
shutdownFut: Future[void],
|
||||
): ThreadDispatchPollFn =
|
||||
let capturedRing = ring
|
||||
let capturedCtx = loopCtx
|
||||
let capturedShutdownFut = shutdownFut
|
||||
return proc(): int {.gcsafe, raises: [].} =
|
||||
{.cast(gcsafe).}:
|
||||
var cellIdx: uint32
|
||||
if not capturedRing.tryDequeue(cellIdx):
|
||||
# Empty. If the ring has been closed by shutdown, this is
|
||||
# the definitive "drained" point (no more producers can
|
||||
# enqueue past `closed=true`). Complete the shutdown
|
||||
# future and self-unregister.
|
||||
if capturedRing.isClosed():
|
||||
if not capturedShutdownFut.finished:
|
||||
capturedShutdownFut.complete()
|
||||
return 2
|
||||
return 0
|
||||
case cellIdx
|
||||
of CtrlClearListeners:
|
||||
`clearListenersIdent`(capturedCtx)
|
||||
return 1
|
||||
else:
|
||||
# Normal cell: decode, dispatch, decRef. dataPtr/dataLen resolve
|
||||
# the heap-spill buffer when the payload spilled, else the inline
|
||||
# cell region.
|
||||
var ev: `typeIdent`
|
||||
let payloadPtr = `globalSlabIdent`.dataPtr(cellIdx)
|
||||
let payloadLen = `globalSlabIdent`.dataLen(cellIdx)
|
||||
let ok =
|
||||
try:
|
||||
`unmarshalIdent`(payloadPtr, payloadLen, ev)
|
||||
except Exception:
|
||||
false
|
||||
if ok:
|
||||
var idx = -1
|
||||
for i in 0 ..< `tvListenerCtxIdent`.len:
|
||||
if `tvListenerCtxIdent`[i] == capturedCtx:
|
||||
idx = i
|
||||
break
|
||||
if idx >= 0:
|
||||
var callbacks: seq[`handlerProcIdent`] = @[]
|
||||
for cb in `tvListenerHandlersIdent`[idx].values:
|
||||
callbacks.add(cb)
|
||||
for cb in callbacks:
|
||||
let fut: Future[void] = `listenerTaskIdent`(cb, ev)
|
||||
`tvListenerFutsIdent`.add((capturedCtx, fut))
|
||||
asyncSpawn fut
|
||||
else:
|
||||
error "Failed to unmarshal event payload", eventType = `typeNameLit`
|
||||
`releaseCellIdent`(cellIdx)
|
||||
return 1
|
||||
|
||||
)
|
||||
|
||||
# ── listen impl ──────────────────────────────────────────────────────
|
||||
result.add(
|
||||
quote do:
|
||||
proc `listenImplIdent`(
|
||||
brokerCtx: BrokerContext, handler: `handlerProcIdent`
|
||||
): Result[`listenerHandleIdent`, string] =
|
||||
if handler.isNil():
|
||||
return err("Must provide a non-nil event handler")
|
||||
`initProcIdent`()
|
||||
|
||||
var tvIdx = -1
|
||||
for i in 0 ..< `tvListenerCtxIdent`.len:
|
||||
if `tvListenerCtxIdent`[i] == brokerCtx:
|
||||
tvIdx = i
|
||||
break
|
||||
if tvIdx < 0:
|
||||
`tvListenerCtxIdent`.add(brokerCtx)
|
||||
`tvListenerHandlersIdent`.add(initTable[uint64, `handlerProcIdent`]())
|
||||
`tvNextIdsIdent`.add(1'u64)
|
||||
tvIdx = `tvListenerCtxIdent`.len - 1
|
||||
|
||||
if `tvNextIdsIdent`[tvIdx] == high(uint64):
|
||||
return err("Cannot add more listeners: ID space exhausted")
|
||||
let newId = `tvNextIdsIdent`[tvIdx]
|
||||
`tvNextIdsIdent`[tvIdx] += 1
|
||||
`tvListenerHandlersIdent`[tvIdx][newId] = handler
|
||||
|
||||
# Ensure a bucket + ring exists for (brokerCtx, this thread).
|
||||
let myThreadId = currentMtThreadId()
|
||||
let myThreadGen = currentMtThreadGen()
|
||||
var bucketExists = false
|
||||
var spawnRing: ptr VyukovMpscRing[uint32]
|
||||
withLock(`globalLockIdent`):
|
||||
for i in 0 ..< `globalBucketCountIdent`:
|
||||
if `globalBucketsIdent`[i].brokerCtx == brokerCtx and
|
||||
`globalBucketsIdent`[i].threadId == myThreadId and
|
||||
`globalBucketsIdent`[i].threadGen == myThreadGen:
|
||||
`globalBucketsIdent`[i].hasListeners = true
|
||||
`globalBucketsIdent`[i].active = true
|
||||
bucketExists = true
|
||||
break
|
||||
if not bucketExists:
|
||||
if `globalBucketCountIdent` >= `globalBucketCapIdent`:
|
||||
`growProcIdent`()
|
||||
let ring = newVyukovMpscRing[uint32](`queueDepthLit`)
|
||||
let listenerSig = getOrInitBrokerSignal()
|
||||
let idx = `globalBucketCountIdent`
|
||||
`globalBucketsIdent`[idx] = `bucketName`(
|
||||
brokerCtx: brokerCtx,
|
||||
ring: ring,
|
||||
listenerSignal: listenerSig,
|
||||
threadId: myThreadId,
|
||||
threadGen: myThreadGen,
|
||||
active: true,
|
||||
hasListeners: true,
|
||||
)
|
||||
`globalBucketCountIdent` += 1
|
||||
spawnRing = ring
|
||||
|
||||
if not bucketExists and not spawnRing.isNil:
|
||||
let shutdownFut =
|
||||
newFuture[void]("eventBroker." & `typeNameLit` & ".shutdown")
|
||||
`tvShutdownFutsIdent`.add((brokerCtx, shutdownFut))
|
||||
registerBrokerPoller(`pollFnMakerIdent`(spawnRing, brokerCtx, shutdownFut))
|
||||
ensureBrokerDispatchStarted()
|
||||
|
||||
return ok(`listenerHandleIdent`(id: newId, threadId: myThreadId))
|
||||
|
||||
)
|
||||
|
||||
# ── Public listen ─────────────────────────────────────────────────────
|
||||
result.add(
|
||||
quote do:
|
||||
proc listen*(
|
||||
_: typedesc[`typeIdent`], handler: `handlerProcIdent`
|
||||
): Result[`listenerHandleIdent`, string] =
|
||||
return `listenImplIdent`(DefaultBrokerContext, handler)
|
||||
|
||||
proc listen*(
|
||||
_: typedesc[`typeIdent`],
|
||||
brokerCtx: BrokerContext,
|
||||
handler: `handlerProcIdent`,
|
||||
): Result[`listenerHandleIdent`, string] =
|
||||
return `listenImplIdent`(brokerCtx, handler)
|
||||
|
||||
)
|
||||
|
||||
# ── emit impl ─────────────────────────────────────────────────────────
|
||||
result.add(
|
||||
quote do:
|
||||
proc `emitImplIdent`(
|
||||
brokerCtx: BrokerContext, event: `typeIdent`
|
||||
) {.async: (raises: []).} =
|
||||
`initProcIdent`()
|
||||
|
||||
when compiles(event.isNil()):
|
||||
if event.isNil():
|
||||
error "Cannot emit uninitialized event object", eventType = `typeNameLit`
|
||||
return
|
||||
|
||||
type CrossTarget = object
|
||||
ring: ptr VyukovMpscRing[uint32]
|
||||
signal: ThreadSignalPtr
|
||||
|
||||
var crossTargets: seq[CrossTarget] = @[]
|
||||
var hasSameThread = false
|
||||
let myThreadId = currentMtThreadId()
|
||||
let myThreadGen = currentMtThreadGen()
|
||||
|
||||
withLock(`globalLockIdent`):
|
||||
for i in 0 ..< `globalBucketCountIdent`:
|
||||
if `globalBucketsIdent`[i].brokerCtx == brokerCtx and
|
||||
`globalBucketsIdent`[i].active and `globalBucketsIdent`[i].hasListeners:
|
||||
if `globalBucketsIdent`[i].threadId == myThreadId and
|
||||
`globalBucketsIdent`[i].threadGen == myThreadGen:
|
||||
hasSameThread = true
|
||||
else:
|
||||
crossTargets.add(
|
||||
CrossTarget(
|
||||
ring: `globalBucketsIdent`[i].ring,
|
||||
signal: `globalBucketsIdent`[i].listenerSignal,
|
||||
)
|
||||
)
|
||||
|
||||
# Same-thread fast path: bypass ring entirely.
|
||||
if hasSameThread:
|
||||
var idx = -1
|
||||
for i in 0 ..< `tvListenerCtxIdent`.len:
|
||||
if `tvListenerCtxIdent`[i] == brokerCtx:
|
||||
idx = i
|
||||
break
|
||||
if idx >= 0:
|
||||
var callbacks: seq[`handlerProcIdent`] = @[]
|
||||
for cb in `tvListenerHandlersIdent`[idx].values:
|
||||
callbacks.add(cb)
|
||||
for cb in callbacks:
|
||||
let fut: Future[void] = `listenerTaskIdent`(cb, event)
|
||||
`tvListenerFutsIdent`.add((brokerCtx, fut))
|
||||
asyncSpawn fut
|
||||
|
||||
if crossTargets.len == 0:
|
||||
return
|
||||
|
||||
# Cross-thread fan-out via shared refcounted cell.
|
||||
let shardHint = `shardHintIdent`()
|
||||
let cellIdx = `globalSlabIdent`.claim(shardHint)
|
||||
if cellIdx == EmptyIdx:
|
||||
warn "event dropped: slab exhausted",
|
||||
eventType = `typeNameLit`, targets = crossTargets.len
|
||||
return
|
||||
|
||||
let cell = `globalSlabIdent`.cellPtr(cellIdx)
|
||||
let payloadPtr = `globalSlabIdent`.cellPayloadPtr(cellIdx)
|
||||
let written =
|
||||
try:
|
||||
`marshalIdent`(payloadPtr, int(`globalSlabIdent`.cellPayloadCap), event)
|
||||
except Exception:
|
||||
-1
|
||||
if written >= 0:
|
||||
# Fast path: payload fit the fixed cell.
|
||||
cell.payloadSize = uint32(written)
|
||||
else:
|
||||
# Auto-spill: payload exceeded the cell — marshal into an exact-size
|
||||
# heap buffer instead of dropping. Owned by the cell; freed at release.
|
||||
let needed =
|
||||
try:
|
||||
`marshalSizeIdent`(event)
|
||||
except Exception:
|
||||
-1
|
||||
if needed < 0 or needed > `maxDynPayloadLit`:
|
||||
error "event dropped: payload exceeds maxDynamicPayloadBytes",
|
||||
eventType = `typeNameLit`, needed = needed, cap = `maxDynPayloadLit`
|
||||
`globalSlabIdent`.release(cellIdx, shardHint)
|
||||
return
|
||||
let spillBuf = allocShared0(needed)
|
||||
if spillBuf.isNil:
|
||||
error "event dropped: spill allocation failed",
|
||||
eventType = `typeNameLit`, needed = needed
|
||||
`globalSlabIdent`.release(cellIdx, shardHint)
|
||||
return
|
||||
let w2 =
|
||||
try:
|
||||
`marshalIdent`(cast[ptr UncheckedArray[byte]](spillBuf), needed, event)
|
||||
except Exception:
|
||||
-1
|
||||
if w2 < 0:
|
||||
deallocShared(spillBuf)
|
||||
`globalSlabIdent`.release(cellIdx, shardHint)
|
||||
return
|
||||
`globalSlabIdent`.setOverflow(cellIdx, spillBuf, uint32(w2))
|
||||
cell.refcount.store(crossTargets.len, moRelease)
|
||||
|
||||
for target in crossTargets:
|
||||
if not target.ring.tryEnqueue(cellIdx):
|
||||
warn "event dropped: listener queue full", eventType = `typeNameLit`
|
||||
`releaseCellIdent`(cellIdx)
|
||||
else:
|
||||
fireBrokerSignal(target.signal)
|
||||
|
||||
)
|
||||
|
||||
# ── Public emit ───────────────────────────────────────────────────────
|
||||
result.add(
|
||||
quote do:
|
||||
proc emit*(event: `typeIdent`) {.async: (raises: []).} =
|
||||
await `emitImplIdent`(DefaultBrokerContext, event)
|
||||
|
||||
proc emit*(_: typedesc[`typeIdent`], event: `typeIdent`) {.async: (raises: []).} =
|
||||
await `emitImplIdent`(DefaultBrokerContext, event)
|
||||
|
||||
proc emit*(
|
||||
_: typedesc[`typeIdent`], brokerCtx: BrokerContext, event: `typeIdent`
|
||||
) {.async: (raises: []).} =
|
||||
await `emitImplIdent`(brokerCtx, event)
|
||||
|
||||
)
|
||||
|
||||
# ── Field-constructor emit overloads (for inline object types) ────────
|
||||
if hasInlineFields:
|
||||
let typedescParamType =
|
||||
newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent))
|
||||
|
||||
let asyncPragma = newTree(
|
||||
nnkPragma,
|
||||
newTree(
|
||||
nnkExprColonExpr,
|
||||
ident("async"),
|
||||
newTree(
|
||||
nnkTupleConstr,
|
||||
newTree(nnkExprColonExpr, ident("raises"), newTree(nnkBracket)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
var emitCtorParams = newTree(nnkFormalParams, newEmptyNode())
|
||||
emitCtorParams.add(
|
||||
newTree(nnkIdentDefs, ident("_"), typedescParamType, newEmptyNode())
|
||||
)
|
||||
for i in 0 ..< fieldNames.len:
|
||||
emitCtorParams.add(
|
||||
newTree(
|
||||
nnkIdentDefs,
|
||||
copyNimTree(fieldNames[i]),
|
||||
copyNimTree(fieldTypes[i]),
|
||||
newEmptyNode(),
|
||||
)
|
||||
)
|
||||
|
||||
var emitCtorExpr = newTree(nnkObjConstr, copyNimTree(typeIdent))
|
||||
for i in 0 ..< fieldNames.len:
|
||||
emitCtorExpr.add(
|
||||
newTree(
|
||||
nnkExprColonExpr, copyNimTree(fieldNames[i]), copyNimTree(fieldNames[i])
|
||||
)
|
||||
)
|
||||
|
||||
let emitCtorCallDefault =
|
||||
newCall(copyNimTree(emitImplIdent), ident("DefaultBrokerContext"), emitCtorExpr)
|
||||
let emitCtorBodyDefault = quote:
|
||||
await `emitCtorCallDefault`
|
||||
|
||||
let typedescEmitProcDefault = newTree(
|
||||
nnkProcDef,
|
||||
postfix(ident("emit"), "*"),
|
||||
newEmptyNode(),
|
||||
newEmptyNode(),
|
||||
emitCtorParams,
|
||||
copyNimTree(asyncPragma),
|
||||
newEmptyNode(),
|
||||
emitCtorBodyDefault,
|
||||
)
|
||||
result.add(typedescEmitProcDefault)
|
||||
|
||||
var emitCtorParamsCtx = newTree(nnkFormalParams, newEmptyNode())
|
||||
emitCtorParamsCtx.add(
|
||||
newTree(nnkIdentDefs, ident("_"), typedescParamType, newEmptyNode())
|
||||
)
|
||||
emitCtorParamsCtx.add(
|
||||
newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode())
|
||||
)
|
||||
for i in 0 ..< fieldNames.len:
|
||||
emitCtorParamsCtx.add(
|
||||
newTree(
|
||||
nnkIdentDefs,
|
||||
copyNimTree(fieldNames[i]),
|
||||
copyNimTree(fieldTypes[i]),
|
||||
newEmptyNode(),
|
||||
)
|
||||
)
|
||||
|
||||
let emitCtorCallCtx =
|
||||
newCall(copyNimTree(emitImplIdent), ident("brokerCtx"), copyNimTree(emitCtorExpr))
|
||||
let emitCtorBodyCtx = quote:
|
||||
await `emitCtorCallCtx`
|
||||
|
||||
let typedescEmitProcCtx = newTree(
|
||||
nnkProcDef,
|
||||
postfix(ident("emit"), "*"),
|
||||
newEmptyNode(),
|
||||
newEmptyNode(),
|
||||
emitCtorParamsCtx,
|
||||
copyNimTree(asyncPragma),
|
||||
newEmptyNode(),
|
||||
emitCtorBodyCtx,
|
||||
)
|
||||
result.add(typedescEmitProcCtx)
|
||||
|
||||
# ── dropListener impl ─────────────────────────────────────────────────
|
||||
result.add(
|
||||
quote do:
|
||||
proc `dropListenerImplIdent`(
|
||||
brokerCtx: BrokerContext, handle: `listenerHandleIdent`
|
||||
) =
|
||||
if handle.id == 0'u64:
|
||||
return
|
||||
if handle.threadId != currentMtThreadId():
|
||||
error "dropListener called from wrong thread",
|
||||
eventType = `typeNameLit`,
|
||||
handleThread = repr(handle.threadId),
|
||||
currentThread = repr(currentMtThreadId())
|
||||
return
|
||||
|
||||
var tvIdx = -1
|
||||
for i in 0 ..< `tvListenerCtxIdent`.len:
|
||||
if `tvListenerCtxIdent`[i] == brokerCtx:
|
||||
tvIdx = i
|
||||
break
|
||||
if tvIdx < 0:
|
||||
return
|
||||
|
||||
`tvListenerHandlersIdent`[tvIdx].del(handle.id)
|
||||
|
||||
if `tvListenerHandlersIdent`[tvIdx].len == 0:
|
||||
`tvListenerCtxIdent`.del(tvIdx)
|
||||
`tvListenerHandlersIdent`.del(tvIdx)
|
||||
`tvNextIdsIdent`.del(tvIdx)
|
||||
|
||||
let myThreadId = currentMtThreadId()
|
||||
let myThreadGen = currentMtThreadGen()
|
||||
withLock(`globalLockIdent`):
|
||||
for i in 0 ..< `globalBucketCountIdent`:
|
||||
if `globalBucketsIdent`[i].brokerCtx == brokerCtx and
|
||||
`globalBucketsIdent`[i].threadId == myThreadId and
|
||||
`globalBucketsIdent`[i].threadGen == myThreadGen:
|
||||
`globalBucketsIdent`[i].hasListeners = false
|
||||
break
|
||||
|
||||
)
|
||||
|
||||
# ── dropAllListeners impl ─────────────────────────────────────────────
|
||||
# Same-thread: clears tvHandlers immediately + flips flag under lock.
|
||||
# Cross-thread: flips flag + pushes a CtrlClearListeners sentinel into
|
||||
# the bucket's ring so the listener thread clears its tvHandlers on
|
||||
# the next poll cycle.
|
||||
result.add(
|
||||
quote do:
|
||||
proc `dropAllListenersImplIdent`(brokerCtx: BrokerContext) =
|
||||
`initProcIdent`()
|
||||
|
||||
let myThreadId = currentMtThreadId()
|
||||
var crossRings: seq[(ptr VyukovMpscRing[uint32], ThreadSignalPtr)] = @[]
|
||||
|
||||
withLock(`globalLockIdent`):
|
||||
for i in 0 ..< `globalBucketCountIdent`:
|
||||
if `globalBucketsIdent`[i].brokerCtx == brokerCtx and
|
||||
`globalBucketsIdent`[i].hasListeners:
|
||||
`globalBucketsIdent`[i].hasListeners = false
|
||||
if `globalBucketsIdent`[i].threadId != myThreadId:
|
||||
crossRings.add(
|
||||
(`globalBucketsIdent`[i].ring, `globalBucketsIdent`[i].listenerSignal)
|
||||
)
|
||||
|
||||
# Same-thread tv clear.
|
||||
var tvIdx = -1
|
||||
for i in 0 ..< `tvListenerCtxIdent`.len:
|
||||
if `tvListenerCtxIdent`[i] == brokerCtx:
|
||||
tvIdx = i
|
||||
break
|
||||
if tvIdx >= 0:
|
||||
`tvListenerHandlersIdent`[tvIdx].clear()
|
||||
`tvListenerCtxIdent`.del(tvIdx)
|
||||
`tvListenerHandlersIdent`.del(tvIdx)
|
||||
`tvNextIdsIdent`.del(tvIdx)
|
||||
|
||||
# Cross-thread: send control sentinel.
|
||||
for (ring, sig) in crossRings:
|
||||
discard ring.tryEnqueue(CtrlClearListeners)
|
||||
fireBrokerSignal(sig)
|
||||
|
||||
# Part D-3: invoke the companion cleanup hook (if any) AFTER
|
||||
# listener clearing. The CBOR FFI library registers this hook
|
||||
# in its per-event installer to clear the foreign-subscriber
|
||||
# registry + reset the per-event atomic counter, keeping
|
||||
# `SubsRegistry` in lock-step with the MT EventBroker listener
|
||||
# table on dropAllListeners. The hook runs unlocked on the
|
||||
# caller's thread; it's the hook's responsibility to acquire
|
||||
# whatever locks its data structures need.
|
||||
var hookSnap: `dropAllHookProcTypeIdent` = nil
|
||||
{.cast(gcsafe).}:
|
||||
withLock(`dropAllHookLockIdent`):
|
||||
hookSnap = `dropAllHookIdent`
|
||||
if not hookSnap.isNil:
|
||||
hookSnap(brokerCtx)
|
||||
|
||||
)
|
||||
|
||||
# ── Public dropListener / dropAllListeners ────────────────────────────
|
||||
result.add(
|
||||
quote do:
|
||||
proc dropListener*(_: typedesc[`typeIdent`], handle: `listenerHandleIdent`) =
|
||||
`dropListenerImplIdent`(DefaultBrokerContext, handle)
|
||||
|
||||
proc dropListener*(
|
||||
_: typedesc[`typeIdent`],
|
||||
brokerCtx: BrokerContext,
|
||||
handle: `listenerHandleIdent`,
|
||||
) =
|
||||
`dropListenerImplIdent`(brokerCtx, handle)
|
||||
|
||||
proc dropAllListeners*(_: typedesc[`typeIdent`]) =
|
||||
`dropAllListenersImplIdent`(DefaultBrokerContext)
|
||||
|
||||
proc dropAllListeners*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) =
|
||||
`dropAllListenersImplIdent`(brokerCtx)
|
||||
|
||||
proc `setDropAllHookIdent`*(
|
||||
_: typedesc[`typeIdent`], hook: `dropAllHookProcTypeIdent`
|
||||
) =
|
||||
## Part D-3: register a companion cleanup hook fired by
|
||||
## `dropAllListeners` (any overload) AFTER listener clearing
|
||||
## completes. Passing `nil` clears the slot. Single slot per
|
||||
## event type — the intended sole caller is the CBOR FFI
|
||||
## library's per-event installer.
|
||||
`initProcIdent`()
|
||||
{.cast(gcsafe).}:
|
||||
withLock(`dropAllHookLockIdent`):
|
||||
`dropAllHookIdent` = hook
|
||||
|
||||
)
|
||||
|
||||
# ── shutdownProcessLoopsForCtx (internal; used by API teardown) ───────
|
||||
# Must run on the bucket-owning thread. Drains the bucket's ring,
|
||||
# decRefs remaining cells, removes the bucket from the registry, and
|
||||
# deallocs its ring. The owner thread is the only safe deallocator
|
||||
# (Invariant I0).
|
||||
result.add(
|
||||
quote do:
|
||||
proc `shutdownProcessLoopsForCtxIdent`(
|
||||
ctx: BrokerContext
|
||||
) {.async: (raises: []).} =
|
||||
let myThreadId = currentMtThreadId()
|
||||
let myThreadGen = currentMtThreadGen()
|
||||
var ringsToShutdown: seq[(ptr VyukovMpscRing[uint32], ThreadSignalPtr)] = @[]
|
||||
withLock(`globalLockIdent`):
|
||||
var i = 0
|
||||
while i < `globalBucketCountIdent`:
|
||||
if `globalBucketsIdent`[i].brokerCtx == ctx and
|
||||
`globalBucketsIdent`[i].threadId == myThreadId and
|
||||
`globalBucketsIdent`[i].threadGen == myThreadGen and
|
||||
`globalBucketsIdent`[i].active:
|
||||
ringsToShutdown.add(
|
||||
(`globalBucketsIdent`[i].ring, `globalBucketsIdent`[i].listenerSignal)
|
||||
)
|
||||
for j in i ..< `globalBucketCountIdent` - 1:
|
||||
`globalBucketsIdent`[j] = `globalBucketsIdent`[j + 1]
|
||||
`globalBucketCountIdent` -= 1
|
||||
else:
|
||||
inc i
|
||||
|
||||
var shutdownFuts: seq[Future[void]] = @[]
|
||||
var k = 0
|
||||
while k < `tvShutdownFutsIdent`.len:
|
||||
if `tvShutdownFutsIdent`[k][0] == ctx:
|
||||
shutdownFuts.add(`tvShutdownFutsIdent`[k][1])
|
||||
`tvShutdownFutsIdent`.del(k)
|
||||
else:
|
||||
inc k
|
||||
|
||||
# Close each ring; the poll fn observes `closed && empty` and
|
||||
# self-unregisters via return-code 2 + completes shutdownFut.
|
||||
# Signal the dispatcher so the poll fn actually runs.
|
||||
for (ring, sig) in ringsToShutdown:
|
||||
ring.close()
|
||||
fireBrokerSignal(sig)
|
||||
|
||||
for fut in shutdownFuts:
|
||||
if not fut.finished():
|
||||
try:
|
||||
discard await withTimeout(fut, chronos.seconds(5))
|
||||
except CatchableError:
|
||||
discard
|
||||
|
||||
# Drain in-flight listener futures for this context.
|
||||
var j = 0
|
||||
while j < `tvListenerFutsIdent`.len:
|
||||
if `tvListenerFutsIdent`[j][0] == ctx:
|
||||
let fut = `tvListenerFutsIdent`[j][1]
|
||||
if not fut.finished():
|
||||
try:
|
||||
discard await withTimeout(fut, chronos.seconds(5))
|
||||
except CatchableError:
|
||||
discard
|
||||
`tvListenerFutsIdent`.del(j)
|
||||
else:
|
||||
inc j
|
||||
|
||||
# Grace window: an emit that captured ring pointers under lock
|
||||
# before we removed the bucket may still be mid-`tryEnqueue`.
|
||||
# The poll fn has already self-unregistered (return 2), and the
|
||||
# ring is closed, so any new tryEnqueue gets rejected — but we
|
||||
# need a brief delay before deallocShared so the in-flight
|
||||
# callers can complete their access. 50ms matches the original
|
||||
# `deferredFreeEventChan` window.
|
||||
try:
|
||||
await sleepAsync(chronos.milliseconds(50))
|
||||
except CatchableError:
|
||||
discard
|
||||
for (ring, _) in ringsToShutdown:
|
||||
freeVyukovMpscRing(ring)
|
||||
|
||||
)
|
||||
|
||||
# ── Public shutdown ───────────────────────────────────────────────────
|
||||
result.add(
|
||||
quote do:
|
||||
proc shutdown*(_: typedesc[`typeIdent`]): Future[void] {.async: (raises: []).} =
|
||||
await `shutdownProcessLoopsForCtxIdent`(DefaultBrokerContext)
|
||||
|
||||
proc shutdown*(
|
||||
_: typedesc[`typeIdent`], brokerCtx: BrokerContext
|
||||
): Future[void] {.async: (raises: []).} =
|
||||
await `shutdownProcessLoopsForCtxIdent`(brokerCtx)
|
||||
|
||||
)
|
||||
|
||||
when defined(brokerDebug):
|
||||
writeBrokerDebug("EventBrokerMt", typeDisplayName, result)
|
||||
when defined(brokerDebugStdout):
|
||||
echo result.repr
|
||||
@@ -0,0 +1,592 @@
|
||||
## Multi-Thread Broker Queue Primitives
|
||||
## ------------------------------------
|
||||
## Lock-free MPSC primitives used to replace `Channel[T]` in the (mt)
|
||||
## brokers. Implements `doc/REFACTOR_MT_QUEUE.md` §3.
|
||||
##
|
||||
## Invariants enforced by *structural design*, not by runtime asserts:
|
||||
##
|
||||
## I0 every `createShared` / `deallocShared` runs on a persistent owner
|
||||
## thread (bucket-owner for per-bucket structures, global-slab-owner
|
||||
## for events). Hot path (claim / release / enqueue / dequeue) never
|
||||
## calls any Nim allocator.
|
||||
## CARVE-OUT (flexible-mt-dispatch): when a marshaled payload exceeds the
|
||||
## fixed cell, the producer `allocShared0`s a heap-spill buffer and the
|
||||
## consumer-side `release` `deallocShared`s it. So the spill path DOES
|
||||
## allocate on the hot path — a deliberate trade so oversized payloads
|
||||
## (>cell, e.g. >1 MiB) succeed instead of being dropped. The common
|
||||
## fits-the-cell path is unchanged and allocator-free. Spill buffers are
|
||||
## POD bytes with single producer→consumer ownership, same cross-thread
|
||||
## contract as `storage` itself.
|
||||
## I1 Senders only execute: atomic load / store / CAS, memcpy into
|
||||
## pre-allocated cells, and `ThreadSignalPtr.fireSync()` (external).
|
||||
## I2 The owner thread must outlive every structure it owns.
|
||||
##
|
||||
## Phase 1 deliverable: this module + its tests. No broker code calls it
|
||||
## yet — that lands in Phases 2-4.
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/atomics
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cache-line padding helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
const CacheLineBytes* = 64
|
||||
|
||||
type CacheLineGap = array[CacheLineBytes, byte]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ShardedFreeList — Treiber stack with ABA tagging, sharded by thread hash
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Head word layout (uint64):
|
||||
# bits 0..31 index into the free-list's external `nextLinks` array
|
||||
# bits 32..63 ABA tag (incremented on every successful CAS)
|
||||
#
|
||||
# A special INDEX value `EmptyIdx` means "this shard is empty".
|
||||
# The free-list does NOT own the storage — callers manage capacity and
|
||||
# the `nextLinks` array externally. This keeps the primitive composable.
|
||||
|
||||
const EmptyIdx*: uint32 = high(uint32)
|
||||
|
||||
template makeHead(idx, tag: uint32): uint64 =
|
||||
(uint64(tag) shl 32) or uint64(idx)
|
||||
|
||||
template headIdx(v: uint64): uint32 =
|
||||
uint32(v and 0xFFFFFFFF'u64)
|
||||
|
||||
template headTag(v: uint64): uint32 =
|
||||
uint32(v shr 32)
|
||||
|
||||
type
|
||||
FreeListShard = object
|
||||
head: Atomic[uint64]
|
||||
gap: CacheLineGap
|
||||
|
||||
ShardedFreeList* = object
|
||||
nShardsMask: uint32 ## nShards - 1; nShards is power-of-2
|
||||
nShards: uint32
|
||||
shards: ptr UncheckedArray[FreeListShard]
|
||||
nextLinks: ptr UncheckedArray[uint32] ## idx → next idx (or EmptyIdx)
|
||||
|
||||
proc initShardedFreeList*(
|
||||
fl: var ShardedFreeList, nShards: uint32, capacity: uint32
|
||||
) {.gcsafe.} =
|
||||
## Initialize a sharded free-list. `nShards` MUST be a power of two.
|
||||
## `nextLinks` is allocated as a parallel array of `capacity` indices,
|
||||
## all initialised to `EmptyIdx`.
|
||||
doAssert nShards > 0 and (nShards and (nShards - 1)) == 0,
|
||||
"nShards must be power of two"
|
||||
fl.nShards = nShards
|
||||
fl.nShardsMask = nShards - 1
|
||||
fl.shards =
|
||||
cast[ptr UncheckedArray[FreeListShard]](createShared(FreeListShard, nShards.int))
|
||||
for i in 0 ..< nShards.int:
|
||||
fl.shards[i].head.store(makeHead(EmptyIdx, 0), moRelaxed)
|
||||
fl.nextLinks = cast[ptr UncheckedArray[uint32]](createShared(uint32, capacity.int))
|
||||
for i in 0 ..< capacity.int:
|
||||
fl.nextLinks[i] = EmptyIdx
|
||||
|
||||
proc deinitShardedFreeList*(fl: var ShardedFreeList) {.gcsafe.} =
|
||||
if not fl.shards.isNil:
|
||||
deallocShared(fl.shards)
|
||||
fl.shards = nil
|
||||
if not fl.nextLinks.isNil:
|
||||
deallocShared(fl.nextLinks)
|
||||
fl.nextLinks = nil
|
||||
|
||||
proc push*(fl: var ShardedFreeList, idx: uint32, shardHint: uint32) {.gcsafe.} =
|
||||
## Push `idx` onto the free-list. `shardHint` selects the shard to push to.
|
||||
let shardIdx = shardHint and fl.nShardsMask
|
||||
let shard = addr fl.shards[shardIdx]
|
||||
while true:
|
||||
let oldHead = shard.head.load(moAcquire)
|
||||
fl.nextLinks[idx] = headIdx(oldHead)
|
||||
# Tag increments on every successful push to dodge ABA.
|
||||
let newHead = makeHead(idx, headTag(oldHead) + 1)
|
||||
var expected = oldHead
|
||||
if shard.head.compareExchangeWeak(expected, newHead, moAcquireRelease, moAcquire):
|
||||
return
|
||||
|
||||
proc pop*(fl: var ShardedFreeList, shardHint: uint32): uint32 {.gcsafe.} =
|
||||
## Pop an index from the free-list. Tries `shardHint`'s shard first;
|
||||
## if empty, scans other shards. Returns `EmptyIdx` if all shards are empty.
|
||||
let preferred = shardHint and fl.nShardsMask
|
||||
for offset in 0'u32 ..< fl.nShards:
|
||||
let shardIdx = (preferred + offset) and fl.nShardsMask
|
||||
let shard = addr fl.shards[shardIdx]
|
||||
while true:
|
||||
let oldHead = shard.head.load(moAcquire)
|
||||
let idx = headIdx(oldHead)
|
||||
if idx == EmptyIdx:
|
||||
break # try next shard
|
||||
let nextIdx = fl.nextLinks[idx]
|
||||
let newHead = makeHead(nextIdx, headTag(oldHead) + 1)
|
||||
var expected = oldHead
|
||||
if shard.head.compareExchangeWeak(expected, newHead, moAcquireRelease, moAcquire):
|
||||
return idx
|
||||
# CAS failed; loop and retry on this shard.
|
||||
return EmptyIdx
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# VyukovMpscRing[T] — bounded MPSC ring with closed-flag handoff
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Atomic protocol:
|
||||
# Producer (`tryEnqueue`):
|
||||
# 1. closed-check (acquire) → if closed, return false.
|
||||
# 2. pos = enqPos.load(relaxed)
|
||||
# 3. loop:
|
||||
# slot = &slots[pos & mask]
|
||||
# seq = slot.seq.load(acquire)
|
||||
# diff = seq - pos (as signed)
|
||||
# if diff == 0:
|
||||
# CAS enqPos: pos → pos+1 (acquireRelease on success, acquire on failure)
|
||||
# if success: break (slot is claimed; must publish)
|
||||
# else: pos was reloaded into `expected`; loop
|
||||
# elif diff < 0:
|
||||
# return false (full)
|
||||
# else:
|
||||
# another producer claimed this slot already; reload pos, loop
|
||||
# 4. write slot.payload
|
||||
# 5. slot.seq.store(pos+1, release) -- publish
|
||||
#
|
||||
# Consumer (`tryDequeue`, single-thread):
|
||||
# 1. pos = deqPos
|
||||
# 2. seq = slots[pos & mask].seq.load(acquire)
|
||||
# 3. if seq != pos+1: return false (empty / not yet published)
|
||||
# 4. read payload
|
||||
# 5. slots[pos & mask].seq.store(pos + capacity, release) -- slot reusable
|
||||
# 6. deqPos = pos + 1
|
||||
#
|
||||
# Closed-flag handoff (`drain` called by owner):
|
||||
# 1. closed.store(true, release)
|
||||
# 2. spin-loop: tryDequeue all visible items; sleep if there's a gap
|
||||
# (slot not yet published by an in-flight producer that already CAS'd).
|
||||
# Exit when deqPos == enqPos.
|
||||
|
||||
type
|
||||
Slot*[T] = object
|
||||
seq: Atomic[uint64]
|
||||
payload*: T
|
||||
|
||||
VyukovMpscRing*[T] = object
|
||||
capacity*: uint64
|
||||
mask: uint64
|
||||
closed: Atomic[bool]
|
||||
gap0: CacheLineGap
|
||||
enqPos: Atomic[uint64]
|
||||
gap1: CacheLineGap
|
||||
deqPos: uint64
|
||||
gap2: CacheLineGap
|
||||
slots: ptr UncheckedArray[Slot[T]]
|
||||
|
||||
proc newVyukovMpscRing*[T](capacity: int): ptr VyukovMpscRing[T] {.gcsafe.} =
|
||||
## Allocate a ring of the given capacity (must be power-of-2).
|
||||
## Returns ownership; deinit via `freeVyukovMpscRing`.
|
||||
doAssert capacity > 0 and (capacity and (capacity - 1)) == 0,
|
||||
"capacity must be power-of-2"
|
||||
result = cast[ptr VyukovMpscRing[T]](createShared(VyukovMpscRing[T], 1))
|
||||
result.capacity = uint64(capacity)
|
||||
result.mask = uint64(capacity - 1)
|
||||
result.closed.store(false, moRelaxed)
|
||||
result.enqPos.store(0, moRelaxed)
|
||||
result.deqPos = 0
|
||||
result.slots = cast[ptr UncheckedArray[Slot[T]]](createShared(Slot[T], capacity))
|
||||
for i in 0 ..< capacity:
|
||||
result.slots[i].seq.store(uint64(i), moRelaxed)
|
||||
|
||||
proc freeVyukovMpscRing*[T](ring: ptr VyukovMpscRing[T]) {.gcsafe.} =
|
||||
## Deallocate. Must be called on the owner thread, with the ring already
|
||||
## drained (caller responsibility).
|
||||
if ring.isNil:
|
||||
return
|
||||
if not ring.slots.isNil:
|
||||
deallocShared(ring.slots)
|
||||
deallocShared(ring)
|
||||
|
||||
proc isClosed*[T](ring: ptr VyukovMpscRing[T]): bool {.gcsafe.} =
|
||||
ring.closed.load(moAcquire)
|
||||
|
||||
proc close*[T](ring: ptr VyukovMpscRing[T]) {.gcsafe.} =
|
||||
ring.closed.store(true, moRelease)
|
||||
|
||||
proc tryEnqueue*[T](ring: ptr VyukovMpscRing[T], item: sink T): bool {.gcsafe.} =
|
||||
## Returns true if enqueued, false if full or closed.
|
||||
## Safe to call from any number of producer threads.
|
||||
if ring.closed.load(moAcquire):
|
||||
return false
|
||||
var pos = ring.enqPos.load(moRelaxed)
|
||||
while true:
|
||||
let slot = addr ring.slots[pos and ring.mask]
|
||||
let seqV = slot.seq.load(moAcquire)
|
||||
let diff = cast[int64](seqV) - cast[int64](pos)
|
||||
if diff == 0:
|
||||
var expected = pos
|
||||
if ring.enqPos.compareExchangeWeak(expected, pos + 1, moAcquireRelease, moAcquire):
|
||||
# We own slot[pos]. Re-check closed for the "closed after our
|
||||
# initial check but before CAS" race; if closed, we must still
|
||||
# publish so the consumer can observe and drain it. The drain
|
||||
# protocol counts on the slot being published.
|
||||
slot.payload = item
|
||||
slot.seq.store(pos + 1, moRelease)
|
||||
return true
|
||||
# CAS failed; `expected` now holds the latest enqPos; retry.
|
||||
pos = expected
|
||||
elif diff < 0:
|
||||
# Full: slot.seq lags pos, which means the prior occupant hasn't
|
||||
# been consumed yet.
|
||||
return false
|
||||
else:
|
||||
# diff > 0: another producer is ahead of us; reload pos.
|
||||
pos = ring.enqPos.load(moRelaxed)
|
||||
|
||||
proc tryDequeue*[T](ring: ptr VyukovMpscRing[T], outItem: var T): bool {.gcsafe.} =
|
||||
## Returns true if an item was dequeued, false if empty.
|
||||
## MUST be called from a single consumer thread.
|
||||
let pos = ring.deqPos
|
||||
let slot = addr ring.slots[pos and ring.mask]
|
||||
let seqV = slot.seq.load(moAcquire)
|
||||
let diff = cast[int64](seqV) - cast[int64](pos + 1)
|
||||
if diff != 0:
|
||||
return false
|
||||
outItem = slot.payload
|
||||
slot.seq.store(pos + ring.capacity, moRelease)
|
||||
ring.deqPos = pos + 1
|
||||
return true
|
||||
|
||||
proc isEmpty*[T](ring: ptr VyukovMpscRing[T]): bool {.gcsafe.} =
|
||||
## Consumer-side observation; producers may concurrently enqueue,
|
||||
## so callers must treat the result as a hint unless they also hold
|
||||
## a guarantee that no producers are active.
|
||||
ring.enqPos.load(moAcquire) == ring.deqPos
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# RefCountedCell + PayloadSlab — pre-allocated payload cells
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Cell layout (computed at runtime, since payload bytes are variable-size):
|
||||
# CellHeader fields: refcount (Atomic[int]), payloadSize (uint32),
|
||||
# overflowLen (uint32), overflow (pointer)
|
||||
# then: payloadBytes[] (payloadCap bytes; from
|
||||
# slab.cellPayloadCap), starting at sizeof(CellHeader).
|
||||
# (cellStride is alignUp(sizeof(CellHeader) + payloadCap, 8), so the exact
|
||||
# payload offset is always sizeof(CellHeader) regardless of field packing —
|
||||
# do not assume a hard-coded offset, the header grew with the spill fields.)
|
||||
#
|
||||
# We address cells by index (uint32) so the free-list can ABA-tag indices
|
||||
# rather than pointers. Pointer access is via `slab.cellPtr(idx)`.
|
||||
|
||||
type
|
||||
CellHeader* = object
|
||||
refcount*: Atomic[int]
|
||||
payloadSize*: uint32
|
||||
## inline marshaled bytes used. uint32 (not uint16) so a configured cell
|
||||
## may exceed 64 KiB — broker messages can be >1 MiB. 0 when the payload
|
||||
## spilled to the heap (see `overflow`).
|
||||
overflowLen*: uint32 ## spilled byte count; 0 when the payload fit inline.
|
||||
overflow*: pointer
|
||||
## heap-spill buffer (`allocShared0`) when the marshaled payload exceeded
|
||||
## the fixed cell; `nil` on the inline fast path. Owned by the cell: freed
|
||||
## in `release` (refcount→0 chokepoint) and walked by `deinitPayloadSlab`.
|
||||
## POD bytes only — same cross-thread ownership contract as `storage`.
|
||||
|
||||
PayloadSlab* = object
|
||||
capacity: uint32
|
||||
cellPayloadCap*: uint32 ## bytes available for marshaled data per cell
|
||||
cellStride: uint32 ## sizeof(CellHeader) + cellPayloadCap, aligned
|
||||
storage: ptr UncheckedArray[byte]
|
||||
freeList: ShardedFreeList
|
||||
|
||||
proc cellHeaderSize(): uint32 {.compileTime.} =
|
||||
uint32(sizeof(CellHeader))
|
||||
|
||||
proc alignUp(v, a: uint32): uint32 =
|
||||
(v + a - 1) and not (a - 1)
|
||||
|
||||
proc initPayloadSlab*(
|
||||
slab: var PayloadSlab, capacity: uint32, payloadBytes: uint32, nShards: uint32
|
||||
) {.gcsafe.} =
|
||||
## Pre-allocates `capacity` cells, each with `payloadBytes` of payload
|
||||
## space. Uses `nShards` (must be power-of-2) for free-list contention.
|
||||
## All cells start on the free-list.
|
||||
doAssert capacity > 0
|
||||
doAssert payloadBytes > 0
|
||||
slab.capacity = capacity
|
||||
slab.cellPayloadCap = payloadBytes
|
||||
slab.cellStride = alignUp(cellHeaderSize() + payloadBytes, 8'u32)
|
||||
slab.storage = cast[ptr UncheckedArray[byte]](createShared(
|
||||
byte, int(capacity) * int(slab.cellStride)
|
||||
))
|
||||
initShardedFreeList(slab.freeList, nShards, capacity)
|
||||
# Seed the free-list with every cell.
|
||||
for i in 0 ..< capacity:
|
||||
push(slab.freeList, i, i)
|
||||
|
||||
proc cellPtr*(slab: PayloadSlab, idx: uint32): ptr CellHeader {.gcsafe.} =
|
||||
## Returns the header pointer for the cell at `idx`. The payload bytes
|
||||
## immediately follow the header (at `cast[ptr byte](header) +%
|
||||
## sizeof(CellHeader)`).
|
||||
cast[ptr CellHeader](addr slab.storage[int(idx) * int(slab.cellStride)])
|
||||
|
||||
proc cellPayloadPtr*(
|
||||
slab: PayloadSlab, idx: uint32
|
||||
): ptr UncheckedArray[byte] {.gcsafe.} =
|
||||
cast[ptr UncheckedArray[byte]](cast[uint](addr slab.storage[
|
||||
int(idx) * int(slab.cellStride)
|
||||
]) + uint(sizeof(CellHeader)))
|
||||
|
||||
proc deinitPayloadSlab*(slab: var PayloadSlab) {.gcsafe.} =
|
||||
## MUST be called on the owner thread after every outstanding cell has
|
||||
## been released (caller responsibility). Frees the slab's storage and
|
||||
## the free-list's internal arrays. Also walks every cell to free any
|
||||
## heap-spill buffer still attached — covers shutdown / clearProvider with
|
||||
## undelivered in-flight cells (a cell closed before delivery never passes
|
||||
## through `release`, so its spill would otherwise leak).
|
||||
if not slab.storage.isNil:
|
||||
for i in 0'u32 ..< slab.capacity:
|
||||
let cell = slab.cellPtr(i)
|
||||
if not cell.overflow.isNil:
|
||||
deallocShared(cell.overflow)
|
||||
cell.overflow = nil
|
||||
cell.overflowLen = 0
|
||||
deinitShardedFreeList(slab.freeList)
|
||||
if not slab.storage.isNil:
|
||||
deallocShared(slab.storage)
|
||||
slab.storage = nil
|
||||
|
||||
proc setOverflow*(
|
||||
slab: PayloadSlab, idx: uint32, buf: pointer, len: uint32
|
||||
) {.gcsafe.} =
|
||||
## Attach a heap-spill buffer to a cell (payload exceeded the inline cell).
|
||||
## The cell takes ownership; `release`/`deinitPayloadSlab` free it.
|
||||
let cell = slab.cellPtr(idx)
|
||||
cell.overflow = buf
|
||||
cell.overflowLen = len
|
||||
cell.payloadSize = 0
|
||||
|
||||
proc dataPtr*(slab: PayloadSlab, idx: uint32): ptr UncheckedArray[byte] {.gcsafe.} =
|
||||
## Pointer to the marshaled bytes for a cell — the heap-spill buffer when the
|
||||
## payload spilled, else the inline payload region.
|
||||
let cell = slab.cellPtr(idx)
|
||||
if not cell.overflow.isNil:
|
||||
cast[ptr UncheckedArray[byte]](cell.overflow)
|
||||
else:
|
||||
slab.cellPayloadPtr(idx)
|
||||
|
||||
proc dataLen*(slab: PayloadSlab, idx: uint32): int {.gcsafe.} =
|
||||
## Marshaled byte count for a cell (spill length or inline payloadSize).
|
||||
let cell = slab.cellPtr(idx)
|
||||
if not cell.overflow.isNil:
|
||||
int(cell.overflowLen)
|
||||
else:
|
||||
int(cell.payloadSize)
|
||||
|
||||
proc claim*(slab: var PayloadSlab, shardHint: uint32): uint32 {.gcsafe.} =
|
||||
## Returns a cell index or `EmptyIdx` if the slab is exhausted.
|
||||
pop(slab.freeList, shardHint)
|
||||
|
||||
proc release*(slab: var PayloadSlab, idx: uint32, shardHint: uint32) {.gcsafe.} =
|
||||
## Returns a cell to the free-list. Caller must ensure no other thread
|
||||
## still holds a reference (refcount == 0). This is the single chokepoint a
|
||||
## cell passes through on its way back to the free-list (all delivery / drop /
|
||||
## error paths funnel here once refcount hits 0), so any heap-spill buffer is
|
||||
## freed here exactly once.
|
||||
let cell = slab.cellPtr(idx)
|
||||
if not cell.overflow.isNil:
|
||||
deallocShared(cell.overflow)
|
||||
cell.overflow = nil
|
||||
cell.overflowLen = 0
|
||||
push(slab.freeList, idx, shardHint)
|
||||
|
||||
proc incRef*(slab: PayloadSlab, idx: uint32) {.gcsafe.} =
|
||||
discard slab.cellPtr(idx).refcount.fetchAdd(1, moAcquireRelease)
|
||||
|
||||
proc decRefAndCheck*(slab: PayloadSlab, idx: uint32): bool {.gcsafe.} =
|
||||
## Returns true if this decrement brought refcount to zero (caller should
|
||||
## then `release(idx)`).
|
||||
let prev = slab.cellPtr(idx).refcount.fetchSub(1, moAcquireRelease)
|
||||
prev == 1
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ResponseSlot[T] + ResponseSlotPool[T] — single-shot request reply
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# State machine on the slot's `state` byte:
|
||||
# Empty(0) ── requester claimed; provider hasn't written yet
|
||||
# │
|
||||
# ├── (provider) CAS Empty→Ready, write payload, signal requester
|
||||
# │ │
|
||||
# │ └── (requester) read payload; release slot
|
||||
# │
|
||||
# └── (requester timeout) CAS Empty→Abandoned
|
||||
# │
|
||||
# └── (provider) sees Abandoned; releases slot
|
||||
#
|
||||
# In both terminal cases the slot returns to the pool's free-list
|
||||
# exactly once.
|
||||
|
||||
type
|
||||
ResponseState* {.pure.} = enum
|
||||
Empty = 0'u8
|
||||
Writing = 1'u8 ## reserved by provider; bytes in flight
|
||||
Ready = 2'u8
|
||||
Abandoned = 3'u8
|
||||
|
||||
ResponseSlotHeader = object
|
||||
state: Atomic[uint8]
|
||||
pad0: array[3, byte] ## align the uint32 payloadSize to a 4-byte boundary
|
||||
payloadSize: uint32
|
||||
## uint32 (not uint16) so a response slot may exceed 64 KiB.
|
||||
## state(1) + pad0(3) + payloadSize(4) = 8 bytes → 8-aligned.
|
||||
overflowLen: uint32 ## spilled response byte count; 0 when the response fit inline.
|
||||
pad1: uint32 ## keep the pointer that follows 8-aligned (overflowLen at +8)
|
||||
overflow: pointer
|
||||
## heap-spill buffer for an oversized response; `nil` inline. Owned by the
|
||||
## slot: freed in `release` and walked by `deinitResponseSlotPool`.
|
||||
|
||||
ResponseSlotPool* = object
|
||||
capacity*: uint32
|
||||
slotPayloadCap*: uint32
|
||||
slotStride: uint32
|
||||
storage: ptr UncheckedArray[byte]
|
||||
freeList: ShardedFreeList
|
||||
|
||||
proc respSlotHeaderSize(): uint32 {.compileTime.} =
|
||||
uint32(sizeof(ResponseSlotHeader))
|
||||
|
||||
proc slotHeaderPtr(
|
||||
pool: ResponseSlotPool, idx: uint32
|
||||
): ptr ResponseSlotHeader {.gcsafe.} =
|
||||
cast[ptr ResponseSlotHeader](addr pool.storage[int(idx) * int(pool.slotStride)])
|
||||
|
||||
proc slotPayloadPtr*(
|
||||
pool: ResponseSlotPool, idx: uint32
|
||||
): ptr UncheckedArray[byte] {.gcsafe.} =
|
||||
cast[ptr UncheckedArray[byte]](cast[uint](addr pool.storage[
|
||||
int(idx) * int(pool.slotStride)
|
||||
]) + uint(sizeof(ResponseSlotHeader)))
|
||||
|
||||
proc initResponseSlotPool*(
|
||||
pool: var ResponseSlotPool,
|
||||
capacity: uint32,
|
||||
maxPayloadBytes: uint32,
|
||||
nShards: uint32,
|
||||
) {.gcsafe.} =
|
||||
pool.capacity = capacity
|
||||
pool.slotPayloadCap = maxPayloadBytes
|
||||
pool.slotStride = alignUp(respSlotHeaderSize() + maxPayloadBytes, 8'u32)
|
||||
pool.storage = cast[ptr UncheckedArray[byte]](createShared(
|
||||
byte, int(capacity) * int(pool.slotStride)
|
||||
))
|
||||
initShardedFreeList(pool.freeList, nShards, capacity)
|
||||
for i in 0 ..< capacity:
|
||||
let hdr = pool.slotHeaderPtr(i)
|
||||
hdr.state.store(uint8(ResponseState.Empty), moRelaxed)
|
||||
hdr.payloadSize = 0
|
||||
push(pool.freeList, i, i)
|
||||
|
||||
proc deinitResponseSlotPool*(pool: var ResponseSlotPool) {.gcsafe.} =
|
||||
## Walk every slot to free any heap-spill buffer still attached (shutdown
|
||||
## with an undelivered response), then free storage + free-list arrays.
|
||||
if not pool.storage.isNil:
|
||||
for i in 0'u32 ..< pool.capacity:
|
||||
let hdr = pool.slotHeaderPtr(i)
|
||||
if not hdr.overflow.isNil:
|
||||
deallocShared(hdr.overflow)
|
||||
hdr.overflow = nil
|
||||
hdr.overflowLen = 0
|
||||
deinitShardedFreeList(pool.freeList)
|
||||
if not pool.storage.isNil:
|
||||
deallocShared(pool.storage)
|
||||
pool.storage = nil
|
||||
|
||||
proc claim*(pool: var ResponseSlotPool, shardHint: uint32): uint32 {.gcsafe.} =
|
||||
let idx = pop(pool.freeList, shardHint)
|
||||
if idx != EmptyIdx:
|
||||
let hdr = pool.slotHeaderPtr(idx)
|
||||
hdr.payloadSize = 0
|
||||
# release() already frees+nils any spill, but defend against a slot that
|
||||
# reached the free-list without passing release (it should not).
|
||||
if not hdr.overflow.isNil:
|
||||
deallocShared(hdr.overflow)
|
||||
hdr.overflow = nil
|
||||
hdr.overflowLen = 0
|
||||
hdr.state.store(uint8(ResponseState.Empty), moRelease)
|
||||
idx
|
||||
|
||||
proc release*(pool: var ResponseSlotPool, idx: uint32, shardHint: uint32) {.gcsafe.} =
|
||||
## Single chokepoint a slot passes through back to the free-list (requester
|
||||
## after read, or provider on abandon). Free any heap-spill buffer here.
|
||||
let hdr = pool.slotHeaderPtr(idx)
|
||||
if not hdr.overflow.isNil:
|
||||
deallocShared(hdr.overflow)
|
||||
hdr.overflow = nil
|
||||
hdr.overflowLen = 0
|
||||
push(pool.freeList, idx, shardHint)
|
||||
|
||||
proc beginWrite*(pool: ResponseSlotPool, idx: uint32): bool {.gcsafe.} =
|
||||
## Provider: CAS Empty→Writing. Returns false if the requester abandoned
|
||||
## the slot first (caller should release without writing).
|
||||
let hdr = pool.slotHeaderPtr(idx)
|
||||
var expected = uint8(ResponseState.Empty)
|
||||
hdr.state.compareExchange(
|
||||
expected, uint8(ResponseState.Writing), moAcquireRelease, moAcquire
|
||||
)
|
||||
|
||||
proc commitWrite*(pool: ResponseSlotPool, idx: uint32, payloadSize: uint32) {.gcsafe.} =
|
||||
## Provider: finalize after writing payload bytes. Stores size + flips
|
||||
## state to Ready (release-ordered, so the bytes-write is visible to
|
||||
## any acquire-loader on the state).
|
||||
let hdr = pool.slotHeaderPtr(idx)
|
||||
hdr.payloadSize = payloadSize
|
||||
hdr.state.store(uint8(ResponseState.Ready), moRelease)
|
||||
|
||||
proc commitWriteOverflow*(
|
||||
pool: ResponseSlotPool, idx: uint32, buf: pointer, len: uint32
|
||||
) {.gcsafe.} =
|
||||
## Provider: finalize an oversized response that spilled to the heap. The
|
||||
## slot takes ownership of `buf` (freed in `release`/`deinitResponseSlotPool`).
|
||||
## Sets inline payloadSize = 0 and flips state to Ready (release-ordered so the
|
||||
## buffer pointer + the bytes it points to are visible to an acquire-loader).
|
||||
let hdr = pool.slotHeaderPtr(idx)
|
||||
hdr.overflow = buf
|
||||
hdr.overflowLen = len
|
||||
hdr.payloadSize = 0
|
||||
hdr.state.store(uint8(ResponseState.Ready), moRelease)
|
||||
|
||||
proc respDataPtr*(
|
||||
pool: ResponseSlotPool, idx: uint32
|
||||
): ptr UncheckedArray[byte] {.gcsafe.} =
|
||||
## Pointer to the marshaled response bytes — spill buffer when spilled, else
|
||||
## the inline slot payload region.
|
||||
let hdr = pool.slotHeaderPtr(idx)
|
||||
if not hdr.overflow.isNil:
|
||||
cast[ptr UncheckedArray[byte]](hdr.overflow)
|
||||
else:
|
||||
pool.slotPayloadPtr(idx)
|
||||
|
||||
proc respDataLen*(pool: ResponseSlotPool, idx: uint32): int {.gcsafe.} =
|
||||
let hdr = pool.slotHeaderPtr(idx)
|
||||
if not hdr.overflow.isNil:
|
||||
int(hdr.overflowLen)
|
||||
else:
|
||||
int(hdr.payloadSize)
|
||||
|
||||
proc abandon*(pool: ResponseSlotPool, idx: uint32): bool {.gcsafe.} =
|
||||
## Requester: CAS Empty→Abandoned. Returns true if abandonment took
|
||||
## effect (provider hadn't started writing yet). If false, requester
|
||||
## must still wait for state==Ready and consume normally — provider
|
||||
## is mid-write or already done.
|
||||
let hdr = pool.slotHeaderPtr(idx)
|
||||
var expected = uint8(ResponseState.Empty)
|
||||
hdr.state.compareExchange(
|
||||
expected, uint8(ResponseState.Abandoned), moAcquireRelease, moAcquire
|
||||
)
|
||||
|
||||
proc readyState*(pool: ResponseSlotPool, idx: uint32): bool {.gcsafe.} =
|
||||
pool.slotHeaderPtr(idx).state.load(moAcquire) == uint8(ResponseState.Ready)
|
||||
|
||||
proc payloadSize*(pool: ResponseSlotPool, idx: uint32): uint32 {.gcsafe.} =
|
||||
pool.slotHeaderPtr(idx).payloadSize
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,746 @@
|
||||
## MultiRequestBroker
|
||||
## --------------------
|
||||
## MultiRequestBroker represents a proactive decoupling pattern, that
|
||||
## allows defining request-response style interactions between modules without
|
||||
## need for direct dependencies in between.
|
||||
## Worth considering using it for use cases where you need to collect data from multiple providers.
|
||||
##
|
||||
## Generates a standalone, type-safe request broker for the declared type.
|
||||
## The macro exports the value type itself plus a broker companion that manages
|
||||
## providers via thread-local storage.
|
||||
##
|
||||
## Unlike `RequestBroker`, every call to `request` fan-outs to every registered
|
||||
## provider and returns all collected responses.
|
||||
## The request succeeds only if all providers succeed, otherwise it fails.
|
||||
##
|
||||
## Type definitions:
|
||||
## - Inline `object` / `ref object` definitions are supported.
|
||||
## - Native types, aliases, and externally-defined types are also supported.
|
||||
## In that case, MultiRequestBroker will automatically wrap the declared RHS
|
||||
## type in `distinct` unless you already used `distinct`.
|
||||
## This keeps request types unique even when multiple brokers share the same
|
||||
## underlying base type.
|
||||
##
|
||||
## Default vs. context aware use:
|
||||
## Every generated broker is a thread-local global instance.
|
||||
## Sometimes you want multiple independent provider sets for the same request
|
||||
## type within the same thread (e.g. multiple components). For that, you can use
|
||||
## context-aware MultiRequestBroker.
|
||||
##
|
||||
## Context awareness is supported through the `BrokerContext` argument for
|
||||
## `setProvider`, `request`, `removeProvider`, and `clearProviders`.
|
||||
## Provider stores are kept separate per broker context.
|
||||
##
|
||||
## Default broker context is defined as `DefaultBrokerContext`. If you don't
|
||||
## need context awareness, you can keep using the interfaces without the context
|
||||
## argument, which operate on `DefaultBrokerContext`.
|
||||
##
|
||||
## Usage:
|
||||
##
|
||||
## Declare collectable request data type inside a `MultiRequestBroker` macro, add any number of fields:
|
||||
## ```nim
|
||||
## MultiRequestBroker:
|
||||
## type TypeName = object
|
||||
## field1*: Type1
|
||||
## field2*: Type2
|
||||
##
|
||||
## ## Define the request and provider signature, that is enforced at compile time.
|
||||
## proc signature*(): Future[Result[TypeName, string]] {.async: (raises: []).}
|
||||
##
|
||||
## ## Also possible to define signature with arbitrary input arguments.
|
||||
## proc signature*(arg1: ArgType, arg2: AnotherArgType): Future[Result[TypeName, string]] {.async: (raises: []).}
|
||||
##
|
||||
## ```
|
||||
##
|
||||
## You can register a request processor (provider) anywhere without the need to
|
||||
## know who will request.
|
||||
## Register provider functions with `TypeName.setProvider(...)`.
|
||||
## Providers are async procs or lambdas that return `Future[Result[TypeName, string]]`.
|
||||
## `setProvider` returns a handle (or an error) that can later be used to remove
|
||||
## the provider.
|
||||
|
||||
## Requests can be made from anywhere with no direct dependency on the provider(s)
|
||||
## by calling `TypeName.request()` (with arguments respecting the declared signature).
|
||||
## This will asynchronously call all registered providers and return the collected
|
||||
## responses as `Future[Result[seq[TypeName], string]]`.
|
||||
##
|
||||
## Whenever you don't want to process requests anymore (or your object instance that provides the request goes out of scope),
|
||||
## you can remove it from the broker with `TypeName.removeProvider(handle)`.
|
||||
## Alternatively, you can remove all registered providers through `TypeName.clearProviders()`.
|
||||
##
|
||||
## Example:
|
||||
## ```nim
|
||||
## MultiRequestBroker:
|
||||
## type Greeting = object
|
||||
## text*: string
|
||||
##
|
||||
## ## Define the request and provider signature, that is enforced at compile time.
|
||||
## proc signature*(): Future[Result[Greeting, string]] {.async: (raises: []).}
|
||||
##
|
||||
## ## Also possible to define signature with arbitrary input arguments.
|
||||
## proc signature*(lang: string): Future[Result[Greeting, string]] {.async: (raises: []).}
|
||||
##
|
||||
## ...
|
||||
## let handle = Greeting.setProvider(
|
||||
## proc(): Future[Result[Greeting, string]] {.async: (raises: []).} =
|
||||
## ok(Greeting(text: "hello"))
|
||||
## )
|
||||
##
|
||||
## let anotherHandle = Greeting.setProvider(
|
||||
## proc(): Future[Result[Greeting, string]] {.async: (raises: []).} =
|
||||
## ok(Greeting(text: "szia"))
|
||||
## )
|
||||
##
|
||||
## let responses = (await Greeting.request()).valueOr(@[Greeting(text: "default")])
|
||||
##
|
||||
## echo responses.len
|
||||
## Greeting.clearProviders()
|
||||
## ```
|
||||
## If no `signature` proc is declared, a zero-argument form is generated
|
||||
## automatically, so the caller only needs to provide the type definition.
|
||||
|
||||
import std/[macros, strutils, tables, sugar]
|
||||
import chronos
|
||||
import results
|
||||
import ./internal/helper/broker_utils
|
||||
import ./broker_context
|
||||
import ./internal/broker_debug
|
||||
|
||||
export results, chronos, broker_context
|
||||
|
||||
proc isReturnTypeValid(returnType, typeIdent: NimNode): bool =
|
||||
## Accept Future[Result[TypeIdent, string]] as the contract.
|
||||
if returnType.kind != nnkBracketExpr or returnType.len != 2:
|
||||
return false
|
||||
if returnType[0].kind != nnkIdent or not returnType[0].eqIdent("Future"):
|
||||
return false
|
||||
let inner = returnType[1]
|
||||
if inner.kind != nnkBracketExpr or inner.len != 3:
|
||||
return false
|
||||
if inner[0].kind != nnkIdent or not inner[0].eqIdent("Result"):
|
||||
return false
|
||||
if inner[1].kind != nnkIdent or not inner[1].eqIdent($typeIdent):
|
||||
return false
|
||||
inner[2].kind == nnkIdent and inner[2].eqIdent("string")
|
||||
|
||||
proc makeProcType(returnType: NimNode, params: seq[NimNode]): NimNode =
|
||||
var formal = newTree(nnkFormalParams)
|
||||
formal.add(returnType)
|
||||
for param in params:
|
||||
formal.add(param)
|
||||
|
||||
let pragmas = quote:
|
||||
{.async.}
|
||||
|
||||
newTree(nnkProcTy, formal, pragmas)
|
||||
|
||||
macro MultiRequestBroker*(body: untyped): untyped =
|
||||
when defined(brokerDebug):
|
||||
echo body.treeRepr
|
||||
let parsed = parseSingleTypeDef(body, "MultiRequestBroker")
|
||||
let typeIdent = parsed.typeIdent
|
||||
let objectDef = parsed.objectDef
|
||||
let isRefObject = parsed.isRefObject
|
||||
|
||||
when defined(brokerDebug):
|
||||
echo "MultiRequestBroker generating type: ", $typeIdent
|
||||
|
||||
let exportedTypeIdent = postfix(copyNimTree(typeIdent), "*")
|
||||
let sanitized = sanitizeIdentName(typeIdent)
|
||||
let typeNameLit = newLit($typeIdent)
|
||||
let isRefObjectLit = newLit(isRefObject)
|
||||
let uint64Ident = ident("uint64")
|
||||
let providerKindIdent = ident(sanitized & "ProviderKind")
|
||||
let providerHandleIdent = ident(sanitized & "ProviderHandle")
|
||||
let exportedProviderHandleIdent = postfix(copyNimTree(providerHandleIdent), "*")
|
||||
let bucketTypeIdent = ident(sanitized & "CtxBucket")
|
||||
let findBucketIdxIdent = ident(sanitized & "FindBucketIdx")
|
||||
let getOrCreateBucketIdxIdent = ident(sanitized & "GetOrCreateBucketIdx")
|
||||
let zeroKindIdent = ident("pk" & sanitized & "NoArgs")
|
||||
let argKindIdent = ident("pk" & sanitized & "WithArgs")
|
||||
var zeroArgSig: NimNode = nil
|
||||
var zeroArgProviderName: NimNode = nil
|
||||
var zeroArgFieldName: NimNode = nil
|
||||
var argSig: NimNode = nil
|
||||
var argParams: seq[NimNode] = @[]
|
||||
var argProviderName: NimNode = nil
|
||||
var argFieldName: NimNode = nil
|
||||
|
||||
for stmt in body:
|
||||
case stmt.kind
|
||||
of nnkProcDef:
|
||||
let procName = stmt[0]
|
||||
let procNameIdent =
|
||||
case procName.kind
|
||||
of nnkIdent:
|
||||
procName
|
||||
of nnkPostfix:
|
||||
procName[1]
|
||||
else:
|
||||
procName
|
||||
let procNameStr = $procNameIdent
|
||||
if not procNameStr.startsWith("signature"):
|
||||
error("Signature proc names must start with `signature`", procName)
|
||||
let params = stmt.params
|
||||
if params.len == 0:
|
||||
error("Signature must declare a return type", stmt)
|
||||
let returnType = params[0]
|
||||
if not isReturnTypeValid(returnType, typeIdent):
|
||||
error(
|
||||
"Signature must return Future[Result[`" & $typeIdent & "`, string]]", stmt
|
||||
)
|
||||
let paramCount = params.len - 1
|
||||
if paramCount == 0:
|
||||
if zeroArgSig != nil:
|
||||
error("Only one zero-argument signature is allowed", stmt)
|
||||
zeroArgSig = stmt
|
||||
zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs")
|
||||
zeroArgFieldName = ident("providerNoArgs")
|
||||
elif paramCount >= 1:
|
||||
if argSig != nil:
|
||||
error("Only one argument-based signature is allowed", stmt)
|
||||
argSig = stmt
|
||||
argParams = @[]
|
||||
for idx in 1 ..< params.len:
|
||||
let paramDef = params[idx]
|
||||
if paramDef.kind != nnkIdentDefs:
|
||||
error(
|
||||
"Signature parameter must be a standard identifier declaration", paramDef
|
||||
)
|
||||
let paramTypeNode = paramDef[paramDef.len - 2]
|
||||
if paramTypeNode.kind == nnkEmpty:
|
||||
error("Signature parameter must declare a type", paramDef)
|
||||
var hasName = false
|
||||
for i in 0 ..< paramDef.len - 2:
|
||||
if paramDef[i].kind != nnkEmpty:
|
||||
hasName = true
|
||||
if not hasName:
|
||||
error("Signature parameter must declare a name", paramDef)
|
||||
argParams.add(copyNimTree(paramDef))
|
||||
argProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderWithArgs")
|
||||
argFieldName = ident("providerWithArgs")
|
||||
of nnkTypeSection, nnkEmpty:
|
||||
discard
|
||||
else:
|
||||
error("Unsupported statement inside MultiRequestBroker definition", stmt)
|
||||
|
||||
if zeroArgSig.isNil() and argSig.isNil():
|
||||
zeroArgSig = newEmptyNode()
|
||||
zeroArgProviderName = ident(sanitizeIdentName(typeIdent) & "ProviderNoArgs")
|
||||
zeroArgFieldName = ident("providerNoArgs")
|
||||
|
||||
var typeSection = newTree(nnkTypeSection)
|
||||
typeSection.add(newTree(nnkTypeDef, exportedTypeIdent, newEmptyNode(), objectDef))
|
||||
|
||||
var kindEnum = newTree(nnkEnumTy, newEmptyNode())
|
||||
if not zeroArgSig.isNil():
|
||||
kindEnum.add(zeroKindIdent)
|
||||
if not argSig.isNil():
|
||||
kindEnum.add(argKindIdent)
|
||||
typeSection.add(newTree(nnkTypeDef, providerKindIdent, newEmptyNode(), kindEnum))
|
||||
|
||||
var handleRecList = newTree(nnkRecList)
|
||||
handleRecList.add(newTree(nnkIdentDefs, ident("id"), uint64Ident, newEmptyNode()))
|
||||
handleRecList.add(
|
||||
newTree(nnkIdentDefs, ident("kind"), providerKindIdent, newEmptyNode())
|
||||
)
|
||||
typeSection.add(
|
||||
newTree(
|
||||
nnkTypeDef,
|
||||
exportedProviderHandleIdent,
|
||||
newEmptyNode(),
|
||||
newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), handleRecList),
|
||||
)
|
||||
)
|
||||
|
||||
let returnType = quote:
|
||||
Future[Result[`typeIdent`, string]]
|
||||
|
||||
if not zeroArgSig.isNil():
|
||||
let procType = makeProcType(returnType, @[])
|
||||
typeSection.add(newTree(nnkTypeDef, zeroArgProviderName, newEmptyNode(), procType))
|
||||
if not argSig.isNil():
|
||||
let procType = makeProcType(returnType, cloneParams(argParams))
|
||||
typeSection.add(newTree(nnkTypeDef, argProviderName, newEmptyNode(), procType))
|
||||
|
||||
var bucketRecList = newTree(nnkRecList)
|
||||
bucketRecList.add(
|
||||
newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode())
|
||||
)
|
||||
if not zeroArgSig.isNil():
|
||||
bucketRecList.add(
|
||||
newTree(
|
||||
nnkIdentDefs,
|
||||
zeroArgFieldName,
|
||||
newTree(nnkBracketExpr, ident("seq"), zeroArgProviderName),
|
||||
newEmptyNode(),
|
||||
)
|
||||
)
|
||||
if not argSig.isNil():
|
||||
bucketRecList.add(
|
||||
newTree(
|
||||
nnkIdentDefs,
|
||||
argFieldName,
|
||||
newTree(nnkBracketExpr, ident("seq"), argProviderName),
|
||||
newEmptyNode(),
|
||||
)
|
||||
)
|
||||
typeSection.add(
|
||||
newTree(
|
||||
nnkTypeDef,
|
||||
bucketTypeIdent,
|
||||
newEmptyNode(),
|
||||
newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), bucketRecList),
|
||||
)
|
||||
)
|
||||
|
||||
var brokerRecList = newTree(nnkRecList)
|
||||
brokerRecList.add(
|
||||
newTree(
|
||||
nnkIdentDefs,
|
||||
ident("buckets"),
|
||||
newTree(nnkBracketExpr, ident("seq"), bucketTypeIdent),
|
||||
newEmptyNode(),
|
||||
)
|
||||
)
|
||||
let brokerTypeIdent = ident(sanitizeIdentName(typeIdent) & "Broker")
|
||||
typeSection.add(
|
||||
newTree(
|
||||
nnkTypeDef,
|
||||
brokerTypeIdent,
|
||||
newEmptyNode(),
|
||||
newTree(
|
||||
nnkRefTy, newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), brokerRecList)
|
||||
),
|
||||
)
|
||||
)
|
||||
result = newStmtList()
|
||||
result.add(typeSection)
|
||||
|
||||
let globalVarIdent = ident("g" & sanitizeIdentName(typeIdent) & "Broker")
|
||||
let accessProcIdent = ident("access" & sanitizeIdentName(typeIdent) & "Broker")
|
||||
result.add(
|
||||
quote do:
|
||||
var `globalVarIdent` {.threadvar.}: `brokerTypeIdent`
|
||||
|
||||
proc `findBucketIdxIdent`(
|
||||
broker: `brokerTypeIdent`, brokerCtx: BrokerContext
|
||||
): int =
|
||||
if brokerCtx == DefaultBrokerContext:
|
||||
return 0
|
||||
for i in 1 ..< broker.buckets.len:
|
||||
if broker.buckets[i].brokerCtx == brokerCtx:
|
||||
return i
|
||||
return -1
|
||||
|
||||
proc `getOrCreateBucketIdxIdent`(
|
||||
broker: `brokerTypeIdent`, brokerCtx: BrokerContext
|
||||
): int =
|
||||
let idx = `findBucketIdxIdent`(broker, brokerCtx)
|
||||
if idx >= 0:
|
||||
return idx
|
||||
broker.buckets.add(`bucketTypeIdent`(brokerCtx: brokerCtx))
|
||||
return broker.buckets.high
|
||||
|
||||
proc `accessProcIdent`(): `brokerTypeIdent` =
|
||||
if `globalVarIdent`.isNil():
|
||||
new(`globalVarIdent`)
|
||||
`globalVarIdent`.buckets =
|
||||
@[`bucketTypeIdent`(brokerCtx: DefaultBrokerContext)]
|
||||
return `globalVarIdent`
|
||||
|
||||
)
|
||||
|
||||
var clearBody = newStmtList()
|
||||
if not zeroArgSig.isNil():
|
||||
result.add(
|
||||
quote do:
|
||||
proc setProvider*(
|
||||
_: typedesc[`typeIdent`],
|
||||
brokerCtx: BrokerContext,
|
||||
handler: `zeroArgProviderName`,
|
||||
): Result[`providerHandleIdent`, string] =
|
||||
if handler.isNil():
|
||||
return err("Provider handler must be provided")
|
||||
let broker = `accessProcIdent`()
|
||||
let bucketIdx = `getOrCreateBucketIdxIdent`(broker, brokerCtx)
|
||||
for i, existing in broker.buckets[bucketIdx].`zeroArgFieldName`:
|
||||
if not existing.isNil() and existing == handler:
|
||||
return ok(`providerHandleIdent`(id: uint64(i + 1), kind: `zeroKindIdent`))
|
||||
broker.buckets[bucketIdx].`zeroArgFieldName`.add(handler)
|
||||
return ok(
|
||||
`providerHandleIdent`(
|
||||
id: uint64(broker.buckets[bucketIdx].`zeroArgFieldName`.len),
|
||||
kind: `zeroKindIdent`,
|
||||
)
|
||||
)
|
||||
|
||||
proc setProvider*(
|
||||
_: typedesc[`typeIdent`], handler: `zeroArgProviderName`
|
||||
): Result[`providerHandleIdent`, string] =
|
||||
return setProvider(`typeIdent`, DefaultBrokerContext, handler)
|
||||
|
||||
)
|
||||
result.add(
|
||||
quote do:
|
||||
proc request*(
|
||||
_: typedesc[`typeIdent`], brokerCtx: BrokerContext
|
||||
): Future[Result[seq[`typeIdent`], string]] {.async: (raises: []), gcsafe.} =
|
||||
var aggregated: seq[`typeIdent`] = @[]
|
||||
let broker = `accessProcIdent`()
|
||||
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
|
||||
if bucketIdx < 0:
|
||||
return ok(aggregated)
|
||||
let providers = broker.buckets[bucketIdx].`zeroArgFieldName`
|
||||
if providers.len == 0:
|
||||
return ok(aggregated)
|
||||
# var providersFut: seq[Future[Result[`typeIdent`, string]]] = collect:
|
||||
var providersFut = collect(newSeq):
|
||||
for provider in providers:
|
||||
if provider.isNil():
|
||||
continue
|
||||
provider()
|
||||
|
||||
let catchable = catch:
|
||||
await allFinished(providersFut)
|
||||
|
||||
catchable.isOkOr:
|
||||
return err("Some provider(s) failed:" & error.msg)
|
||||
|
||||
for fut in catchable.get():
|
||||
if fut.failed():
|
||||
return err("Some provider(s) failed:" & fut.error.msg)
|
||||
elif fut.finished():
|
||||
let providerResult = fut.value()
|
||||
if providerResult.isOk:
|
||||
let providerValue = providerResult.get()
|
||||
when `isRefObjectLit`:
|
||||
if providerValue.isNil():
|
||||
return err(
|
||||
"MultiRequestBroker(" & `typeNameLit` &
|
||||
"): provider returned nil result"
|
||||
)
|
||||
aggregated.add(providerValue)
|
||||
else:
|
||||
return err("Some provider(s) failed:" & providerResult.error)
|
||||
|
||||
return ok(aggregated)
|
||||
|
||||
proc request*(
|
||||
_: typedesc[`typeIdent`]
|
||||
): Future[Result[seq[`typeIdent`], string]] =
|
||||
return request(`typeIdent`, DefaultBrokerContext)
|
||||
|
||||
)
|
||||
if not argSig.isNil():
|
||||
result.add(
|
||||
quote do:
|
||||
proc setProvider*(
|
||||
_: typedesc[`typeIdent`],
|
||||
brokerCtx: BrokerContext,
|
||||
handler: `argProviderName`,
|
||||
): Result[`providerHandleIdent`, string] =
|
||||
if handler.isNil():
|
||||
return err("Provider handler must be provided")
|
||||
let broker = `accessProcIdent`()
|
||||
let bucketIdx = `getOrCreateBucketIdxIdent`(broker, brokerCtx)
|
||||
for i, existing in broker.buckets[bucketIdx].`argFieldName`:
|
||||
if not existing.isNil() and existing == handler:
|
||||
return ok(`providerHandleIdent`(id: uint64(i + 1), kind: `argKindIdent`))
|
||||
broker.buckets[bucketIdx].`argFieldName`.add(handler)
|
||||
return ok(
|
||||
`providerHandleIdent`(
|
||||
id: uint64(broker.buckets[bucketIdx].`argFieldName`.len),
|
||||
kind: `argKindIdent`,
|
||||
)
|
||||
)
|
||||
|
||||
proc setProvider*(
|
||||
_: typedesc[`typeIdent`], handler: `argProviderName`
|
||||
): Result[`providerHandleIdent`, string] =
|
||||
return setProvider(`typeIdent`, DefaultBrokerContext, handler)
|
||||
|
||||
)
|
||||
let requestParamDefs = cloneParams(argParams)
|
||||
let argNameIdents = collectParamNames(requestParamDefs)
|
||||
let providerSym = genSym(nskLet, "providerVal")
|
||||
var providerCall = newCall(providerSym)
|
||||
for argName in argNameIdents:
|
||||
providerCall.add(argName)
|
||||
var formalParams = newTree(nnkFormalParams)
|
||||
formalParams.add(
|
||||
quote do:
|
||||
Future[Result[seq[`typeIdent`], string]]
|
||||
)
|
||||
formalParams.add(
|
||||
newTree(
|
||||
nnkIdentDefs,
|
||||
ident("_"),
|
||||
newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)),
|
||||
newEmptyNode(),
|
||||
)
|
||||
)
|
||||
formalParams.add(
|
||||
newTree(nnkIdentDefs, ident("brokerCtx"), ident("BrokerContext"), newEmptyNode())
|
||||
)
|
||||
for paramDef in requestParamDefs:
|
||||
formalParams.add(paramDef)
|
||||
let requestPragmas = quote:
|
||||
{.async: (raises: []), gcsafe.}
|
||||
let requestBody = quote:
|
||||
var aggregated: seq[`typeIdent`] = @[]
|
||||
let broker = `accessProcIdent`()
|
||||
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
|
||||
if bucketIdx < 0:
|
||||
return ok(aggregated)
|
||||
let providers = broker.buckets[bucketIdx].`argFieldName`
|
||||
if providers.len == 0:
|
||||
return ok(aggregated)
|
||||
var providersFut = collect(newSeq):
|
||||
for provider in providers:
|
||||
if provider.isNil():
|
||||
continue
|
||||
let `providerSym` = provider
|
||||
`providerCall`
|
||||
let catchable = catch:
|
||||
await allFinished(providersFut)
|
||||
catchable.isOkOr:
|
||||
return err("Some provider(s) failed:" & error.msg)
|
||||
for fut in catchable.get():
|
||||
if fut.failed():
|
||||
return err("Some provider(s) failed:" & fut.error.msg)
|
||||
elif fut.finished():
|
||||
let providerResult = fut.value()
|
||||
if providerResult.isOk:
|
||||
let providerValue = providerResult.get()
|
||||
when `isRefObjectLit`:
|
||||
if providerValue.isNil():
|
||||
return err(
|
||||
"MultiRequestBroker(" & `typeNameLit` &
|
||||
"): provider returned nil result"
|
||||
)
|
||||
aggregated.add(providerValue)
|
||||
else:
|
||||
return err("Some provider(s) failed:" & providerResult.error)
|
||||
return ok(aggregated)
|
||||
|
||||
result.add(
|
||||
newTree(
|
||||
nnkProcDef,
|
||||
postfix(ident("request"), "*"),
|
||||
newEmptyNode(),
|
||||
newEmptyNode(),
|
||||
formalParams,
|
||||
requestPragmas,
|
||||
newEmptyNode(),
|
||||
requestBody,
|
||||
)
|
||||
)
|
||||
|
||||
# Backward-compatible default-context overload (no brokerCtx parameter).
|
||||
var formalParamsDefault = newTree(nnkFormalParams)
|
||||
formalParamsDefault.add(
|
||||
quote do:
|
||||
Future[Result[seq[`typeIdent`], string]]
|
||||
)
|
||||
formalParamsDefault.add(
|
||||
newTree(
|
||||
nnkIdentDefs,
|
||||
ident("_"),
|
||||
newTree(nnkBracketExpr, ident("typedesc"), copyNimTree(typeIdent)),
|
||||
newEmptyNode(),
|
||||
)
|
||||
)
|
||||
for paramDef in requestParamDefs:
|
||||
formalParamsDefault.add(copyNimTree(paramDef))
|
||||
|
||||
var wrapperCall = newCall(ident("request"))
|
||||
wrapperCall.add(copyNimTree(typeIdent))
|
||||
wrapperCall.add(ident("DefaultBrokerContext"))
|
||||
for argName in argNameIdents:
|
||||
wrapperCall.add(copyNimTree(argName))
|
||||
|
||||
result.add(
|
||||
newTree(
|
||||
nnkProcDef,
|
||||
postfix(ident("request"), "*"),
|
||||
newEmptyNode(),
|
||||
newEmptyNode(),
|
||||
formalParamsDefault,
|
||||
newEmptyNode(),
|
||||
newEmptyNode(),
|
||||
newStmtList(newTree(nnkReturnStmt, wrapperCall)),
|
||||
)
|
||||
)
|
||||
let removeHandleCtxSym = genSym(nskParam, "handle")
|
||||
let removeHandleDefaultSym = genSym(nskParam, "handle")
|
||||
|
||||
when true:
|
||||
# Generate clearProviders / removeProvider with macro-time knowledge about which
|
||||
# provider lists exist (zero-arg and/or arg providers).
|
||||
if not zeroArgSig.isNil() and not argSig.isNil():
|
||||
result.add(
|
||||
quote do:
|
||||
proc clearProviders*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) =
|
||||
let broker = `accessProcIdent`()
|
||||
if broker.isNil():
|
||||
return
|
||||
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
|
||||
if bucketIdx < 0:
|
||||
return
|
||||
broker.buckets[bucketIdx].`zeroArgFieldName`.setLen(0)
|
||||
broker.buckets[bucketIdx].`argFieldName`.setLen(0)
|
||||
if brokerCtx != DefaultBrokerContext:
|
||||
broker.buckets.delete(bucketIdx)
|
||||
|
||||
proc clearProviders*(_: typedesc[`typeIdent`]) =
|
||||
clearProviders(`typeIdent`, DefaultBrokerContext)
|
||||
|
||||
proc removeProvider*(
|
||||
_: typedesc[`typeIdent`],
|
||||
brokerCtx: BrokerContext,
|
||||
`removeHandleCtxSym`: `providerHandleIdent`,
|
||||
) =
|
||||
if `removeHandleCtxSym`.id == 0'u64:
|
||||
return
|
||||
let broker = `accessProcIdent`()
|
||||
if broker.isNil():
|
||||
return
|
||||
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
|
||||
if bucketIdx < 0:
|
||||
return
|
||||
|
||||
if `removeHandleCtxSym`.kind == `zeroKindIdent`:
|
||||
let idx = int(`removeHandleCtxSym`.id) - 1
|
||||
if idx >= 0 and idx < broker.buckets[bucketIdx].`zeroArgFieldName`.len:
|
||||
broker.buckets[bucketIdx].`zeroArgFieldName`[idx] = nil
|
||||
elif `removeHandleCtxSym`.kind == `argKindIdent`:
|
||||
let idx = int(`removeHandleCtxSym`.id) - 1
|
||||
if idx >= 0 and idx < broker.buckets[bucketIdx].`argFieldName`.len:
|
||||
broker.buckets[bucketIdx].`argFieldName`[idx] = nil
|
||||
|
||||
if brokerCtx != DefaultBrokerContext:
|
||||
var hasAny = false
|
||||
for p in broker.buckets[bucketIdx].`zeroArgFieldName`:
|
||||
if not p.isNil():
|
||||
hasAny = true
|
||||
break
|
||||
if not hasAny:
|
||||
for p in broker.buckets[bucketIdx].`argFieldName`:
|
||||
if not p.isNil():
|
||||
hasAny = true
|
||||
break
|
||||
if not hasAny:
|
||||
broker.buckets.delete(bucketIdx)
|
||||
|
||||
proc removeProvider*(
|
||||
_: typedesc[`typeIdent`], `removeHandleDefaultSym`: `providerHandleIdent`
|
||||
) =
|
||||
removeProvider(`typeIdent`, DefaultBrokerContext, `removeHandleDefaultSym`)
|
||||
|
||||
)
|
||||
elif not zeroArgSig.isNil():
|
||||
result.add(
|
||||
quote do:
|
||||
proc clearProviders*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) =
|
||||
let broker = `accessProcIdent`()
|
||||
if broker.isNil():
|
||||
return
|
||||
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
|
||||
if bucketIdx < 0:
|
||||
return
|
||||
broker.buckets[bucketIdx].`zeroArgFieldName`.setLen(0)
|
||||
if brokerCtx != DefaultBrokerContext:
|
||||
broker.buckets.delete(bucketIdx)
|
||||
|
||||
proc clearProviders*(_: typedesc[`typeIdent`]) =
|
||||
clearProviders(`typeIdent`, DefaultBrokerContext)
|
||||
|
||||
proc removeProvider*(
|
||||
_: typedesc[`typeIdent`],
|
||||
brokerCtx: BrokerContext,
|
||||
`removeHandleCtxSym`: `providerHandleIdent`,
|
||||
) =
|
||||
if `removeHandleCtxSym`.id == 0'u64:
|
||||
return
|
||||
let broker = `accessProcIdent`()
|
||||
if broker.isNil():
|
||||
return
|
||||
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
|
||||
if bucketIdx < 0:
|
||||
return
|
||||
if `removeHandleCtxSym`.kind != `zeroKindIdent`:
|
||||
return
|
||||
let idx = int(`removeHandleCtxSym`.id) - 1
|
||||
if idx >= 0 and idx < broker.buckets[bucketIdx].`zeroArgFieldName`.len:
|
||||
broker.buckets[bucketIdx].`zeroArgFieldName`[idx] = nil
|
||||
if brokerCtx != DefaultBrokerContext:
|
||||
var hasAny = false
|
||||
for p in broker.buckets[bucketIdx].`zeroArgFieldName`:
|
||||
if not p.isNil():
|
||||
hasAny = true
|
||||
break
|
||||
if not hasAny:
|
||||
broker.buckets.delete(bucketIdx)
|
||||
|
||||
proc removeProvider*(
|
||||
_: typedesc[`typeIdent`], `removeHandleDefaultSym`: `providerHandleIdent`
|
||||
) =
|
||||
removeProvider(`typeIdent`, DefaultBrokerContext, `removeHandleDefaultSym`)
|
||||
|
||||
)
|
||||
else:
|
||||
result.add(
|
||||
quote do:
|
||||
proc clearProviders*(_: typedesc[`typeIdent`], brokerCtx: BrokerContext) =
|
||||
let broker = `accessProcIdent`()
|
||||
if broker.isNil():
|
||||
return
|
||||
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
|
||||
if bucketIdx < 0:
|
||||
return
|
||||
broker.buckets[bucketIdx].`argFieldName`.setLen(0)
|
||||
if brokerCtx != DefaultBrokerContext:
|
||||
broker.buckets.delete(bucketIdx)
|
||||
|
||||
proc clearProviders*(_: typedesc[`typeIdent`]) =
|
||||
clearProviders(`typeIdent`, DefaultBrokerContext)
|
||||
|
||||
proc removeProvider*(
|
||||
_: typedesc[`typeIdent`],
|
||||
brokerCtx: BrokerContext,
|
||||
`removeHandleCtxSym`: `providerHandleIdent`,
|
||||
) =
|
||||
if `removeHandleCtxSym`.id == 0'u64:
|
||||
return
|
||||
let broker = `accessProcIdent`()
|
||||
if broker.isNil():
|
||||
return
|
||||
let bucketIdx = `findBucketIdxIdent`(broker, brokerCtx)
|
||||
if bucketIdx < 0:
|
||||
return
|
||||
if `removeHandleCtxSym`.kind != `argKindIdent`:
|
||||
return
|
||||
let idx = int(`removeHandleCtxSym`.id) - 1
|
||||
if idx >= 0 and idx < broker.buckets[bucketIdx].`argFieldName`.len:
|
||||
broker.buckets[bucketIdx].`argFieldName`[idx] = nil
|
||||
if brokerCtx != DefaultBrokerContext:
|
||||
var hasAny = false
|
||||
for p in broker.buckets[bucketIdx].`argFieldName`:
|
||||
if not p.isNil():
|
||||
hasAny = true
|
||||
break
|
||||
if not hasAny:
|
||||
broker.buckets.delete(bucketIdx)
|
||||
|
||||
proc removeProvider*(
|
||||
_: typedesc[`typeIdent`], `removeHandleDefaultSym`: `providerHandleIdent`
|
||||
) =
|
||||
removeProvider(`typeIdent`, DefaultBrokerContext, `removeHandleDefaultSym`)
|
||||
|
||||
)
|
||||
|
||||
when defined(brokerDebug):
|
||||
writeBrokerDebug("MultiRequestBroker", sanitized, result)
|
||||
when defined(brokerDebugStdout):
|
||||
echo result.repr
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metaData": {
|
||||
"url": "https://github.com/NagyZoltanPeter/nim-brokers.git",
|
||||
"downloadMethod": "git",
|
||||
"vcsRevision": "a7316a35f1b62e3497ae8ee0fc1aace74df0beb2",
|
||||
"files": [
|
||||
"/brokers/internal/mt_broker_common.nim",
|
||||
"/brokers.nim",
|
||||
"/brokers/event_broker.nim",
|
||||
"/brokers/internal/mt_queue.nim",
|
||||
"/brokers/internal/api_cbor_descriptor.nim",
|
||||
"/brokers/internal/api_codegen_cbor_hpp.nim",
|
||||
"/brokers/api_library.nim",
|
||||
"/brokers/internal/api_codegen_cmake.nim",
|
||||
"/brokers/internal/api_cbor_tuple.nim",
|
||||
"/brokers/internal/mt_codec.nim",
|
||||
"/brokers/broker_context.nim",
|
||||
"/brokers/internal/api_type_resolver.nim",
|
||||
"/brokers/multi_request_broker.nim",
|
||||
"/brokers/internal/api_request_broker_cbor.nim",
|
||||
"/brokers/internal/api_outdir.nim",
|
||||
"/brokers/internal/api_codegen_cbor_h.nim",
|
||||
"/brokers/broker_interface.nim",
|
||||
"/brokers/internal/api_codegen_cbor_go.nim",
|
||||
"/brokers/internal/api_event_broker_cbor.nim",
|
||||
"/brokers/broker_implement.nim",
|
||||
"/brokers/internal/api_schema.nim",
|
||||
"/brokers/internal/api_codegen_cbor_py.nim",
|
||||
"/brokers/internal/api_codegen_cbor_cddl.nim",
|
||||
"/brokers/internal/helper/broker_utils.nim",
|
||||
"/brokers/internal/mt_config.nim",
|
||||
"/brokers/internal/api_common.nim",
|
||||
"/brokers/internal/mt_event_broker.nim",
|
||||
"/brokers/internal/api_cbor_codec.nim",
|
||||
"/brokers/request_broker.nim",
|
||||
"/brokers/internal/api_cbor_subs_registry.nim",
|
||||
"/brokers/internal/api_cbor_event_courier.nim",
|
||||
"/brokers/internal/broker_debug.nim",
|
||||
"/brokers.nimble",
|
||||
"/brokers/internal/api_codegen_cbor_rust.nim",
|
||||
"/brokers/internal/api_cbor_courier.nim",
|
||||
"/brokers/internal/mt_request_broker.nim"
|
||||
],
|
||||
"binaries": [],
|
||||
"specialVersions": [
|
||||
"3.1.1",
|
||||
"#v3.1.1"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
# SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
# Copyright (c) Status Research & Development GmbH
|
||||
|
||||
## This module contains a Switch Building helper.
|
||||
runnableExamples:
|
||||
let switch = SwitchBuilder.new().withRng(rng).withAddresses(multiaddress)
|
||||
# etc
|
||||
.build()
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
# NOTE: wasm/edge override of libp2p/builders. Identical to upstream EXCEPT the
|
||||
# QUIC transport is removed — quictransport pulls lsquic + boringssl x86 asm that
|
||||
# cannot build for wasm32, and an edge browser node never uses QUIC. Imports are
|
||||
# rewritten to absolute `libp2p/...` form so this single-file override resolves
|
||||
# the rest of the package (Nim keys modules by absolute path, so type identity is
|
||||
# preserved). Placed first on the path via --path so it shadows upstream builders.
|
||||
import options, tables, chronos, chronicles, sequtils
|
||||
import
|
||||
libp2p/switch,
|
||||
libp2p/peerid,
|
||||
libp2p/peerinfo,
|
||||
libp2p/stream/connection,
|
||||
libp2p/multiaddress,
|
||||
libp2p/crypto/crypto,
|
||||
# wstransport dropped: it imports autotls/service -> certificate_ffi -> lsquic.
|
||||
# The edge node uses WsBrowserTransport; builders' withWsTransport is removed.
|
||||
libp2p/transports/[transport, tcptransport, memorytransport],
|
||||
libp2p/muxers/[muxer, mplex/mplex, yamux/yamux],
|
||||
libp2p/protocols/[identify, secure/secure, secure/noise, rendezvous, kademlia],
|
||||
libp2p/protocols/connectivity/[
|
||||
autonat/server,
|
||||
autonatv2/server,
|
||||
autonatv2/service,
|
||||
autonatv2/client,
|
||||
relay/relay,
|
||||
relay/client,
|
||||
relay/rtransport,
|
||||
],
|
||||
libp2p/connmanager,
|
||||
libp2p/upgrademngrs/muxedupgrade,
|
||||
libp2p/observedaddrmanager,
|
||||
libp2p/nameresolving/nameresolver,
|
||||
libp2p/errors,
|
||||
libp2p/utility
|
||||
import libp2p/services/wildcardresolverservice
|
||||
|
||||
# autotls is trimmed in the wasm/edge override: its `certificate_ffi` pulls
|
||||
# lsquic/boringssl, which won't build for wasm, and a browser edge node never
|
||||
# provisions TLS certs (the browser's WebSocket handles wss). The field stays as
|
||||
# a never-`Some` stub so the SwitchBuilder shape is otherwise unchanged.
|
||||
type AutotlsService* = ref object
|
||||
|
||||
# TLSPrivateKey/TLSCertificate/TLSFlags dropped from exports — they came from the
|
||||
# removed wstransport. ServerFlags stays (from tcptransport).
|
||||
export switch, peerid, peerinfo, connection, multiaddress, crypto, errors, ServerFlags
|
||||
|
||||
const MemoryAutoAddress* = memorytransport.MemoryAutoAddress
|
||||
|
||||
type
|
||||
TransportProvider* {.deprecated: "Use TransportBuilder instead".} =
|
||||
proc(upgr: Upgrade, privateKey: PrivateKey): Transport {.gcsafe, raises: [].}
|
||||
|
||||
TransportBuilder* {.public.} =
|
||||
proc(config: TransportConfig): Transport {.gcsafe, raises: [].}
|
||||
|
||||
TransportConfig* = ref object
|
||||
upgr*: Upgrade
|
||||
privateKey*: PrivateKey
|
||||
autotls*: Opt[AutotlsService]
|
||||
|
||||
SecureProtocol* {.pure.} = enum
|
||||
Noise
|
||||
|
||||
KadInfo = object
|
||||
config*: KadDHTConfig
|
||||
bootstrapNodes*: seq[(PeerId, seq[MultiAddress])]
|
||||
|
||||
SwitchBuilder* = ref object
|
||||
privKey: Opt[PrivateKey]
|
||||
addresses: seq[MultiAddress]
|
||||
secureManagers: seq[SecureProtocol]
|
||||
muxers: seq[MuxerProvider]
|
||||
transports: seq[TransportBuilder]
|
||||
rng: ref HmacDrbgContext
|
||||
maxConnections: int
|
||||
maxIn: int
|
||||
sendSignedPeerRecord: bool
|
||||
maxOut: int
|
||||
maxConnsPerPeer: int
|
||||
protoVersion: string
|
||||
agentVersion: string
|
||||
nameResolver: NameResolver
|
||||
peerStoreCapacity: Opt[int]
|
||||
autonat: bool
|
||||
autonatV2ServerConfig: Opt[AutonatV2Config]
|
||||
autonatV2Client: AutonatV2Client
|
||||
autonatV2ServiceConfig: AutonatV2ServiceConfig
|
||||
autotls: Opt[AutotlsService]
|
||||
circuitRelay: Opt[Relay]
|
||||
rdv: Opt[RendezVous]
|
||||
kad: Opt[KadInfo]
|
||||
services: seq[Service]
|
||||
observedAddrManager: ObservedAddrManager
|
||||
enableWildcardResolver: bool
|
||||
|
||||
proc new*(T: type[SwitchBuilder]): T {.public.} =
|
||||
## Creates a SwitchBuilder
|
||||
|
||||
let address =
|
||||
MultiAddress.init("/ip4/127.0.0.1/tcp/0").expect("Should initialize to default")
|
||||
|
||||
SwitchBuilder(
|
||||
privKey: Opt.none(PrivateKey),
|
||||
addresses: @[address],
|
||||
secureManagers: @[],
|
||||
maxConnections: MaxConnections,
|
||||
maxIn: -1,
|
||||
maxOut: -1,
|
||||
maxConnsPerPeer: MaxConnectionsPerPeer,
|
||||
protoVersion: ProtoVersion,
|
||||
agentVersion: AgentVersion,
|
||||
autotls: Opt.none(AutotlsService),
|
||||
circuitRelay: Opt.none(Relay),
|
||||
rdv: Opt.none(RendezVous),
|
||||
kad: Opt.none(KadInfo),
|
||||
enableWildcardResolver: true,
|
||||
)
|
||||
|
||||
proc withPrivateKey*(
|
||||
b: SwitchBuilder, privateKey: PrivateKey
|
||||
): SwitchBuilder {.public.} =
|
||||
## Set the private key of the switch. Will be used to
|
||||
## generate a PeerId
|
||||
|
||||
b.privKey = Opt.some(privateKey)
|
||||
b
|
||||
|
||||
proc withAddresses*(
|
||||
b: SwitchBuilder, addresses: seq[MultiAddress], enableWildcardResolver: bool = true
|
||||
): SwitchBuilder {.public.} =
|
||||
## | Set the listening addresses of the switch
|
||||
## | Calling it multiple time will override the value
|
||||
b.addresses = addresses
|
||||
b.enableWildcardResolver = enableWildcardResolver
|
||||
b
|
||||
|
||||
proc withAddress*(
|
||||
b: SwitchBuilder, address: MultiAddress, enableWildcardResolver: bool = true
|
||||
): SwitchBuilder {.public.} =
|
||||
## | Set the listening address of the switch
|
||||
## | Calling it multiple time will override the value
|
||||
b.withAddresses(@[address], enableWildcardResolver)
|
||||
|
||||
proc withSignedPeerRecord*(b: SwitchBuilder, sendIt = true): SwitchBuilder {.public.} =
|
||||
b.sendSignedPeerRecord = sendIt
|
||||
b
|
||||
|
||||
proc withMplex*(
|
||||
b: SwitchBuilder, inTimeout = 5.minutes, outTimeout = 5.minutes, maxChannCount = 200
|
||||
): SwitchBuilder {.public.} =
|
||||
## | Uses `Mplex <https://docs.libp2p.io/concepts/stream-multiplexing/#mplex>`_ as a multiplexer
|
||||
## | `Timeout` is the duration after which a inactive connection will be closed
|
||||
proc newMuxer(conn: Connection): Muxer =
|
||||
Mplex.new(conn, inTimeout, outTimeout, maxChannCount)
|
||||
|
||||
assert b.muxers.countIt(it.codec == MplexCodec) == 0, "Mplex build multiple times"
|
||||
b.muxers.add(MuxerProvider.new(newMuxer, MplexCodec))
|
||||
b
|
||||
|
||||
proc withYamux*(
|
||||
b: SwitchBuilder,
|
||||
maxChannCount: int = MaxChannelCount,
|
||||
windowSize: int = YamuxDefaultWindowSize,
|
||||
inTimeout: Duration = 5.minutes,
|
||||
outTimeout: Duration = 5.minutes,
|
||||
): SwitchBuilder =
|
||||
proc newMuxer(conn: Connection): Muxer =
|
||||
Yamux.new(
|
||||
conn,
|
||||
maxChannCount = maxChannCount,
|
||||
windowSize = windowSize,
|
||||
inTimeout = inTimeout,
|
||||
outTimeout = outTimeout,
|
||||
)
|
||||
|
||||
assert b.muxers.countIt(it.codec == YamuxCodec) == 0, "Yamux build multiple times"
|
||||
b.muxers.add(MuxerProvider.new(newMuxer, YamuxCodec))
|
||||
b
|
||||
|
||||
proc withNoise*(b: SwitchBuilder): SwitchBuilder {.public.} =
|
||||
b.secureManagers.add(SecureProtocol.Noise)
|
||||
b
|
||||
|
||||
proc withTransport*(
|
||||
b: SwitchBuilder, prov: TransportBuilder
|
||||
): SwitchBuilder {.public.} =
|
||||
## Use a custom transport
|
||||
runnableExamples:
|
||||
let switch = SwitchBuilder
|
||||
.new()
|
||||
.withTransport(
|
||||
proc(config: TransportConfig): Transport =
|
||||
TcpTransport.new(flags, config.upgr)
|
||||
)
|
||||
.build()
|
||||
b.transports.add(prov)
|
||||
b
|
||||
|
||||
proc withTransport*(
|
||||
b: SwitchBuilder, prov: TransportProvider
|
||||
): SwitchBuilder {.deprecated: "Use TransportBuilder instead".} =
|
||||
## Use a custom transport
|
||||
runnableExamples:
|
||||
let switch = SwitchBuilder
|
||||
.new()
|
||||
.withTransport(
|
||||
proc(upgr: Upgrade, privateKey: PrivateKey): Transport =
|
||||
TcpTransport.new(flags, upgr)
|
||||
)
|
||||
.build()
|
||||
let tBuilder: TransportBuilder = proc(config: TransportConfig): Transport =
|
||||
prov(config.upgr, config.privateKey)
|
||||
b.withTransport(tBuilder)
|
||||
|
||||
proc withTcpTransport*(
|
||||
b: SwitchBuilder, flags: set[ServerFlags] = {}
|
||||
): SwitchBuilder {.public.} =
|
||||
b.withTransport(
|
||||
proc(config: TransportConfig): Transport =
|
||||
TcpTransport.new(flags, config.upgr)
|
||||
)
|
||||
|
||||
# withWsTransport removed in the wasm/edge override: it threads `config.autotls`
|
||||
# into the real WsTransport, and the edge node uses the browser-WebSocket
|
||||
# transport instead. (withQuicTransport removed too — no QUIC in the browser.)
|
||||
|
||||
proc withMemoryTransport*(b: SwitchBuilder): SwitchBuilder {.public.} =
|
||||
b.withTransport(
|
||||
proc(config: TransportConfig): Transport =
|
||||
MemoryTransport.new(config.upgr)
|
||||
)
|
||||
|
||||
proc withRng*(b: SwitchBuilder, rng: ref HmacDrbgContext): SwitchBuilder {.public.} =
|
||||
b.rng = rng
|
||||
b
|
||||
|
||||
proc withMaxConnections*(
|
||||
b: SwitchBuilder, maxConnections: int
|
||||
): SwitchBuilder {.public.} =
|
||||
## Maximum concurrent connections of the switch. You should either use this, or
|
||||
## `withMaxIn <#withMaxIn,SwitchBuilder,int>`_ & `withMaxOut<#withMaxOut,SwitchBuilder,int>`_
|
||||
b.maxConnections = maxConnections
|
||||
b
|
||||
|
||||
proc withMaxIn*(b: SwitchBuilder, maxIn: int): SwitchBuilder {.public.} =
|
||||
## Maximum concurrent incoming connections. Should be used with `withMaxOut<#withMaxOut,SwitchBuilder,int>`_
|
||||
b.maxIn = maxIn
|
||||
b
|
||||
|
||||
proc withMaxOut*(b: SwitchBuilder, maxOut: int): SwitchBuilder {.public.} =
|
||||
## Maximum concurrent outgoing connections. Should be used with `withMaxIn<#withMaxIn,SwitchBuilder,int>`_
|
||||
b.maxOut = maxOut
|
||||
b
|
||||
|
||||
proc withMaxConnsPerPeer*(
|
||||
b: SwitchBuilder, maxConnsPerPeer: int
|
||||
): SwitchBuilder {.public.} =
|
||||
b.maxConnsPerPeer = maxConnsPerPeer
|
||||
b
|
||||
|
||||
proc withPeerStore*(b: SwitchBuilder, capacity: int): SwitchBuilder {.public.} =
|
||||
b.peerStoreCapacity = Opt.some(capacity)
|
||||
b
|
||||
|
||||
proc withProtoVersion*(
|
||||
b: SwitchBuilder, protoVersion: string
|
||||
): SwitchBuilder {.public.} =
|
||||
b.protoVersion = protoVersion
|
||||
b
|
||||
|
||||
proc withAgentVersion*(
|
||||
b: SwitchBuilder, agentVersion: string
|
||||
): SwitchBuilder {.public.} =
|
||||
b.agentVersion = agentVersion
|
||||
b
|
||||
|
||||
proc withNameResolver*(
|
||||
b: SwitchBuilder, nameResolver: NameResolver
|
||||
): SwitchBuilder {.public.} =
|
||||
b.nameResolver = nameResolver
|
||||
b
|
||||
|
||||
proc withAutonat*(b: SwitchBuilder): SwitchBuilder =
|
||||
b.autonat = true
|
||||
b
|
||||
|
||||
proc withAutonatV2Server*(
|
||||
b: SwitchBuilder, config: AutonatV2Config = AutonatV2Config.new()
|
||||
): SwitchBuilder =
|
||||
b.autonatV2ServerConfig = Opt.some(config)
|
||||
b
|
||||
|
||||
proc withAutonatV2*(
|
||||
b: SwitchBuilder, serviceConfig = AutonatV2ServiceConfig.new()
|
||||
): SwitchBuilder =
|
||||
b.autonatV2Client = AutonatV2Client.new(b.rng)
|
||||
b.autonatV2ServiceConfig = serviceConfig
|
||||
b
|
||||
|
||||
when defined(libp2p_autotls_support):
|
||||
proc withAutotls*(
|
||||
b: SwitchBuilder, config: AutotlsConfig = AutotlsConfig.new()
|
||||
): SwitchBuilder {.public.} =
|
||||
b.autotls = Opt.some(AutotlsService.new(config = config))
|
||||
b
|
||||
|
||||
proc withCircuitRelay*(b: SwitchBuilder, r: Relay = Relay.new()): SwitchBuilder =
|
||||
b.circuitRelay = Opt.some(r)
|
||||
b
|
||||
|
||||
proc withRendezVous*(b: SwitchBuilder, rdv: RendezVous): SwitchBuilder =
|
||||
var lrdv = rdv
|
||||
if rdv.isNil():
|
||||
lrdv = RendezVous.new()
|
||||
|
||||
b.rdv = Opt.some(lrdv)
|
||||
b
|
||||
|
||||
proc withKademlia*(
|
||||
b: SwitchBuilder,
|
||||
bootstrapNodes: seq[(PeerId, seq[MultiAddress])] = @[],
|
||||
config: KadDHTConfig = KadDHTConfig.new(),
|
||||
): SwitchBuilder =
|
||||
b.kad = Opt.some(KadInfo(config: config, bootstrapNodes: bootstrapNodes))
|
||||
b
|
||||
|
||||
proc withServices*(b: SwitchBuilder, services: seq[Service]): SwitchBuilder =
|
||||
b.services = services
|
||||
b
|
||||
|
||||
proc withObservedAddrManager*(
|
||||
b: SwitchBuilder, observedAddrManager: ObservedAddrManager
|
||||
): SwitchBuilder =
|
||||
b.observedAddrManager = observedAddrManager
|
||||
b
|
||||
|
||||
proc build*(b: SwitchBuilder): Switch {.raises: [LPError], public.} =
|
||||
if b.rng == nil: # newRng could fail
|
||||
raise newException(Defect, "Cannot initialize RNG")
|
||||
|
||||
let pkRes = PrivateKey.random(b.rng[])
|
||||
let seckey = b.privKey.get(otherwise = pkRes.expect("Expected default Private Key"))
|
||||
|
||||
if b.secureManagers.len == 0:
|
||||
debug "no secure managers defined. Adding noise by default"
|
||||
b.secureManagers.add(SecureProtocol.Noise)
|
||||
|
||||
var secureManagerInstances: seq[Secure]
|
||||
if SecureProtocol.Noise in b.secureManagers:
|
||||
secureManagerInstances.add(Noise.new(b.rng, seckey).Secure)
|
||||
|
||||
let peerInfo = PeerInfo.new(
|
||||
seckey, b.addresses, protoVersion = b.protoVersion, agentVersion = b.agentVersion
|
||||
)
|
||||
|
||||
let identify =
|
||||
if b.observedAddrManager != nil:
|
||||
Identify.new(peerInfo, b.sendSignedPeerRecord, b.observedAddrManager)
|
||||
else:
|
||||
Identify.new(peerInfo, b.sendSignedPeerRecord)
|
||||
|
||||
let
|
||||
connManager =
|
||||
ConnManager.new(b.maxConnsPerPeer, b.maxConnections, b.maxIn, b.maxOut)
|
||||
ms = MultistreamSelect.new()
|
||||
muxedUpgrade = MuxedUpgrade.new(b.muxers, secureManagerInstances, ms)
|
||||
|
||||
# autotls service is never created in the edge override (field is always none).
|
||||
|
||||
let transports = block:
|
||||
var transports: seq[Transport]
|
||||
for tProvider in b.transports:
|
||||
transports.add(
|
||||
tProvider(
|
||||
TransportConfig(upgr: muxedUpgrade, privateKey: seckey, autotls: b.autotls)
|
||||
)
|
||||
)
|
||||
transports
|
||||
|
||||
if b.secureManagers.len == 0:
|
||||
b.secureManagers &= SecureProtocol.Noise
|
||||
|
||||
if isNil(b.rng):
|
||||
b.rng = newRng()
|
||||
|
||||
let peerStore = block:
|
||||
b.peerStoreCapacity.withValue(capacity):
|
||||
PeerStore.new(identify, capacity)
|
||||
else:
|
||||
PeerStore.new(identify)
|
||||
|
||||
if b.enableWildcardResolver:
|
||||
b.services.add(WildcardAddressResolverService.new())
|
||||
|
||||
if not isNil(b.autonatV2Client):
|
||||
b.services.add(
|
||||
AutonatV2Service.new(
|
||||
b.rng, client = b.autonatV2Client, config = b.autonatV2ServiceConfig
|
||||
)
|
||||
)
|
||||
|
||||
let switch = newSwitch(
|
||||
peerInfo = peerInfo,
|
||||
transports = transports,
|
||||
secureManagers = secureManagerInstances,
|
||||
connManager = connManager,
|
||||
ms = ms,
|
||||
nameResolver = b.nameResolver,
|
||||
peerStore = peerStore,
|
||||
services = b.services,
|
||||
)
|
||||
|
||||
switch.mount(identify)
|
||||
|
||||
if not isNil(b.autonatV2Client):
|
||||
b.autonatV2Client.setup(switch)
|
||||
switch.mount(b.autonatV2Client)
|
||||
|
||||
b.autonatV2ServerConfig.withValue(config):
|
||||
switch.mount(AutonatV2.new(switch, config = config))
|
||||
|
||||
if b.autonat:
|
||||
switch.mount(Autonat.new(switch))
|
||||
|
||||
b.circuitRelay.withValue(relay):
|
||||
if relay of RelayClient:
|
||||
switch.addTransport(RelayTransport.new(RelayClient(relay), muxedUpgrade))
|
||||
relay.setup(switch)
|
||||
switch.mount(relay)
|
||||
|
||||
b.rdv.withValue(rdvService):
|
||||
rdvService.setup(switch)
|
||||
switch.mount(rdvService)
|
||||
|
||||
b.kad.withValue(kadInfo):
|
||||
let kad = KadDHT.new(
|
||||
switch, bootstrapNodes = kadInfo.bootstrapNodes, config = kadInfo.config
|
||||
)
|
||||
switch.mount(kad)
|
||||
|
||||
return switch
|
||||
|
||||
type TransportType* {.pure.} = enum
|
||||
TCP
|
||||
Memory
|
||||
|
||||
proc newStandardSwitchBuilder*(
|
||||
privKey = Opt.none(PrivateKey),
|
||||
addrs: MultiAddress | seq[MultiAddress] = newSeq[MultiAddress](),
|
||||
transport: TransportType = TransportType.TCP,
|
||||
transportFlags: set[ServerFlags] = {},
|
||||
rng = newRng(),
|
||||
secureManagers: openArray[SecureProtocol] = [SecureProtocol.Noise],
|
||||
inTimeout: Duration = 5.minutes,
|
||||
outTimeout: Duration = 5.minutes,
|
||||
maxConnections = MaxConnections,
|
||||
maxIn = -1,
|
||||
maxOut = -1,
|
||||
maxConnsPerPeer = MaxConnectionsPerPeer,
|
||||
nameResolver = Opt.none(NameResolver),
|
||||
sendSignedPeerRecord = false,
|
||||
peerStoreCapacity = 1000,
|
||||
): SwitchBuilder {.raises: [LPError], public.} =
|
||||
## Helper for common switch configurations.
|
||||
var b = SwitchBuilder
|
||||
.new()
|
||||
.withRng(rng)
|
||||
.withSignedPeerRecord(sendSignedPeerRecord)
|
||||
.withMaxConnections(maxConnections)
|
||||
.withMaxIn(maxIn)
|
||||
.withMaxOut(maxOut)
|
||||
.withMaxConnsPerPeer(maxConnsPerPeer)
|
||||
.withPeerStore(capacity = peerStoreCapacity)
|
||||
.withNoise()
|
||||
|
||||
privKey.withValue(pkey):
|
||||
b = b.withPrivateKey(pkey)
|
||||
|
||||
nameResolver.withValue(nr):
|
||||
b = b.withNameResolver(nr)
|
||||
|
||||
var addrs =
|
||||
when addrs is MultiAddress:
|
||||
@[addrs]
|
||||
else:
|
||||
addrs
|
||||
|
||||
case transport
|
||||
of TransportType.TCP:
|
||||
if addrs.len == 0:
|
||||
addrs = @[MultiAddress.init("/ip4/127.0.0.1/tcp/0").tryGet()]
|
||||
b = b.withTcpTransport(transportFlags).withAddresses(addrs).withMplex(
|
||||
inTimeout, outTimeout
|
||||
)
|
||||
of TransportType.Memory:
|
||||
if addrs.len == 0:
|
||||
addrs = @[MultiAddress.init(MemoryAutoAddress).tryGet()]
|
||||
b = b.withMemoryTransport().withAddresses(addrs).withMplex(inTimeout, outTimeout)
|
||||
|
||||
b
|
||||
|
||||
proc newStandardSwitch*(
|
||||
privKey = Opt.none(PrivateKey),
|
||||
addrs: MultiAddress | seq[MultiAddress] = newSeq[MultiAddress](),
|
||||
transport: TransportType = TransportType.TCP,
|
||||
transportFlags: set[ServerFlags] = {},
|
||||
rng = newRng(),
|
||||
secureManagers: openArray[SecureProtocol] = [SecureProtocol.Noise],
|
||||
inTimeout: Duration = 5.minutes,
|
||||
outTimeout: Duration = 5.minutes,
|
||||
maxConnections = MaxConnections,
|
||||
maxIn = -1,
|
||||
maxOut = -1,
|
||||
maxConnsPerPeer = MaxConnectionsPerPeer,
|
||||
nameResolver = Opt.none(NameResolver),
|
||||
sendSignedPeerRecord = false,
|
||||
peerStoreCapacity = 1000,
|
||||
): Switch {.raises: [LPError], public.} =
|
||||
newStandardSwitchBuilder(
|
||||
privKey = privKey,
|
||||
addrs = addrs,
|
||||
transport = transport,
|
||||
transportFlags = transportFlags,
|
||||
rng = rng,
|
||||
secureManagers = secureManagers,
|
||||
inTimeout = inTimeout,
|
||||
outTimeout = outTimeout,
|
||||
maxConnections = maxConnections,
|
||||
maxIn = maxIn,
|
||||
maxOut = maxOut,
|
||||
maxConnsPerPeer = maxConnsPerPeer,
|
||||
nameResolver = nameResolver,
|
||||
sendSignedPeerRecord = sendSignedPeerRecord,
|
||||
peerStoreCapacity = peerStoreCapacity,
|
||||
)
|
||||
.build()
|
||||
@@ -0,0 +1,10 @@
|
||||
import std/[atomics, tables]
|
||||
import chronos, chronicles
|
||||
import
|
||||
ffi/internal/[ffi_library, ffi_macro],
|
||||
ffi/[alloc, ffi_types, ffi_context, ffi_thread_request]
|
||||
|
||||
export atomics, tables
|
||||
export chronos, chronicles
|
||||
export
|
||||
atomics, alloc, ffi_library, ffi_macro, ffi_types, ffi_context, ffi_thread_request
|
||||
@@ -0,0 +1,22 @@
|
||||
# ffi.nimble
|
||||
|
||||
version = "0.1.3"
|
||||
author = "Institute of Free Technology"
|
||||
description = "FFI framework with custom header generation"
|
||||
license = "MIT or Apache License 2.0"
|
||||
|
||||
packageName = "ffi"
|
||||
|
||||
requires "nim >= 2.2.4"
|
||||
requires "chronos"
|
||||
requires "chronicles"
|
||||
requires "taskpools"
|
||||
|
||||
# Source files to include
|
||||
# srcDir = "src"
|
||||
# installFiles = @["src/ffi.nim", "mylib.h"]
|
||||
|
||||
# # 💡 Custom build step before installation
|
||||
# before install:
|
||||
# echo "Generating custom C header..."
|
||||
# exec "nim r tools/gen_header.nim"
|
||||
@@ -0,0 +1,42 @@
|
||||
## Can be shared safely between threads
|
||||
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 !
|
||||
if str.isNil():
|
||||
var ret = cast[cstring](allocShared(1)) # Allocate memory for the null terminator
|
||||
ret[0] = '\0' # Set the null terminator
|
||||
return ret
|
||||
|
||||
let ret = cast[cstring](allocShared(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))
|
||||
let s = cast[seq[char]](str)
|
||||
for i in 0 ..< str.len:
|
||||
ret[i] = s[i]
|
||||
ret[str.len] = '\0'
|
||||
return ret
|
||||
|
||||
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)
|
||||
return (cast[ptr UncheckedArray[T]](data), s.len)
|
||||
|
||||
proc deallocSharedSeq*[T](s: var SharedSeq[T]) =
|
||||
deallocShared(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])
|
||||
return ret
|
||||
@@ -0,0 +1,11 @@
|
||||
## 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.
|
||||
##
|
||||
## `singleThreaded` collapses the worker onto the calling thread: a request runs
|
||||
## inline to completion. Auto-selected for Emscripten/WASM; forceable anywhere
|
||||
## with `-d:ffiSingleThreaded`.
|
||||
const singleThreaded* = defined(ffiSingleThreaded) or defined(emscripten)
|
||||
@@ -0,0 +1,302 @@
|
||||
{.pragma: exported, exportc, cdecl, raises: [].}
|
||||
{.pragma: callback, cdecl, raises: [], gcsafe.}
|
||||
{.passc: "-fPIC".}
|
||||
|
||||
import std/[options, atomics, os, net, locks, json, 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
|
||||
|
||||
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
|
||||
userData*: pointer
|
||||
eventCallback*: pointer
|
||||
eventUserdata*: pointer
|
||||
running: Atomic[bool] # To control when the threads are running
|
||||
registeredRequests: ptr Table[cstring, FFIRequestProc]
|
||||
# Pointer to with the registered requests at compile time
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
## Sending the request
|
||||
let sentOk = ctx.reqChannel.trySend(ffiRequest)
|
||||
if not sentOk:
|
||||
return err("Couldn't send a request to the ffi thread")
|
||||
|
||||
let fireSyncRes = ctx.reqSignal.fireSync()
|
||||
if fireSyncRes.isErr():
|
||||
return err("failed fireSync: " & $fireSyncRes.error)
|
||||
|
||||
if fireSyncRes.get() == false:
|
||||
return err("Couldn't fireSync in time")
|
||||
|
||||
## 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")
|
||||
|
||||
## Notice that in case of "ok", the deallocShared(req) is performed by the FFI Thread in the
|
||||
## process proc.
|
||||
return ok()
|
||||
|
||||
type Foo = object
|
||||
registerReqFFI(WatchdogReq, foo: ptr Foo):
|
||||
proc(): Future[Result[string, string]] {.async.} =
|
||||
return ok("FFI thread is not blocked")
|
||||
|
||||
type JsonNotRespondingEvent = object
|
||||
eventType: string
|
||||
|
||||
proc init(T: type JsonNotRespondingEvent): T =
|
||||
return JsonNotRespondingEvent(eventType: "not_responding")
|
||||
|
||||
proc `$`(event: JsonNotRespondingEvent): string =
|
||||
$(%*event)
|
||||
|
||||
proc onNotResponding*(ctx: ptr FFIContext) =
|
||||
callEventCallback(ctx, "onNotResponding"):
|
||||
$JsonNotRespondingEvent.init()
|
||||
|
||||
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.
|
||||
|
||||
let watchdogRun = proc(ctx: ptr FFIContext) {.async.} =
|
||||
const WatchdogStartDelay = 10.seconds
|
||||
const WatchdogTimeinterval = 1.seconds
|
||||
const WatchdogTimeout = 20.seconds
|
||||
|
||||
# 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] =
|
||||
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")
|
||||
|
||||
joinThread(ctx.ffiThread)
|
||||
joinThread(ctx.watchdogThread)
|
||||
ctx.lock.deinitLock()
|
||||
?ctx.reqSignal.close()
|
||||
?ctx.reqReceivedSignal.close()
|
||||
freeShared(ctx)
|
||||
|
||||
return ok()
|
||||
|
||||
template checkParams*(ctx: ptr FFIContext, callback: FFICallBack, userData: pointer) =
|
||||
if not isNil(ctx):
|
||||
ctx[].userData = userData
|
||||
|
||||
if isNil(callback):
|
||||
return RET_MISSING_CALLBACK
|
||||
@@ -0,0 +1,64 @@
|
||||
## 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.
|
||||
|
||||
import std/[json, macros], results, tables
|
||||
import chronos
|
||||
import ./ffi_config
|
||||
when not singleThreaded:
|
||||
import chronos/threadsync # ThreadSignalPtr requires threads enabled
|
||||
import ./ffi_types, ./internal/ffi_macro, ./alloc
|
||||
|
||||
type FFIThreadRequest* = object
|
||||
callback: FFICallBack
|
||||
userData: pointer
|
||||
reqId*: cstring
|
||||
reqContent*: pointer
|
||||
|
||||
proc init*(
|
||||
T: typedesc[FFIThreadRequest],
|
||||
callback: FFICallBack,
|
||||
userData: pointer,
|
||||
reqId: cstring,
|
||||
reqContent: pointer,
|
||||
): ptr type T =
|
||||
var ret = createShared(FFIThreadRequest)
|
||||
ret[].callback = callback
|
||||
ret[].userData = userData
|
||||
ret[].reqId = reqId.alloc()
|
||||
ret[].reqContent = reqContent
|
||||
return ret
|
||||
|
||||
proc deleteRequest(request: ptr FFIThreadRequest) =
|
||||
deallocShared(request[].reqId)
|
||||
deallocShared(request)
|
||||
|
||||
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].
|
||||
|
||||
defer:
|
||||
deleteRequest(request)
|
||||
|
||||
if res.isErr():
|
||||
foreignThreadGc:
|
||||
let msg = "ffi error: handleRes fireSyncRes error: " & $res.error
|
||||
request[].callback(
|
||||
RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), request[].userData
|
||||
)
|
||||
return
|
||||
|
||||
foreignThreadGc:
|
||||
var msg: cstring = ""
|
||||
when T is string:
|
||||
msg = res.get().cstring()
|
||||
request[].callback(
|
||||
RET_OK, unsafeAddr msg[0], cast[csize_t](len(msg)), request[].userData
|
||||
)
|
||||
return
|
||||
|
||||
proc nilProcess*(reqId: cstring): Future[Result[string, string]] {.async.} =
|
||||
return err("This request type is not implemented: " & $reqId)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import std/tables
|
||||
import chronos
|
||||
|
||||
################################################################################
|
||||
### Exported types
|
||||
|
||||
type FFICallBack* = proc(
|
||||
callerRet: cint, msg: ptr cchar, len: csize_t, userData: pointer
|
||||
) {.cdecl, gcsafe, raises: [].}
|
||||
|
||||
const RET_OK*: cint = 0
|
||||
const RET_ERR*: cint = 1
|
||||
const RET_MISSING_CALLBACK*: cint = 2
|
||||
|
||||
### End of exported types
|
||||
################################################################################
|
||||
|
||||
################################################################################
|
||||
### FFI utils
|
||||
|
||||
type FFIRequestProc* =
|
||||
proc(request: pointer, reqHandler: pointer): Future[Result[string, string]] {.async.}
|
||||
|
||||
template foreignThreadGc*(body: untyped) =
|
||||
when declared(setupForeignThreadGc):
|
||||
setupForeignThreadGc()
|
||||
|
||||
body
|
||||
|
||||
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.
|
||||
var registeredRequests*: Table[cstring, FFIRequestProc]
|
||||
|
||||
### End of FFI utils
|
||||
################################################################################
|
||||
@@ -0,0 +1,84 @@
|
||||
import std/[macros, atomics], strformat, chronicles, chronos
|
||||
|
||||
macro declareLibrary*(libraryName: static[string]): untyped =
|
||||
var res = newStmtList()
|
||||
|
||||
## Generate {.pragma: exported, exportc, cdecl, raises: [].}
|
||||
res.add nnkPragma.newTree(
|
||||
nnkExprColonExpr.newTree(ident"pragma", ident"exported"),
|
||||
ident"exportc",
|
||||
ident"cdecl",
|
||||
nnkExprColonExpr.newTree(ident"raises", nnkBracket.newTree()),
|
||||
)
|
||||
|
||||
## Generate {.pragma: callback, cdecl, raises: [], gcsafe.}
|
||||
res.add nnkPragma.newTree(
|
||||
nnkExprColonExpr.newTree(ident"pragma", ident"callback"),
|
||||
ident"cdecl",
|
||||
nnkExprColonExpr.newTree(ident"raises", nnkBracket.newTree()),
|
||||
ident"gcsafe",
|
||||
)
|
||||
|
||||
## Generate {.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))
|
||||
)
|
||||
)
|
||||
|
||||
## proc lib{libraryName}NimMain() {.importc.}
|
||||
let libNimMainName = ident(fmt"lib{libraryName}NimMain")
|
||||
let importcPragma = nnkPragma.newTree(ident"importc")
|
||||
let procDef = newProc(
|
||||
name = libNimMainName,
|
||||
params = @[ident"void"],
|
||||
pragmas = importcPragma,
|
||||
body = newEmptyNode(),
|
||||
)
|
||||
res.add(procDef)
|
||||
|
||||
# Create: var initialized: Atomic[bool]
|
||||
let atomicType = nnkBracketExpr.newTree(ident("Atomic"), ident("bool"))
|
||||
let varStmt = nnkVarSection.newTree(
|
||||
nnkIdentDefs.newTree(ident("initialized"), atomicType, newEmptyNode())
|
||||
)
|
||||
res.add(varStmt)
|
||||
|
||||
## Android chronicles redirection
|
||||
let chroniclesBlock = quote:
|
||||
when defined(android) and compiles(defaultChroniclesStream.outputs[0].writer):
|
||||
defaultChroniclesStream.outputs[0].writer = proc(
|
||||
logLevel: LogLevel, msg: LogOutputStr
|
||||
) {.raises: [].} =
|
||||
echo logLevel, msg
|
||||
result.add(chroniclesBlock)
|
||||
|
||||
let procName = ident("initializeLibrary")
|
||||
let nimMainName = ident("lib" & libraryName & "NimMain")
|
||||
|
||||
let initializeLibraryProc = quote:
|
||||
proc `procName`*() {.exported.} =
|
||||
if not initialized.exchange(true):
|
||||
## Every Nim library needs to call `<yourprefix>NimMain` once exactly,
|
||||
## to initialize the Nim runtime.
|
||||
## Being `<yourprefix>` the value given in the optional
|
||||
## compilation flag --nimMainPrefix:yourprefix
|
||||
`nimMainName`()
|
||||
when declared(setupForeignThreadGc):
|
||||
setupForeignThreadGc()
|
||||
when declared(nimGC_setStackBottom):
|
||||
var locals {.volatile, noinit.}: pointer
|
||||
locals = addr(locals)
|
||||
nimGC_setStackBottom(locals)
|
||||
|
||||
res.add(initializeLibraryProc)
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,549 @@
|
||||
import std/[macros, tables]
|
||||
import chronos
|
||||
import ../ffi_types
|
||||
|
||||
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
|
||||
##
|
||||
|
||||
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] # parameters list
|
||||
result = @[]
|
||||
for p in params[1 .. ^1]: # skip return type
|
||||
result.add newIdentDefs(p[0], p[1])
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
proc buildRequestType(reqTypeName: NimNode, body: NimNode): NimNode =
|
||||
## Builds:
|
||||
## type <reqTypeName>* = object
|
||||
## <lambdaParam1Name>: <lambdaParam1Type>
|
||||
## ...
|
||||
## e.g.:
|
||||
## type CreateNodeRequest* = object
|
||||
## configJson: cstring
|
||||
## appCallbacks: AppCallbacks
|
||||
##
|
||||
|
||||
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] # formal params of the lambda
|
||||
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())
|
||||
|
||||
# Wrap fields in a rec list
|
||||
let recList = newTree(nnkRecList, fields)
|
||||
|
||||
# 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 =
|
||||
newNimNode(nnkTypeSection).add(newTree(nnkTypeDef, typeName, newEmptyNode(), objTy))
|
||||
|
||||
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.
|
||||
|
||||
var formalParams = newSeq[NimNode]()
|
||||
|
||||
var procNode: NimNode
|
||||
if body.kind == nnkStmtList and body.len == 1:
|
||||
procNode = body[0] # unwrap single statement
|
||||
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]
|
||||
)
|
||||
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
|
||||
let procParams = procNode[3]
|
||||
for p in procParams[1 .. ^1]:
|
||||
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)
|
||||
)
|
||||
|
||||
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":
|
||||
newBody.add(
|
||||
quote do:
|
||||
`reqObjIdent`[].`fieldNameIdent` = `fieldNameIdent`.alloc()
|
||||
)
|
||||
else:
|
||||
newBody.add(
|
||||
quote do:
|
||||
`reqObjIdent`[].`fieldNameIdent` = `fieldNameIdent`
|
||||
)
|
||||
|
||||
# FFIThreadRequest.init using fnv1aHash32
|
||||
newBody.add(
|
||||
quote do:
|
||||
let typeStr = $T
|
||||
var ret =
|
||||
FFIThreadRequest.init(callback, userData, typeStr.cstring, `reqObjIdent`)
|
||||
return ret
|
||||
)
|
||||
|
||||
# Build the proc node
|
||||
result = newProc(
|
||||
name = postfix(ident("ffiNewReq"), "*"),
|
||||
params = formalParams,
|
||||
body = newBody,
|
||||
pragmas = newEmptyNode(),
|
||||
)
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
proc buildFfiDeleteReqProc(reqTypeName: NimNode, fields: seq[NimNode]): NimNode =
|
||||
## Generates:
|
||||
## proc ffiDeleteReq(self: ptr <reqTypeName>) =
|
||||
## deallocShared(self[].<cstringField>)
|
||||
## deallocShared(self)
|
||||
|
||||
# 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 <reqTypeName>)
|
||||
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]) ...
|
||||
|
||||
if reqHandler.kind != nnkExprColonExpr:
|
||||
error(
|
||||
"Second argument must be a typed parameter, e.g., waku: ptr Waku. Found: " &
|
||||
$reqHandler.kind
|
||||
)
|
||||
|
||||
let rhs = reqHandler[1]
|
||||
if rhs.kind != nnkPtrTy:
|
||||
error("Second argument must be a pointer type, e.g., waku: ptr Waku")
|
||||
|
||||
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 definition. Found: " & $procNode.kind
|
||||
|
||||
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(typedescParam)
|
||||
formalParams.add(newIdentDefs(ident("request"), ident("pointer")))
|
||||
formalParams.add(newIdentDefs(reqHandler[0], rhs)) # e.g. waku: ptr Waku
|
||||
|
||||
# Inject cast/unpack/defer into the body
|
||||
let bodyNode =
|
||||
if procNode.body.kind == nnkStmtList:
|
||||
procNode.body
|
||||
else:
|
||||
newStmtList(procNode.body)
|
||||
|
||||
let newBody = newStmtList()
|
||||
let reqIdent = ident("req")
|
||||
|
||||
newBody.add quote do:
|
||||
let `reqIdent`: ptr `reqTypeName` = cast[ptr `reqTypeName`](request)
|
||||
defer:
|
||||
ffiDeleteReq(`reqIdent`)
|
||||
|
||||
# automatically unpack fields into locals
|
||||
for p in procParams[1 ..^ 1]:
|
||||
let fieldName = p[0] # Ident
|
||||
|
||||
newBody.add quote do:
|
||||
let `fieldName` = `reqIdent`[].`fieldName`
|
||||
|
||||
# Append user's lambda body
|
||||
newBody.add(bodyNode)
|
||||
|
||||
result = newProc(
|
||||
name = postfix(ident("processFFIRequest"), "*"),
|
||||
params = formalParams,
|
||||
body = newBody,
|
||||
procType = nnkProcDef,
|
||||
pragmas =
|
||||
if procNode.len >= 5:
|
||||
procNode[4]
|
||||
else:
|
||||
newEmptyNode(),
|
||||
)
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
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.
|
||||
|
||||
# Build: request[].reqContent
|
||||
let reqContent =
|
||||
newDotExpr(newTree(nnkDerefExpr, ident("request")), ident("reqContent"))
|
||||
|
||||
# Build Future[Result[string, string]] return type
|
||||
let returnType = nnkBracketExpr.newTree(
|
||||
ident("Future"),
|
||||
nnkBracketExpr.newTree(ident("Result"), ident("string"), 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
|
||||
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 callExpr = newCall(
|
||||
newDotExpr(reqTypeName, ident("processFFIRequest")), ident("request"), castedHandler
|
||||
)
|
||||
|
||||
var newBody = newStmtList()
|
||||
newBody.add(
|
||||
quote do:
|
||||
return await `callExpr`
|
||||
)
|
||||
|
||||
# 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")),
|
||||
],
|
||||
body = newBody,
|
||||
pragmas = nnkPragma.newTree(ident("async")),
|
||||
)
|
||||
|
||||
let reqTypeNameStr = $reqTypeName
|
||||
|
||||
let key = newLit($reqTypeName)
|
||||
# Generate: registeredRequests["CreateNodeRequest"] = <generated proc>
|
||||
result =
|
||||
newAssignment(newTree(nnkBracketExpr, ident("registeredRequests"), key), asyncProc)
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
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)
|
||||
##
|
||||
|
||||
var callArgs = @[reqType, callback, userData]
|
||||
for a in args:
|
||||
callArgs.add a
|
||||
|
||||
let newReqCall = newCall(ident("ffiNewReq"), callArgs)
|
||||
|
||||
let sendCall = newCall(
|
||||
newDotExpr(ident("ffi_context"), ident("sendRequestToFFIThread")), ctx, newReqCall
|
||||
)
|
||||
|
||||
result = quote:
|
||||
block:
|
||||
let res = `sendCall`
|
||||
if res.isErr():
|
||||
let msg = "error in sendRequestToFFIThread: " & res.error
|
||||
`callback`(RET_ERR, unsafeAddr msg[0], cast[csize_t](msg.len), `userData`)
|
||||
return RET_ERR
|
||||
return RET_OK
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
|
||||
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.
|
||||
##
|
||||
|
||||
let procName = prc[0]
|
||||
let formalParams = prc[3]
|
||||
let bodyNode = prc[^1]
|
||||
|
||||
if formalParams.len < 2:
|
||||
error("`.ffi.` procs require at least 1 parameter")
|
||||
|
||||
let firstParam = formalParams[1]
|
||||
let paramIdent = firstParam[0]
|
||||
let paramType = firstParam[1]
|
||||
|
||||
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]]
|
||||
|
||||
var userParams = newSeq[NimNode]()
|
||||
userParams.add(futReturnType)
|
||||
if formalParams.len > 3:
|
||||
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 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)
|
||||
|
||||
var anonymousProcNode = newProc(
|
||||
name = newEmptyNode(), # anonymous proc
|
||||
params = userParams,
|
||||
body = newStmtList(bodyNode),
|
||||
pragmas = newTree(nnkPragma, ident"async"),
|
||||
)
|
||||
|
||||
# registerReqFFI wrapper
|
||||
let registerReq = quote:
|
||||
registerReqFFI(`reqName`, `paramIdent`: `paramType`):
|
||||
`anonymousProcNode`
|
||||
|
||||
result = newStmtList(registerReq, ffiProc)
|
||||
|
||||
when defined(ffiDumpMacros):
|
||||
echo result.repr
|
||||
@@ -0,0 +1,106 @@
|
||||
## 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)
|
||||
import
|
||||
std/[typetraits, os, strutils, syncio],
|
||||
chronicles,
|
||||
chronicles/log_output,
|
||||
chronicles/topics_registry
|
||||
|
||||
export chronicles.LogLevel
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
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
|
||||
var
|
||||
res = newStringOfCap(v.len)
|
||||
i: int
|
||||
|
||||
while i < v.len:
|
||||
let c = v[i]
|
||||
if c == '\x1b':
|
||||
var
|
||||
x = i + 1
|
||||
found = false
|
||||
|
||||
while x < v.len: # look for [..m
|
||||
let c2 = v[x]
|
||||
if x == i + 1:
|
||||
if c2 != '[':
|
||||
break
|
||||
else:
|
||||
if c2 in {'0' .. '9'} + {';'}:
|
||||
discard # keep looking
|
||||
elif c2 == 'm':
|
||||
i = x + 1
|
||||
found = true
|
||||
break
|
||||
else:
|
||||
break
|
||||
inc x
|
||||
|
||||
if found: # skip adding c
|
||||
continue
|
||||
res.add c
|
||||
inc i
|
||||
|
||||
res
|
||||
|
||||
proc writeAndFlush(f: syncio.File, s: LogOutputStr) =
|
||||
try:
|
||||
f.write(s)
|
||||
f.flushFile()
|
||||
except CatchableError:
|
||||
logLoggingFailure(cstring(s), getCurrentException())
|
||||
|
||||
## Setup
|
||||
|
||||
proc setupLogLevel(level: LogLevel) =
|
||||
# TODO: Support per topic level configuratio
|
||||
topics_registry.setLogLevel(level)
|
||||
|
||||
proc setupLogFormat(format: LogFormat, color = true) =
|
||||
proc noOutputWriter(logLevel: LogLevel, msg: LogOutputStr) =
|
||||
discard
|
||||
|
||||
proc stdoutOutputWriter(logLevel: LogLevel, msg: LogOutputStr) =
|
||||
writeAndFlush(syncio.stdout, msg)
|
||||
|
||||
proc stdoutNoColorOutputWriter(logLevel: LogLevel, msg: LogOutputStr) =
|
||||
writeAndFlush(syncio.stdout, stripAnsi(msg))
|
||||
|
||||
when defaultChroniclesStream.outputs.type.arity == 2:
|
||||
case format
|
||||
of LogFormat.Text:
|
||||
defaultChroniclesStream.outputs[0].writer =
|
||||
if color: stdoutOutputWriter else: stdoutNoColorOutputWriter
|
||||
defaultChroniclesStream.outputs[1].writer = noOutputWriter
|
||||
of LogFormat.Json:
|
||||
defaultChroniclesStream.outputs[0].writer = noOutputWriter
|
||||
defaultChroniclesStream.outputs[1].writer = stdoutOutputWriter
|
||||
else:
|
||||
{.
|
||||
warning:
|
||||
"the present module should be compiled with '-d:chronicles_default_output_device=dynamic' " &
|
||||
"and '-d:chronicles_sinks=\"textlines,json\"' options"
|
||||
.}
|
||||
|
||||
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:
|
||||
true
|
||||
|
||||
setupLogLevel(level)
|
||||
setupLogFormat(format, color)
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"version": 1,
|
||||
"metaData": {
|
||||
"url": "https://github.com/logos-messaging/nim-ffi",
|
||||
"downloadMethod": "git",
|
||||
"vcsRevision": "06111de155253b34e47ed2aaed1d61d08d62cc1b",
|
||||
"files": [
|
||||
"/ffi.nim",
|
||||
"/ffi/ffi_types.nim",
|
||||
"/ffi.nimble",
|
||||
"/ffi/ffi_thread_request.nim",
|
||||
"/ffi/alloc.nim",
|
||||
"/ffi/logging.nim",
|
||||
"/ffi/internal/ffi_library.nim",
|
||||
"/ffi/internal/ffi_macro.nim",
|
||||
"/ffi/ffi_context.nim"
|
||||
],
|
||||
"binaries": [],
|
||||
"specialVersions": [
|
||||
"0.1.3"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,909 @@
|
||||
/*
|
||||
* Copyright (c) 2000 Apple Computer, Inc. All rights reserved.
|
||||
*
|
||||
* @APPLE_OSREFERENCE_LICENSE_HEADER_START@
|
||||
*
|
||||
* This file contains Original Code and/or Modifications of Original Code
|
||||
* as defined in and that are subject to the Apple Public Source License
|
||||
* Version 2.0 (the 'License'). You may not use this file except in
|
||||
* compliance with the License. The rights granted to you under the License
|
||||
* may not be used to create, or enable the creation or redistribution of,
|
||||
* unlawful or unlicensed copies of an Apple operating system, or to
|
||||
* circumvent, violate, or enable the circumvention or violation of, any
|
||||
* terms of an Apple operating system software license agreement.
|
||||
*
|
||||
* Please obtain a copy of the License at
|
||||
* http://www.opensource.apple.com/apsl/ and read it before using this file.
|
||||
*
|
||||
* The Original Code and all software distributed under the License are
|
||||
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
|
||||
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
|
||||
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
|
||||
* Please see the License for the specific language governing rights and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @APPLE_OSREFERENCE_LICENSE_HEADER_END@
|
||||
*/
|
||||
/*-
|
||||
* Copyright (c) 1991, 1993
|
||||
* The Regents of the University of California. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 4. Neither the name of the University nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*
|
||||
* @(#)queue.h 8.5 (Berkeley) 8/20/94
|
||||
*/
|
||||
|
||||
#ifndef _SYS_QUEUE_H_
|
||||
#define _SYS_QUEUE_H_
|
||||
|
||||
#ifndef __improbable
|
||||
#define __improbable(x) (x) /* noop in userspace */
|
||||
#endif /* __improbable */
|
||||
|
||||
/*
|
||||
* This file defines five types of data structures: singly-linked lists,
|
||||
* singly-linked tail queues, lists, tail queues, and circular queues.
|
||||
*
|
||||
* A singly-linked list is headed by a single forward pointer. The elements
|
||||
* are singly linked for minimum space and pointer manipulation overhead at
|
||||
* the expense of O(n) removal for arbitrary elements. New elements can be
|
||||
* added to the list after an existing element or at the head of the list.
|
||||
* Elements being removed from the head of the list should use the explicit
|
||||
* macro for this purpose for optimum efficiency. A singly-linked list may
|
||||
* only be traversed in the forward direction. Singly-linked lists are ideal
|
||||
* for applications with large datasets and few or no removals or for
|
||||
* implementing a LIFO queue.
|
||||
*
|
||||
* A singly-linked tail queue is headed by a pair of pointers, one to the
|
||||
* head of the list and the other to the tail of the list. The elements are
|
||||
* singly linked for minimum space and pointer manipulation overhead at the
|
||||
* expense of O(n) removal for arbitrary elements. New elements can be added
|
||||
* to the list after an existing element, at the head of the list, or at the
|
||||
* end of the list. Elements being removed from the head of the tail queue
|
||||
* should use the explicit macro for this purpose for optimum efficiency.
|
||||
* A singly-linked tail queue may only be traversed in the forward direction.
|
||||
* Singly-linked tail queues are ideal for applications with large datasets
|
||||
* and few or no removals or for implementing a FIFO queue.
|
||||
*
|
||||
* A list is headed by a single forward pointer (or an array of forward
|
||||
* pointers for a hash table header). The elements are doubly linked
|
||||
* so that an arbitrary element can be removed without a need to
|
||||
* traverse the list. New elements can be added to the list before
|
||||
* or after an existing element or at the head of the list. A list
|
||||
* may only be traversed in the forward direction.
|
||||
*
|
||||
* A tail queue is headed by a pair of pointers, one to the head of the
|
||||
* list and the other to the tail of the list. The elements are doubly
|
||||
* linked so that an arbitrary element can be removed without a need to
|
||||
* traverse the list. New elements can be added to the list before or
|
||||
* after an existing element, at the head of the list, or at the end of
|
||||
* the list. A tail queue may be traversed in either direction.
|
||||
*
|
||||
* A circle queue is headed by a pair of pointers, one to the head of the
|
||||
* list and the other to the tail of the list. The elements are doubly
|
||||
* linked so that an arbitrary element can be removed without a need to
|
||||
* traverse the list. New elements can be added to the list before or after
|
||||
* an existing element, at the head of the list, or at the end of the list.
|
||||
* A circle queue may be traversed in either direction, but has a more
|
||||
* complex end of list detection.
|
||||
* Note that circle queues are deprecated, because, as the removal log
|
||||
* in FreeBSD states, "CIRCLEQs are a disgrace to everything Knuth taught
|
||||
* us in Volume 1 Chapter 2. [...] Use TAILQ instead, it provides the same
|
||||
* functionality." Code using them will continue to compile, but they
|
||||
* are no longer documented on the man page.
|
||||
*
|
||||
* For details on the use of these macros, see the queue(3) manual page.
|
||||
*
|
||||
*
|
||||
* SLIST LIST STAILQ TAILQ CIRCLEQ
|
||||
* _HEAD + + + + +
|
||||
* _HEAD_INITIALIZER + + + + -
|
||||
* _ENTRY + + + + +
|
||||
* _INIT + + + + +
|
||||
* _EMPTY + + + + +
|
||||
* _FIRST + + + + +
|
||||
* _NEXT + + + + +
|
||||
* _PREV - - - + +
|
||||
* _LAST - - + + +
|
||||
* _FOREACH + + + + +
|
||||
* _FOREACH_SAFE + + + + -
|
||||
* _FOREACH_REVERSE - - - + -
|
||||
* _FOREACH_REVERSE_SAFE - - - + -
|
||||
* _INSERT_HEAD + + + + +
|
||||
* _INSERT_BEFORE - + - + +
|
||||
* _INSERT_AFTER + + + + +
|
||||
* _INSERT_TAIL - - + + +
|
||||
* _CONCAT - - + + -
|
||||
* _REMOVE_AFTER + - + - -
|
||||
* _REMOVE_HEAD + - + - -
|
||||
* _REMOVE_HEAD_UNTIL - - + - -
|
||||
* _REMOVE + + + + +
|
||||
* _SWAP - + + + -
|
||||
*
|
||||
*/
|
||||
#ifdef QUEUE_MACRO_DEBUG
|
||||
/* Store the last 2 places the queue element or head was altered */
|
||||
struct qm_trace {
|
||||
char * lastfile;
|
||||
int lastline;
|
||||
char * prevfile;
|
||||
int prevline;
|
||||
};
|
||||
|
||||
#define TRACEBUF struct qm_trace trace;
|
||||
#define TRASHIT(x) do {(x) = (void *)-1;} while (0)
|
||||
|
||||
#define QMD_TRACE_HEAD(head) do { \
|
||||
(head)->trace.prevline = (head)->trace.lastline; \
|
||||
(head)->trace.prevfile = (head)->trace.lastfile; \
|
||||
(head)->trace.lastline = __LINE__; \
|
||||
(head)->trace.lastfile = __FILE__; \
|
||||
} while (0)
|
||||
|
||||
#define QMD_TRACE_ELEM(elem) do { \
|
||||
(elem)->trace.prevline = (elem)->trace.lastline; \
|
||||
(elem)->trace.prevfile = (elem)->trace.lastfile; \
|
||||
(elem)->trace.lastline = __LINE__; \
|
||||
(elem)->trace.lastfile = __FILE__; \
|
||||
} while (0)
|
||||
|
||||
#else
|
||||
#define QMD_TRACE_ELEM(elem)
|
||||
#define QMD_TRACE_HEAD(head)
|
||||
#define TRACEBUF
|
||||
#define TRASHIT(x)
|
||||
#endif /* QUEUE_MACRO_DEBUG */
|
||||
|
||||
/*
|
||||
* Horrible macros to enable use of code that was meant to be C-specific
|
||||
* (and which push struct onto type) in C++; without these, C++ code
|
||||
* that uses these macros in the context of a class will blow up
|
||||
* due to "struct" being preprended to "type" by the macros, causing
|
||||
* inconsistent use of tags.
|
||||
*
|
||||
* This approach is necessary because these are macros; we have to use
|
||||
* these on a per-macro basis (because the queues are implemented as
|
||||
* macros, disabling this warning in the scope of the header file is
|
||||
* insufficient), whuch means we can't use #pragma, and have to use
|
||||
* _Pragma. We only need to use these for the queue macros that
|
||||
* prepend "struct" to "type" and will cause C++ to blow up.
|
||||
*/
|
||||
#if defined(__clang__) && defined(__cplusplus)
|
||||
#define __MISMATCH_TAGS_PUSH \
|
||||
_Pragma("clang diagnostic push") \
|
||||
_Pragma("clang diagnostic ignored \"-Wmismatched-tags\"")
|
||||
#define __MISMATCH_TAGS_POP \
|
||||
_Pragma("clang diagnostic pop")
|
||||
#else
|
||||
#define __MISMATCH_TAGS_PUSH
|
||||
#define __MISMATCH_TAGS_POP
|
||||
#endif
|
||||
|
||||
/*!
|
||||
* Ensures that these macros can safely be used in structs when compiling with
|
||||
* clang. The macros do not allow for nullability attributes to be specified due
|
||||
* to how they are expanded. For example:
|
||||
*
|
||||
* SLIST_HEAD(, foo _Nullable) bar;
|
||||
*
|
||||
* expands to
|
||||
*
|
||||
* struct {
|
||||
* struct foo _Nullable *slh_first;
|
||||
* }
|
||||
*
|
||||
* which is not valid because the nullability specifier has to apply to the
|
||||
* pointer. So just ignore nullability completeness in all the places where this
|
||||
* is an issue.
|
||||
*/
|
||||
#if defined(__clang__)
|
||||
#define __NULLABILITY_COMPLETENESS_PUSH \
|
||||
_Pragma("clang diagnostic push") \
|
||||
_Pragma("clang diagnostic ignored \"-Wnullability-completeness\"")
|
||||
#define __NULLABILITY_COMPLETENESS_POP \
|
||||
_Pragma("clang diagnostic pop")
|
||||
#else
|
||||
#define __NULLABILITY_COMPLETENESS_PUSH
|
||||
#define __NULLABILITY_COMPLETENESS_POP
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Singly-linked List declarations.
|
||||
*/
|
||||
#define SLIST_HEAD(name, type) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
struct name { \
|
||||
struct type *slh_first; /* first element */ \
|
||||
} \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
#define SLIST_HEAD_INITIALIZER(head) \
|
||||
{ NULL }
|
||||
|
||||
#define SLIST_ENTRY(type) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
struct { \
|
||||
struct type *sle_next; /* next element */ \
|
||||
} \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
/*
|
||||
* Singly-linked List functions.
|
||||
*/
|
||||
#define SLIST_EMPTY(head) ((head)->slh_first == NULL)
|
||||
|
||||
#define SLIST_FIRST(head) ((head)->slh_first)
|
||||
|
||||
#define SLIST_FOREACH(var, head, field) \
|
||||
for ((var) = SLIST_FIRST((head)); \
|
||||
(var); \
|
||||
(var) = SLIST_NEXT((var), field))
|
||||
|
||||
#define SLIST_FOREACH_SAFE(var, head, field, tvar) \
|
||||
for ((var) = SLIST_FIRST((head)); \
|
||||
(var) && ((tvar) = SLIST_NEXT((var), field), 1); \
|
||||
(var) = (tvar))
|
||||
|
||||
#define SLIST_FOREACH_PREVPTR(var, varp, head, field) \
|
||||
for ((varp) = &SLIST_FIRST((head)); \
|
||||
((var) = *(varp)) != NULL; \
|
||||
(varp) = &SLIST_NEXT((var), field))
|
||||
|
||||
#define SLIST_INIT(head) do { \
|
||||
SLIST_FIRST((head)) = NULL; \
|
||||
} while (0)
|
||||
|
||||
#define SLIST_INSERT_AFTER(slistelm, elm, field) do { \
|
||||
SLIST_NEXT((elm), field) = SLIST_NEXT((slistelm), field); \
|
||||
SLIST_NEXT((slistelm), field) = (elm); \
|
||||
} while (0)
|
||||
|
||||
#define SLIST_INSERT_HEAD(head, elm, field) do { \
|
||||
SLIST_NEXT((elm), field) = SLIST_FIRST((head)); \
|
||||
SLIST_FIRST((head)) = (elm); \
|
||||
} while (0)
|
||||
|
||||
#define SLIST_NEXT(elm, field) ((elm)->field.sle_next)
|
||||
|
||||
#define SLIST_REMOVE(head, elm, type, field) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
do { \
|
||||
if (SLIST_FIRST((head)) == (elm)) { \
|
||||
SLIST_REMOVE_HEAD((head), field); \
|
||||
} \
|
||||
else { \
|
||||
struct type *curelm = SLIST_FIRST((head)); \
|
||||
while (SLIST_NEXT(curelm, field) != (elm)) \
|
||||
curelm = SLIST_NEXT(curelm, field); \
|
||||
SLIST_REMOVE_AFTER(curelm, field); \
|
||||
} \
|
||||
TRASHIT((elm)->field.sle_next); \
|
||||
} while (0) \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
#define SLIST_REMOVE_AFTER(elm, field) do { \
|
||||
SLIST_NEXT(elm, field) = \
|
||||
SLIST_NEXT(SLIST_NEXT(elm, field), field); \
|
||||
} while (0)
|
||||
|
||||
#define SLIST_REMOVE_HEAD(head, field) do { \
|
||||
SLIST_FIRST((head)) = SLIST_NEXT(SLIST_FIRST((head)), field); \
|
||||
} while (0)
|
||||
|
||||
/*
|
||||
* Singly-linked Tail queue declarations.
|
||||
*/
|
||||
#define STAILQ_HEAD(name, type) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
struct name { \
|
||||
struct type *stqh_first;/* first element */ \
|
||||
struct type **stqh_last;/* addr of last next element */ \
|
||||
} \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
#define STAILQ_HEAD_INITIALIZER(head) \
|
||||
{ NULL, &(head).stqh_first }
|
||||
|
||||
#define STAILQ_ENTRY(type) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
struct { \
|
||||
struct type *stqe_next; /* next element */ \
|
||||
} \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
/*
|
||||
* Singly-linked Tail queue functions.
|
||||
*/
|
||||
#define STAILQ_CONCAT(head1, head2) do { \
|
||||
if (!STAILQ_EMPTY((head2))) { \
|
||||
*(head1)->stqh_last = (head2)->stqh_first; \
|
||||
(head1)->stqh_last = (head2)->stqh_last; \
|
||||
STAILQ_INIT((head2)); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define STAILQ_EMPTY(head) ((head)->stqh_first == NULL)
|
||||
|
||||
#define STAILQ_FIRST(head) ((head)->stqh_first)
|
||||
|
||||
#define STAILQ_FOREACH(var, head, field) \
|
||||
for((var) = STAILQ_FIRST((head)); \
|
||||
(var); \
|
||||
(var) = STAILQ_NEXT((var), field))
|
||||
|
||||
|
||||
#define STAILQ_FOREACH_SAFE(var, head, field, tvar) \
|
||||
for ((var) = STAILQ_FIRST((head)); \
|
||||
(var) && ((tvar) = STAILQ_NEXT((var), field), 1); \
|
||||
(var) = (tvar))
|
||||
|
||||
#define STAILQ_INIT(head) do { \
|
||||
STAILQ_FIRST((head)) = NULL; \
|
||||
(head)->stqh_last = &STAILQ_FIRST((head)); \
|
||||
} while (0)
|
||||
|
||||
#define STAILQ_INSERT_AFTER(head, tqelm, elm, field) do { \
|
||||
if ((STAILQ_NEXT((elm), field) = STAILQ_NEXT((tqelm), field)) == NULL)\
|
||||
(head)->stqh_last = &STAILQ_NEXT((elm), field); \
|
||||
STAILQ_NEXT((tqelm), field) = (elm); \
|
||||
} while (0)
|
||||
|
||||
#define STAILQ_INSERT_HEAD(head, elm, field) do { \
|
||||
if ((STAILQ_NEXT((elm), field) = STAILQ_FIRST((head))) == NULL) \
|
||||
(head)->stqh_last = &STAILQ_NEXT((elm), field); \
|
||||
STAILQ_FIRST((head)) = (elm); \
|
||||
} while (0)
|
||||
|
||||
#define STAILQ_INSERT_TAIL(head, elm, field) do { \
|
||||
STAILQ_NEXT((elm), field) = NULL; \
|
||||
*(head)->stqh_last = (elm); \
|
||||
(head)->stqh_last = &STAILQ_NEXT((elm), field); \
|
||||
} while (0)
|
||||
|
||||
#define STAILQ_LAST(head, type, field) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
(STAILQ_EMPTY((head)) ? \
|
||||
NULL : \
|
||||
((struct type *)(void *) \
|
||||
((char *)((head)->stqh_last) - __offsetof(struct type, field))))\
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
#define STAILQ_NEXT(elm, field) ((elm)->field.stqe_next)
|
||||
|
||||
#define STAILQ_REMOVE(head, elm, type, field) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
do { \
|
||||
if (STAILQ_FIRST((head)) == (elm)) { \
|
||||
STAILQ_REMOVE_HEAD((head), field); \
|
||||
} \
|
||||
else { \
|
||||
struct type *curelm = STAILQ_FIRST((head)); \
|
||||
while (STAILQ_NEXT(curelm, field) != (elm)) \
|
||||
curelm = STAILQ_NEXT(curelm, field); \
|
||||
STAILQ_REMOVE_AFTER(head, curelm, field); \
|
||||
} \
|
||||
TRASHIT((elm)->field.stqe_next); \
|
||||
} while (0) \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
#define STAILQ_REMOVE_HEAD(head, field) do { \
|
||||
if ((STAILQ_FIRST((head)) = \
|
||||
STAILQ_NEXT(STAILQ_FIRST((head)), field)) == NULL) \
|
||||
(head)->stqh_last = &STAILQ_FIRST((head)); \
|
||||
} while (0)
|
||||
|
||||
#define STAILQ_REMOVE_HEAD_UNTIL(head, elm, field) do { \
|
||||
if ((STAILQ_FIRST((head)) = STAILQ_NEXT((elm), field)) == NULL) \
|
||||
(head)->stqh_last = &STAILQ_FIRST((head)); \
|
||||
} while (0)
|
||||
|
||||
#define STAILQ_REMOVE_AFTER(head, elm, field) do { \
|
||||
if ((STAILQ_NEXT(elm, field) = \
|
||||
STAILQ_NEXT(STAILQ_NEXT(elm, field), field)) == NULL) \
|
||||
(head)->stqh_last = &STAILQ_NEXT((elm), field); \
|
||||
} while (0)
|
||||
|
||||
#define STAILQ_SWAP(head1, head2, type) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
do { \
|
||||
struct type *swap_first = STAILQ_FIRST(head1); \
|
||||
struct type **swap_last = (head1)->stqh_last; \
|
||||
STAILQ_FIRST(head1) = STAILQ_FIRST(head2); \
|
||||
(head1)->stqh_last = (head2)->stqh_last; \
|
||||
STAILQ_FIRST(head2) = swap_first; \
|
||||
(head2)->stqh_last = swap_last; \
|
||||
if (STAILQ_EMPTY(head1)) \
|
||||
(head1)->stqh_last = &STAILQ_FIRST(head1); \
|
||||
if (STAILQ_EMPTY(head2)) \
|
||||
(head2)->stqh_last = &STAILQ_FIRST(head2); \
|
||||
} while (0) \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
|
||||
/*
|
||||
* List declarations.
|
||||
*/
|
||||
#define LIST_HEAD(name, type) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
struct name { \
|
||||
struct type *lh_first; /* first element */ \
|
||||
} \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
#define LIST_HEAD_INITIALIZER(head) \
|
||||
{ NULL }
|
||||
|
||||
#define LIST_ENTRY(type) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
struct { \
|
||||
struct type *le_next; /* next element */ \
|
||||
struct type **le_prev; /* address of previous next element */ \
|
||||
} \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
/*
|
||||
* List functions.
|
||||
*/
|
||||
|
||||
#define LIST_CHECK_HEAD(head, field)
|
||||
#define LIST_CHECK_NEXT(elm, field)
|
||||
#define LIST_CHECK_PREV(elm, field)
|
||||
|
||||
#define LIST_EMPTY(head) ((head)->lh_first == NULL)
|
||||
|
||||
#define LIST_FIRST(head) ((head)->lh_first)
|
||||
|
||||
#define LIST_FOREACH(var, head, field) \
|
||||
for ((var) = LIST_FIRST((head)); \
|
||||
(var); \
|
||||
(var) = LIST_NEXT((var), field))
|
||||
|
||||
#define LIST_FOREACH_SAFE(var, head, field, tvar) \
|
||||
for ((var) = LIST_FIRST((head)); \
|
||||
(var) && ((tvar) = LIST_NEXT((var), field), 1); \
|
||||
(var) = (tvar))
|
||||
|
||||
#define LIST_INIT(head) do { \
|
||||
LIST_FIRST((head)) = NULL; \
|
||||
} while (0)
|
||||
|
||||
#define LIST_INSERT_AFTER(listelm, elm, field) do { \
|
||||
LIST_CHECK_NEXT(listelm, field); \
|
||||
if ((LIST_NEXT((elm), field) = LIST_NEXT((listelm), field)) != NULL)\
|
||||
LIST_NEXT((listelm), field)->field.le_prev = \
|
||||
&LIST_NEXT((elm), field); \
|
||||
LIST_NEXT((listelm), field) = (elm); \
|
||||
(elm)->field.le_prev = &LIST_NEXT((listelm), field); \
|
||||
} while (0)
|
||||
|
||||
#define LIST_INSERT_BEFORE(listelm, elm, field) do { \
|
||||
LIST_CHECK_PREV(listelm, field); \
|
||||
(elm)->field.le_prev = (listelm)->field.le_prev; \
|
||||
LIST_NEXT((elm), field) = (listelm); \
|
||||
*(listelm)->field.le_prev = (elm); \
|
||||
(listelm)->field.le_prev = &LIST_NEXT((elm), field); \
|
||||
} while (0)
|
||||
|
||||
#define LIST_INSERT_HEAD(head, elm, field) do { \
|
||||
LIST_CHECK_HEAD((head), field); \
|
||||
if ((LIST_NEXT((elm), field) = LIST_FIRST((head))) != NULL) \
|
||||
LIST_FIRST((head))->field.le_prev = &LIST_NEXT((elm), field);\
|
||||
LIST_FIRST((head)) = (elm); \
|
||||
(elm)->field.le_prev = &LIST_FIRST((head)); \
|
||||
} while (0)
|
||||
|
||||
#define LIST_NEXT(elm, field) ((elm)->field.le_next)
|
||||
|
||||
#define LIST_REMOVE(elm, field) do { \
|
||||
LIST_CHECK_NEXT(elm, field); \
|
||||
LIST_CHECK_PREV(elm, field); \
|
||||
if (LIST_NEXT((elm), field) != NULL) \
|
||||
LIST_NEXT((elm), field)->field.le_prev = \
|
||||
(elm)->field.le_prev; \
|
||||
*(elm)->field.le_prev = LIST_NEXT((elm), field); \
|
||||
TRASHIT((elm)->field.le_next); \
|
||||
TRASHIT((elm)->field.le_prev); \
|
||||
} while (0)
|
||||
|
||||
#define LIST_SWAP(head1, head2, type, field) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
do { \
|
||||
struct type *swap_tmp = LIST_FIRST((head1)); \
|
||||
LIST_FIRST((head1)) = LIST_FIRST((head2)); \
|
||||
LIST_FIRST((head2)) = swap_tmp; \
|
||||
if ((swap_tmp = LIST_FIRST((head1))) != NULL) \
|
||||
swap_tmp->field.le_prev = &LIST_FIRST((head1)); \
|
||||
if ((swap_tmp = LIST_FIRST((head2))) != NULL) \
|
||||
swap_tmp->field.le_prev = &LIST_FIRST((head2)); \
|
||||
} while (0) \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
/*
|
||||
* Tail queue declarations.
|
||||
*/
|
||||
#define TAILQ_HEAD(name, type) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
struct name { \
|
||||
struct type *tqh_first; /* first element */ \
|
||||
struct type **tqh_last; /* addr of last next element */ \
|
||||
TRACEBUF \
|
||||
} \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
#define TAILQ_HEAD_INITIALIZER(head) \
|
||||
{ NULL, &(head).tqh_first }
|
||||
|
||||
#define TAILQ_ENTRY(type) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
struct { \
|
||||
struct type *tqe_next; /* next element */ \
|
||||
struct type **tqe_prev; /* address of previous next element */ \
|
||||
TRACEBUF \
|
||||
} \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
/*
|
||||
* Tail queue functions.
|
||||
*/
|
||||
#define TAILQ_CHECK_HEAD(head, field)
|
||||
#define TAILQ_CHECK_NEXT(elm, field)
|
||||
#define TAILQ_CHECK_PREV(elm, field)
|
||||
|
||||
#define TAILQ_CONCAT(head1, head2, field) do { \
|
||||
if (!TAILQ_EMPTY(head2)) { \
|
||||
*(head1)->tqh_last = (head2)->tqh_first; \
|
||||
(head2)->tqh_first->field.tqe_prev = (head1)->tqh_last; \
|
||||
(head1)->tqh_last = (head2)->tqh_last; \
|
||||
TAILQ_INIT((head2)); \
|
||||
QMD_TRACE_HEAD(head1); \
|
||||
QMD_TRACE_HEAD(head2); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define TAILQ_EMPTY(head) ((head)->tqh_first == NULL)
|
||||
|
||||
#define TAILQ_FIRST(head) ((head)->tqh_first)
|
||||
|
||||
#define TAILQ_FOREACH(var, head, field) \
|
||||
for ((var) = TAILQ_FIRST((head)); \
|
||||
(var); \
|
||||
(var) = TAILQ_NEXT((var), field))
|
||||
|
||||
#define TAILQ_FOREACH_SAFE(var, head, field, tvar) \
|
||||
for ((var) = TAILQ_FIRST((head)); \
|
||||
(var) && ((tvar) = TAILQ_NEXT((var), field), 1); \
|
||||
(var) = (tvar))
|
||||
|
||||
#define TAILQ_FOREACH_REVERSE(var, head, headname, field) \
|
||||
for ((var) = TAILQ_LAST((head), headname); \
|
||||
(var); \
|
||||
(var) = TAILQ_PREV((var), headname, field))
|
||||
|
||||
#define TAILQ_FOREACH_REVERSE_SAFE(var, head, headname, field, tvar) \
|
||||
for ((var) = TAILQ_LAST((head), headname); \
|
||||
(var) && ((tvar) = TAILQ_PREV((var), headname, field), 1); \
|
||||
(var) = (tvar))
|
||||
|
||||
|
||||
#define TAILQ_INIT(head) do { \
|
||||
TAILQ_FIRST((head)) = NULL; \
|
||||
(head)->tqh_last = &TAILQ_FIRST((head)); \
|
||||
QMD_TRACE_HEAD(head); \
|
||||
} while (0)
|
||||
|
||||
|
||||
#define TAILQ_INSERT_AFTER(head, listelm, elm, field) do { \
|
||||
TAILQ_CHECK_NEXT(listelm, field); \
|
||||
if ((TAILQ_NEXT((elm), field) = TAILQ_NEXT((listelm), field)) != NULL)\
|
||||
TAILQ_NEXT((elm), field)->field.tqe_prev = \
|
||||
&TAILQ_NEXT((elm), field); \
|
||||
else { \
|
||||
(head)->tqh_last = &TAILQ_NEXT((elm), field); \
|
||||
QMD_TRACE_HEAD(head); \
|
||||
} \
|
||||
TAILQ_NEXT((listelm), field) = (elm); \
|
||||
(elm)->field.tqe_prev = &TAILQ_NEXT((listelm), field); \
|
||||
QMD_TRACE_ELEM(&(elm)->field); \
|
||||
QMD_TRACE_ELEM(&listelm->field); \
|
||||
} while (0)
|
||||
|
||||
#define TAILQ_INSERT_BEFORE(listelm, elm, field) do { \
|
||||
TAILQ_CHECK_PREV(listelm, field); \
|
||||
(elm)->field.tqe_prev = (listelm)->field.tqe_prev; \
|
||||
TAILQ_NEXT((elm), field) = (listelm); \
|
||||
*(listelm)->field.tqe_prev = (elm); \
|
||||
(listelm)->field.tqe_prev = &TAILQ_NEXT((elm), field); \
|
||||
QMD_TRACE_ELEM(&(elm)->field); \
|
||||
QMD_TRACE_ELEM(&listelm->field); \
|
||||
} while (0)
|
||||
|
||||
#define TAILQ_INSERT_HEAD(head, elm, field) do { \
|
||||
TAILQ_CHECK_HEAD(head, field); \
|
||||
if ((TAILQ_NEXT((elm), field) = TAILQ_FIRST((head))) != NULL) \
|
||||
TAILQ_FIRST((head))->field.tqe_prev = \
|
||||
&TAILQ_NEXT((elm), field); \
|
||||
else \
|
||||
(head)->tqh_last = &TAILQ_NEXT((elm), field); \
|
||||
TAILQ_FIRST((head)) = (elm); \
|
||||
(elm)->field.tqe_prev = &TAILQ_FIRST((head)); \
|
||||
QMD_TRACE_HEAD(head); \
|
||||
QMD_TRACE_ELEM(&(elm)->field); \
|
||||
} while (0)
|
||||
|
||||
#define TAILQ_INSERT_TAIL(head, elm, field) do { \
|
||||
TAILQ_NEXT((elm), field) = NULL; \
|
||||
(elm)->field.tqe_prev = (head)->tqh_last; \
|
||||
*(head)->tqh_last = (elm); \
|
||||
(head)->tqh_last = &TAILQ_NEXT((elm), field); \
|
||||
QMD_TRACE_HEAD(head); \
|
||||
QMD_TRACE_ELEM(&(elm)->field); \
|
||||
} while (0)
|
||||
|
||||
#define TAILQ_LAST(head, headname) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
(*(((struct headname *)((head)->tqh_last))->tqh_last)) \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
#define TAILQ_NEXT(elm, field) ((elm)->field.tqe_next)
|
||||
|
||||
#define TAILQ_PREV(elm, headname, field) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
(*(((struct headname *)((elm)->field.tqe_prev))->tqh_last)) \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
#define TAILQ_REMOVE(head, elm, field) do { \
|
||||
TAILQ_CHECK_NEXT(elm, field); \
|
||||
TAILQ_CHECK_PREV(elm, field); \
|
||||
if ((TAILQ_NEXT((elm), field)) != NULL) \
|
||||
TAILQ_NEXT((elm), field)->field.tqe_prev = \
|
||||
(elm)->field.tqe_prev; \
|
||||
else { \
|
||||
(head)->tqh_last = (elm)->field.tqe_prev; \
|
||||
QMD_TRACE_HEAD(head); \
|
||||
} \
|
||||
*(elm)->field.tqe_prev = TAILQ_NEXT((elm), field); \
|
||||
TRASHIT((elm)->field.tqe_next); \
|
||||
TRASHIT((elm)->field.tqe_prev); \
|
||||
QMD_TRACE_ELEM(&(elm)->field); \
|
||||
} while (0)
|
||||
|
||||
/*
|
||||
* Why did they switch to spaces for this one macro?
|
||||
*/
|
||||
#define TAILQ_SWAP(head1, head2, type, field) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
do { \
|
||||
struct type *swap_first = (head1)->tqh_first; \
|
||||
struct type **swap_last = (head1)->tqh_last; \
|
||||
(head1)->tqh_first = (head2)->tqh_first; \
|
||||
(head1)->tqh_last = (head2)->tqh_last; \
|
||||
(head2)->tqh_first = swap_first; \
|
||||
(head2)->tqh_last = swap_last; \
|
||||
if ((swap_first = (head1)->tqh_first) != NULL) \
|
||||
swap_first->field.tqe_prev = &(head1)->tqh_first; \
|
||||
else \
|
||||
(head1)->tqh_last = &(head1)->tqh_first; \
|
||||
if ((swap_first = (head2)->tqh_first) != NULL) \
|
||||
swap_first->field.tqe_prev = &(head2)->tqh_first; \
|
||||
else \
|
||||
(head2)->tqh_last = &(head2)->tqh_first; \
|
||||
} while (0) \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
/*
|
||||
* Circular queue definitions.
|
||||
*/
|
||||
#define CIRCLEQ_HEAD(name, type) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
struct name { \
|
||||
struct type *cqh_first; /* first element */ \
|
||||
struct type *cqh_last; /* last element */ \
|
||||
} \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
#define CIRCLEQ_ENTRY(type) \
|
||||
__MISMATCH_TAGS_PUSH \
|
||||
__NULLABILITY_COMPLETENESS_PUSH \
|
||||
struct { \
|
||||
struct type *cqe_next; /* next element */ \
|
||||
struct type *cqe_prev; /* previous element */ \
|
||||
} \
|
||||
__NULLABILITY_COMPLETENESS_POP \
|
||||
__MISMATCH_TAGS_POP
|
||||
|
||||
/*
|
||||
* Circular queue functions.
|
||||
*/
|
||||
#define CIRCLEQ_CHECK_HEAD(head, field)
|
||||
#define CIRCLEQ_CHECK_NEXT(head, elm, field)
|
||||
#define CIRCLEQ_CHECK_PREV(head, elm, field)
|
||||
|
||||
#define CIRCLEQ_EMPTY(head) ((head)->cqh_first == (void *)(head))
|
||||
|
||||
#define CIRCLEQ_FIRST(head) ((head)->cqh_first)
|
||||
|
||||
#define CIRCLEQ_FOREACH(var, head, field) \
|
||||
for((var) = (head)->cqh_first; \
|
||||
(var) != (void *)(head); \
|
||||
(var) = (var)->field.cqe_next)
|
||||
|
||||
#define CIRCLEQ_INIT(head) do { \
|
||||
(head)->cqh_first = (void *)(head); \
|
||||
(head)->cqh_last = (void *)(head); \
|
||||
} while (0)
|
||||
|
||||
#define CIRCLEQ_INSERT_AFTER(head, listelm, elm, field) do { \
|
||||
CIRCLEQ_CHECK_NEXT(head, listelm, field); \
|
||||
(elm)->field.cqe_next = (listelm)->field.cqe_next; \
|
||||
(elm)->field.cqe_prev = (listelm); \
|
||||
if ((listelm)->field.cqe_next == (void *)(head)) \
|
||||
(head)->cqh_last = (elm); \
|
||||
else \
|
||||
(listelm)->field.cqe_next->field.cqe_prev = (elm); \
|
||||
(listelm)->field.cqe_next = (elm); \
|
||||
} while (0)
|
||||
|
||||
#define CIRCLEQ_INSERT_BEFORE(head, listelm, elm, field) do { \
|
||||
CIRCLEQ_CHECK_PREV(head, listelm, field); \
|
||||
(elm)->field.cqe_next = (listelm); \
|
||||
(elm)->field.cqe_prev = (listelm)->field.cqe_prev; \
|
||||
if ((listelm)->field.cqe_prev == (void *)(head)) \
|
||||
(head)->cqh_first = (elm); \
|
||||
else \
|
||||
(listelm)->field.cqe_prev->field.cqe_next = (elm); \
|
||||
(listelm)->field.cqe_prev = (elm); \
|
||||
} while (0)
|
||||
|
||||
#define CIRCLEQ_INSERT_HEAD(head, elm, field) do { \
|
||||
CIRCLEQ_CHECK_HEAD(head, field); \
|
||||
(elm)->field.cqe_next = (head)->cqh_first; \
|
||||
(elm)->field.cqe_prev = (void *)(head); \
|
||||
if ((head)->cqh_last == (void *)(head)) \
|
||||
(head)->cqh_last = (elm); \
|
||||
else \
|
||||
(head)->cqh_first->field.cqe_prev = (elm); \
|
||||
(head)->cqh_first = (elm); \
|
||||
} while (0)
|
||||
|
||||
#define CIRCLEQ_INSERT_TAIL(head, elm, field) do { \
|
||||
(elm)->field.cqe_next = (void *)(head); \
|
||||
(elm)->field.cqe_prev = (head)->cqh_last; \
|
||||
if ((head)->cqh_first == (void *)(head)) \
|
||||
(head)->cqh_first = (elm); \
|
||||
else \
|
||||
(head)->cqh_last->field.cqe_next = (elm); \
|
||||
(head)->cqh_last = (elm); \
|
||||
} while (0)
|
||||
|
||||
#define CIRCLEQ_LAST(head) ((head)->cqh_last)
|
||||
|
||||
#define CIRCLEQ_NEXT(elm, field) ((elm)->field.cqe_next)
|
||||
|
||||
#define CIRCLEQ_PREV(elm, field) ((elm)->field.cqe_prev)
|
||||
|
||||
#define CIRCLEQ_REMOVE(head, elm, field) do { \
|
||||
CIRCLEQ_CHECK_NEXT(head, elm, field); \
|
||||
CIRCLEQ_CHECK_PREV(head, elm, field); \
|
||||
if ((elm)->field.cqe_next == (void *)(head)) \
|
||||
(head)->cqh_last = (elm)->field.cqe_prev; \
|
||||
else \
|
||||
(elm)->field.cqe_next->field.cqe_prev = \
|
||||
(elm)->field.cqe_prev; \
|
||||
if ((elm)->field.cqe_prev == (void *)(head)) \
|
||||
(head)->cqh_first = (elm)->field.cqe_next; \
|
||||
else \
|
||||
(elm)->field.cqe_prev->field.cqe_next = \
|
||||
(elm)->field.cqe_next; \
|
||||
} while (0)
|
||||
|
||||
#ifdef _KERNEL
|
||||
|
||||
#if NOTFB31
|
||||
|
||||
/*
|
||||
* XXX insque() and remque() are an old way of handling certain queues.
|
||||
* They bogusly assumes that all queue heads look alike.
|
||||
*/
|
||||
|
||||
struct quehead {
|
||||
struct quehead *qh_link;
|
||||
struct quehead *qh_rlink;
|
||||
};
|
||||
|
||||
#ifdef __GNUC__
|
||||
#define chkquenext(a)
|
||||
#define chkqueprev(a)
|
||||
|
||||
static __inline void
|
||||
insque(void *a, void *b)
|
||||
{
|
||||
struct quehead *element = (struct quehead *)a,
|
||||
*head = (struct quehead *)b;
|
||||
chkquenext(head);
|
||||
|
||||
element->qh_link = head->qh_link;
|
||||
element->qh_rlink = head;
|
||||
head->qh_link = element;
|
||||
element->qh_link->qh_rlink = element;
|
||||
}
|
||||
|
||||
static __inline void
|
||||
remque(void *a)
|
||||
{
|
||||
struct quehead *element = (struct quehead *)a;
|
||||
chkquenext(element);
|
||||
chkqueprev(element);
|
||||
|
||||
element->qh_link->qh_rlink = element->qh_rlink;
|
||||
element->qh_rlink->qh_link = element->qh_link;
|
||||
element->qh_rlink = 0;
|
||||
}
|
||||
|
||||
#else /* !__GNUC__ */
|
||||
|
||||
void insque(void *a, void *b);
|
||||
void remque(void *a);
|
||||
|
||||
#endif /* __GNUC__ */
|
||||
|
||||
#endif /* NOTFB31 */
|
||||
#endif /* _KERNEL */
|
||||
|
||||
#endif /* !_SYS_QUEUE_H_ */
|
||||
Reference in New Issue
Block a user