@@ -1575,26 +1575,38 @@ macro ffiCtor*(args: varargs[untyped]): untyped =
15751575 return stmts
15761576
15771577macro ffiDtor * (args: varargs [untyped ]): untyped =
1578- # # Defines a C-exported destructor that tears down the FFIContext after the
1579- # # body runs.
1578+ # # Defines a C-exported destructor that tears down the FFIContext.
15801579 # #
1581- # # The annotated proc must have exactly one parameter of the library type.
1582- # # The body contains any library-level cleanup to run before context teardown.
1580+ # # The annotated proc must have exactly one parameter of the library type. It
1581+ # # may be sync (no return type) or async (`Future[void]`) — an async dtor can
1582+ # # `await` a graceful library shutdown (e.g. `switch.stop()`) whose futures
1583+ # # live on the FFI event loop.
15831584 # #
15841585 # # The wire format follows the library default and can be overridden with
15851586 # # `{.ffiDtor: "abi = c".}` / `{.ffiDtor: "abi = cbor".}`.
15861587 # #
1587- # # Example:
1588- # # proc waku_destroy*(w: Waku) {.ffiDtor.} =
1589- # # w.cleanup()
1588+ # # Example (sync):
1589+ # # proc echo_destroy*(e: Echo) {.ffiDtor.} =
1590+ # # e.close()
1591+ # #
1592+ # # Example (async):
1593+ # # proc waku_destroy*(w: Waku): Future[void] {.ffiDtor.} =
1594+ # # await w.stop()
15901595 # #
15911596 # # The generated C-exported proc has the signature:
15921597 # # int waku_destroy(void* ctx)
15931598 # #
1594- # # It extracts the library value from ctx, runs the body, then calls
1595- # # destroyFFIContext to tear down the FFI thread and free the context.
1599+ # # A non-empty body is lifted into an async impl registered in the library's
1600+ # # `ffiTeardownHook` slot; the FFI thread awaits it on its own event loop, after
1601+ # # draining in-flight requests and just before it exits — so the body runs on
1602+ # # the worker thread, not the host (calling) thread. The C wrapper signals the
1603+ # # thread to stop and blocks (up to `ThreadExitTimeout`) until it, and the
1604+ # # teardown, finish, then frees the context. An empty/`discard` body registers
1605+ # # no hook.
1606+ # #
15961607 # # Returns RET_OK on success, RET_ERR on failure (null/invalid ctx, or
1597- # # destroyFFIContext failure).
1608+ # # destroyFFIContext failure — e.g. a teardown that outlasts ThreadExitTimeout,
1609+ # # which leaks the context rather than hanging the caller).
15981610
15991611 requireBeforeGenBindings (" `.ffiDtor.`" )
16001612 requireLibraryDeclared (" `.ffiDtor.`" )
@@ -1612,6 +1624,18 @@ macro ffiDtor*(args: varargs[untyped]): untyped =
16121624 let libParamName = formalParams[1 ][0 ]
16131625 let libTypeName = formalParams[1 ][1 ]
16141626
1627+ # A dtor is sync (no return type) or async (`Future[void]`); reject anything
1628+ # else up front rather than emitting an obscure downstream error.
1629+ let retTypeNode = formalParams[0 ]
1630+ let retIsFutureVoid =
1631+ retTypeNode.kind == nnkBracketExpr and $ retTypeNode[0 ] == " Future" and
1632+ retTypeNode.len == 2 and $ retTypeNode[1 ] == " void"
1633+ if retTypeNode.kind != nnkEmpty and not retIsFutureVoid:
1634+ error (
1635+ " ffiDtor: proc must return nothing (sync) or Future[void] (async), got: " &
1636+ retTypeNode.repr
1637+ )
1638+
16151639 let procNameStr = block :
16161640 let raw = $ procName
16171641 if raw.endsWith (" *" ):
@@ -1639,16 +1663,26 @@ macro ffiDtor*(args: varargs[untyped]): untyped =
16391663 if ctx.isNil or cast [ptr FFIContext [`libTypeName`]](ctx)[].myLib.isNil:
16401664 return RET_ERR
16411665
1642- ffiBody.add quote do :
1643- let `libParamName` = cast [ptr FFIContext [`libTypeName`]](ctx)[].myLib[]
1644-
16451666 let isNoop =
16461667 bodyNode.kind == nnkEmpty or (
16471668 bodyNode.kind == nnkStmtList and bodyNode.len == 1 and
16481669 bodyNode[0 ].kind == nnkDiscardStmt
16491670 )
1650- if not isNoop:
1651- ffiBody.add (bodyNode)
1671+
1672+ # Lift the body into an async impl registered in the per-library
1673+ # `ffiTeardownHook`, which the FFI thread awaits at shutdown (see ffi_thread.nim
1674+ # and ffiTeardownHook's docstring). The C wrapper no longer runs the body.
1675+ let teardownImplName = genSym (nskProc, " ffiTeardownImpl" )
1676+ let teardownRegistration =
1677+ if isNoop:
1678+ newEmptyNode ()
1679+ else :
1680+ quote:
1681+ proc `teardownImplName` (lib: ptr `libTypeName`): Future [void ] {.async .} =
1682+ let `libParamName` = lib[]
1683+ `bodyNode`
1684+
1685+ ffiTeardownHook [`libTypeName`]() = `teardownImplName`
16521686
16531687 let poolIdent = ident ($ libTypeName & " FFIPool" )
16541688 ffiBody.add quote do :
@@ -1690,7 +1724,7 @@ macro ffiDtor*(args: varargs[untyped]): untyped =
16901724 when not declared (`poolIdent`):
16911725 var `poolIdent`: FFIContextPool [`libTypeName`]
16921726
1693- let stmts = newStmtList (poolDecl, ffiProc)
1727+ let stmts = newStmtList (teardownRegistration, poolDecl, ffiProc)
16941728
16951729 when defined (ffiDumpMacros):
16961730 echo stmts.repr
0 commit comments