Simplified FFI authoring with auto-generated C++ and Rust bindings#15
Merged
Conversation
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <copilot@github.com>
1. No timeout → wait_for + 30 s default (ffi/codegen/cpp.nim)
ffi_call_ now takes std::chrono::milliseconds timeout and uses cv.wait_for. All factory/method signatures carry a timeout parameter (default std::chrono::seconds{30}), mirroring the Rust blocking API.
2. Stack-allocated state → shared_ptr ownership (ffi/codegen/cpp.nim)
ffi_cb_ now receives a heap-allocated std::shared_ptr<FfiCallState_>* as user_data. The refcount is 2 going in (one for ffi_call_, one for the callback). If ffi_call_ times out and returns, its copy drops — but the state stays alive (refcount 1) until Nim eventually calls back and delete sptr in ffi_cb_ drops the last reference. No more stack UAF.
3. Destructor + Rule of 5 (ffi/codegen/cpp.nim, examples/nim_timer/nim_timer.nim)
Added nimtimer_destroy to nim_timer.nim with {.dynlib, exportc, cdecl, raises: [].} — joins the FFI and watchdog threads, frees the context
Codegen now always emits void {libName}_destroy(void* ctx) in extern "C" and generates a destructor, deleted copy ctor/assignment, and move ctor/assignment for the context class
timeout_ stored in the class; move transfers it, destructor uses it
4. Hardcoded TimerConfig in createAsync (ffi/codegen/cpp.nim)
createAsync now uses the actual ctorParams list (same as create), so it's correct for any library, not just nim_timer.
5. Opaque exceptions → clear error messages (ffi/codegen/cpp.nim)
deserializeFfiResult wraps nlohmann::json::parse + .get<T>() in a catch that rethrows as "FFI response deserialization failed: ...". The stoull in create() is also try-caught with "FFI create returned non-numeric address: " + raw.
Ivansete-status
force-pushed
the
simplify-ffi
branch
from
May 10, 2026 09:38
c5648ec to
d427166
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This replaces the manual, boilerplate FFI workflow with a pragma-based API and a compile-time binding generator. A library author now annotates regular Nim procs with
{.ffi.}/{.ffiCtor.}, callsgenBindings(), and gets complete C++ and Rust bindings.closes #10
Major changes
New compilation flags
targetLang(=cpp or =rust) selects the output desired language.ffiOutputDir=<path>: where to put the generated code.Notice that, in order to make it work properly, the
genBindings()must be invoked at the end of all the annotated{.ffi.}procs. For example, something like:New macro API
{.ffi.}on a procawait{.ffiCtor.}on a proc{.ffi.}on a typegenBindings()-d:ffiGenBindings)Sync path optimization
{.ffi.} pragma inspects the proc body at compile time. If no
awaitis found then the macro generates a fast inline path that invokes the callback without enqueuing a request to the FFI thread channel.Auto-generated C++ bindings (
ffi/codegen/cpp.nim)extern Cdeclarationsstd:future.Notice the
genbindings_cpptask defined inexamples/nim_timer/nim_timer.nimble, which allows auto-generating C++ bindings.Auto-generated Rust bindings (
ffi/codegen/rust.nim)A complete Rust crate is emitted under the configured output directory.
Notice the
genbindings_rusttask defined inexamples/nim_timer/nim_timer.nimble, which allows auto-generating Rust bindings.Example
A self-contained example under
examples/nim_timer/demonstrating:A constructor (
{.ffiCtor.}) with a TimerConfig argumentAn async method (nimtimerEcho) exercising chronos sleepAsync
A sync method (nimtimerVersion) taking the fast inline path
A method with complex nested types (ComplexRequest with seq, Option, type alias Maybe)
A nimtimer_destroy proc for clean teardown
A C++ client (cpp_bindings/main.cpp) and a Rust client (rust_client/) with both blocking and tokio async calls