Skip to content

Simplified FFI authoring with auto-generated C++ and Rust bindings#15

Merged
Ivansete-status merged 22 commits into
masterfrom
simplify-ffi
May 11, 2026
Merged

Simplified FFI authoring with auto-generated C++ and Rust bindings#15
Ivansete-status merged 22 commits into
masterfrom
simplify-ffi

Conversation

@Ivansete-status

@Ivansete-status Ivansete-status commented May 4, 2026

Copy link
Copy Markdown
Collaborator

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.}, calls genBindings(), and gets complete C++ and Rust bindings.

closes #10


Major changes

New compilation flags

  • ffiGenBindings: when added, activates binding generation.
  • 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:

# mylib.nim  <-- the file you pass to `nim c`
import mylib/timer_api
import mylib/network_api
import mylib/storage_api

genBindings()

New macro API

Pragma Purpose
{.ffi.} on a proc Exports a method; async or sync path chosen automatically by detecting await
{.ffiCtor.} on a proc Exports a constructor that creates and returns the FFI context
{.ffi.} on a type Registers the type for binding generation
genBindings() Emits C++ or Rust bindings at compile time (no-op without -d:ffiGenBindings)

Sync path optimization

{.ffi.} pragma inspects the proc body at compile time. If no await is 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)

  • Struct types with NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE macros
  • extern C declarations
  • Allows async C++ behaviour by the use of std:future.

Notice the genbindings_cpp task defined in examples/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_rust task defined in examples/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 argument
An 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

@fcecin fcecin left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Ivansete-status and others added 16 commits May 10, 2026 11:35
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 Ivansete-status self-assigned this May 11, 2026
@Ivansete-status
Ivansete-status merged commit a52c4fa into master May 11, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

make ffi usage more Nim user friendly

2 participants