mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-05-07 10:49:28 +00:00
Merge 558356149bb3dfa398a2d06702116865a49ac42a into e3eca63236384eee8f8a396ffeb89ebfc07306fc
This commit is contained in:
commit
b07f34c1fe
20
.gitignore
vendored
20
.gitignore
vendored
@ -1,3 +1,23 @@
|
||||
nimble.develop
|
||||
nimble.paths
|
||||
nimbledeps
|
||||
|
||||
# Nim compiler output
|
||||
*.dylib
|
||||
*.so
|
||||
*.dll
|
||||
tests/test_alloc
|
||||
tests/test_ffi_context
|
||||
tests/test_serial
|
||||
|
||||
# Generated binding crates (regenerated by `nimble genbindings_*`)
|
||||
examples/**/rust_bindings/target/
|
||||
|
||||
# Example build artifacts
|
||||
examples/**/cpp_bindings/build/
|
||||
|
||||
# Cargo build artifacts
|
||||
examples/**/rust_client/target/
|
||||
|
||||
# Development plan (local only)
|
||||
PLAN.md
|
||||
|
||||
@ -1,2 +1,7 @@
|
||||
# nim-ffi
|
||||
Allows exposing Nim projects to other languages
|
||||
|
||||
## Example
|
||||
|
||||
`examples/nim_timer` is now a self-contained Nimble project that imports `nim-ffi` directly.
|
||||
Use `cd examples/nim_timer && nimble install -y ../.. && nimble build` to compile the example.
|
||||
|
||||
54
examples/nim_timer/README.md
Normal file
54
examples/nim_timer/README.md
Normal file
@ -0,0 +1,54 @@
|
||||
# nim_timer example
|
||||
|
||||
This example is a self-contained Nimble project demonstrating how to import `nim-ffi` and use the `.ffiCtor.` / `.ffi.` abstraction.
|
||||
|
||||
## Usage
|
||||
|
||||
1. Change into the example directory:
|
||||
```sh
|
||||
cd examples/nim_timer
|
||||
```
|
||||
|
||||
2. Install the local `ffi` dependency:
|
||||
```sh
|
||||
nimble install -y ../..
|
||||
```
|
||||
|
||||
3. Build the example library:
|
||||
```sh
|
||||
nimble build
|
||||
```
|
||||
|
||||
4. Generate bindings:
|
||||
```sh
|
||||
nimble genbindings_rust
|
||||
nimble genbindings_cpp
|
||||
```
|
||||
|
||||
## Rust example clients
|
||||
|
||||
The Rust client lives in `examples/nim_timer/rust_client`.
|
||||
|
||||
- Run the sync example:
|
||||
```sh
|
||||
cd examples/nim_timer/rust_client
|
||||
cargo run --bin rust_client
|
||||
```
|
||||
|
||||
- Run the Tokio example:
|
||||
```sh
|
||||
cd examples/nim_timer/rust_client
|
||||
cargo run --bin tokio_client
|
||||
```
|
||||
|
||||
## C++ example
|
||||
|
||||
The generated C++ example lives in `examples/nim_timer/cpp_bindings`.
|
||||
|
||||
Build and run it with:
|
||||
```sh
|
||||
cd examples/nim_timer/cpp_bindings
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
./build/example
|
||||
```
|
||||
78
examples/nim_timer/cpp_bindings/CMakeLists.txt
Normal file
78
examples/nim_timer/cpp_bindings/CMakeLists.txt
Normal file
@ -0,0 +1,78 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(nimtimer_cpp_bindings CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# ── nlohmann/json ─────────────────────────────────────────────────────────────
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(
|
||||
nlohmann_json
|
||||
GIT_REPOSITORY https://github.com/nlohmann/json.git
|
||||
GIT_TAG v3.11.3
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
FetchContent_MakeAvailable(nlohmann_json)
|
||||
|
||||
# ── Locate the repository root (contains ffi.nimble) ─────────────────────────
|
||||
set(_search_dir "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
set(REPO_ROOT "")
|
||||
foreach(_i RANGE 10)
|
||||
if(EXISTS "${_search_dir}/ffi.nimble")
|
||||
set(REPO_ROOT "${_search_dir}")
|
||||
break()
|
||||
endif()
|
||||
get_filename_component(_search_dir "${_search_dir}" DIRECTORY)
|
||||
endforeach()
|
||||
if("${REPO_ROOT}" STREQUAL "")
|
||||
message(FATAL_ERROR "Cannot find repo root (no ffi.nimble in any ancestor)")
|
||||
endif()
|
||||
|
||||
# ── Nim source path ───────────────────────────────────────────────────────────
|
||||
get_filename_component(NIM_SRC
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/../nim_timer.nim"
|
||||
ABSOLUTE)
|
||||
|
||||
# ── Compile the Nim shared library ───────────────────────────────────────────
|
||||
find_program(NIM_EXECUTABLE nim REQUIRED)
|
||||
|
||||
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/libnimtimer.dylib")
|
||||
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/nimtimer.dll")
|
||||
else()
|
||||
set(NIM_LIB_FILE "${REPO_ROOT}/libnimtimer.so")
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${NIM_LIB_FILE}"
|
||||
COMMAND "${NIM_EXECUTABLE}" c
|
||||
--mm:orc
|
||||
-d:chronicles_log_level=WARN
|
||||
--app:lib
|
||||
--noMain
|
||||
"--nimMainPrefix:libnimtimer"
|
||||
"-o:${NIM_LIB_FILE}"
|
||||
"${NIM_SRC}"
|
||||
WORKING_DIRECTORY "${REPO_ROOT}"
|
||||
DEPENDS "${NIM_SRC}"
|
||||
COMMENT "Compiling Nim library libnimtimer"
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(nim_lib ALL DEPENDS "${NIM_LIB_FILE}")
|
||||
|
||||
add_library(nimtimer SHARED IMPORTED GLOBAL)
|
||||
set_target_properties(nimtimer PROPERTIES IMPORTED_LOCATION "${NIM_LIB_FILE}")
|
||||
add_dependencies(nimtimer nim_lib)
|
||||
|
||||
# ── Interface target exposing the generated header ────────────────────────────
|
||||
add_library(nimtimer_headers INTERFACE)
|
||||
target_include_directories(nimtimer_headers INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_link_libraries(nimtimer_headers INTERFACE nimtimer nlohmann_json::nlohmann_json)
|
||||
|
||||
# ── Optional example executable ───────────────────────────────────────────────
|
||||
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp")
|
||||
add_executable(example main.cpp)
|
||||
target_link_libraries(example PRIVATE nimtimer_headers)
|
||||
add_dependencies(example nim_lib)
|
||||
endif()
|
||||
36
examples/nim_timer/cpp_bindings/README.md
Normal file
36
examples/nim_timer/cpp_bindings/README.md
Normal file
@ -0,0 +1,36 @@
|
||||
# C++ Bindings for nim-timer
|
||||
|
||||
## Purpose
|
||||
|
||||
This folder contains **auto-generated C++ bindings** for the `nim_timer` Nim library. It is generated from `../nim_timer.nim` and provides:
|
||||
|
||||
- `nimtimer.hpp`: High-level C++ class (`NimTimerCtx`) wrapping the FFI interface
|
||||
- `main.cpp`: Example executable demonstrating how to use the bindings
|
||||
- `CMakeLists.txt`: Build configuration that compiles the Nim library and links the C++ example
|
||||
|
||||
## How It's Generated
|
||||
|
||||
Generate or regenerate these bindings by running from the parent directory:
|
||||
|
||||
```sh
|
||||
cd examples/nim_timer
|
||||
nimble genbindings_cpp
|
||||
```
|
||||
|
||||
This command:
|
||||
1. Invokes the Nim compiler with `-d:targetLang:cpp` flag
|
||||
2. Triggers `genBindings("examples/nim_timer/cpp_bindings", "../nim_timer.nim")` in `nim_timer.nim`
|
||||
3. Creates/updates the generated binding files
|
||||
|
||||
## Building the Example
|
||||
|
||||
```sh
|
||||
cd examples/nim_timer/cpp_bindings
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
./build/example
|
||||
```
|
||||
|
||||
## Do Not Edit
|
||||
|
||||
The generated files in this folder are overwritten each time `nimble genbindings_cpp` runs. Any manual changes will be lost.
|
||||
44
examples/nim_timer/cpp_bindings/main.cpp
Normal file
44
examples/nim_timer/cpp_bindings/main.cpp
Normal file
@ -0,0 +1,44 @@
|
||||
#include "nimtimer.hpp"
|
||||
#include <iostream>
|
||||
#include <future>
|
||||
|
||||
int main() {
|
||||
try {
|
||||
auto ctx = NimTimerCtx::create(TimerConfig{"cpp-demo"});
|
||||
std::cout << "[1] Context created\n";
|
||||
|
||||
auto versionFuture = ctx.versionAsync();
|
||||
auto echo1Future = ctx.echoAsync(EchoRequest{"hello from C++", 200});
|
||||
auto echo2Future = ctx.echoAsync(EchoRequest{"second C++ request", 50});
|
||||
|
||||
auto version = versionFuture.get();
|
||||
std::cout << "[2] Version: " << version << "\n";
|
||||
|
||||
auto echo = echo1Future.get();
|
||||
std::cout << "[3] Echo 1: echoed=" << echo.echoed
|
||||
<< ", timerName=" << echo.timerName << "\n";
|
||||
|
||||
auto echo2 = echo2Future.get();
|
||||
std::cout << "[4] Echo 2: echoed=" << echo2.echoed
|
||||
<< ", timerName=" << echo2.timerName << "\n";
|
||||
|
||||
auto complexReq = ComplexRequest{
|
||||
std::vector<EchoRequest>{EchoRequest{"one", 10}, EchoRequest{"two", 20}},
|
||||
std::vector<std::string>{"fast", "async"},
|
||||
std::optional<std::string>("extra note"),
|
||||
std::optional<int64_t>(3)
|
||||
};
|
||||
|
||||
auto complexFuture = ctx.complexAsync(complexReq);
|
||||
auto complex = complexFuture.get();
|
||||
std::cout << "[5] Complex: summary=" << complex.summary
|
||||
<< ", itemCount=" << complex.itemCount
|
||||
<< ", hasNote=" << complex.hasNote << "\n";
|
||||
|
||||
std::cout << "\nDone.\n";
|
||||
} catch (const std::exception& ex) {
|
||||
std::cerr << "Error: " << ex.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
238
examples/nim_timer/cpp_bindings/nimtimer.hpp
Normal file
238
examples/nim_timer/cpp_bindings/nimtimer.hpp
Normal file
@ -0,0 +1,238 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <chrono>
|
||||
#include <stdexcept>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace nlohmann {
|
||||
template<typename T>
|
||||
void to_json(json& j, const std::optional<T>& opt) {
|
||||
if (opt) j = *opt;
|
||||
else j = nullptr;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void from_json(const json& j, std::optional<T>& opt) {
|
||||
if (j.is_null()) opt = std::nullopt;
|
||||
else opt = j.get<T>();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Types
|
||||
// ============================================================
|
||||
|
||||
struct TimerConfig {
|
||||
std::string name;
|
||||
};
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(TimerConfig, name)
|
||||
|
||||
struct EchoRequest {
|
||||
std::string message;
|
||||
int64_t delayMs;
|
||||
};
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(EchoRequest, message, delayMs)
|
||||
|
||||
struct EchoResponse {
|
||||
std::string echoed;
|
||||
std::string timerName;
|
||||
};
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(EchoResponse, echoed, timerName)
|
||||
|
||||
struct ComplexRequest {
|
||||
std::vector<EchoRequest> messages;
|
||||
std::vector<std::string> tags;
|
||||
std::optional<std::string> note;
|
||||
std::optional<int64_t> retries;
|
||||
};
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ComplexRequest, messages, tags, note, retries)
|
||||
|
||||
struct ComplexResponse {
|
||||
std::string summary;
|
||||
int64_t itemCount;
|
||||
bool hasNote;
|
||||
};
|
||||
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ComplexResponse, summary, itemCount, hasNote)
|
||||
|
||||
// ============================================================
|
||||
// C FFI declarations
|
||||
// ============================================================
|
||||
|
||||
extern "C" {
|
||||
typedef void (*FfiCallback)(int ret, const char* msg, size_t len, void* user_data);
|
||||
|
||||
int nimtimer_create(const char* config_json, FfiCallback callback, void* user_data);
|
||||
int nimtimer_echo(void* ctx, FfiCallback callback, void* user_data, const char* req_json);
|
||||
int nimtimer_version(void* ctx, FfiCallback callback, void* user_data);
|
||||
int nimtimer_complex(void* ctx, FfiCallback callback, void* user_data, const char* req_json);
|
||||
void nimtimer_destroy(void* ctx);
|
||||
} // extern "C"
|
||||
|
||||
template<typename T>
|
||||
inline std::string serializeFfiArg(const T& value) {
|
||||
return nlohmann::json(value).dump();
|
||||
}
|
||||
|
||||
inline std::string serializeFfiArg(void* value) {
|
||||
return std::to_string(reinterpret_cast<uintptr_t>(value));
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline T deserializeFfiResult(const std::string& raw) {
|
||||
try {
|
||||
return nlohmann::json::parse(raw).get<T>();
|
||||
} catch (const nlohmann::json::exception& e) {
|
||||
throw std::runtime_error(std::string("FFI response deserialization failed: ") + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void* deserializeFfiResult<void*>(const std::string& raw) {
|
||||
try {
|
||||
return reinterpret_cast<void*>(static_cast<uintptr_t>(std::stoull(raw)));
|
||||
} catch (const std::exception& e) {
|
||||
throw std::runtime_error(std::string("FFI returned non-numeric address: ") + raw);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Synchronous call helper (anonymous namespace, header-only)
|
||||
// ============================================================
|
||||
|
||||
namespace {
|
||||
|
||||
struct FfiCallState_ {
|
||||
std::mutex mtx;
|
||||
std::condition_variable cv;
|
||||
bool done{false};
|
||||
bool ok{false};
|
||||
std::string msg;
|
||||
};
|
||||
|
||||
inline void ffi_cb_(int ret, const char* msg, size_t /*len*/, void* ud) {
|
||||
auto* sptr = static_cast<std::shared_ptr<FfiCallState_>*>(ud);
|
||||
{
|
||||
auto& s = **sptr;
|
||||
std::lock_guard<std::mutex> lock(s.mtx);
|
||||
s.ok = (ret == 0);
|
||||
s.msg = msg ? std::string(msg) : std::string{};
|
||||
s.done = true;
|
||||
s.cv.notify_one();
|
||||
}
|
||||
delete sptr;
|
||||
}
|
||||
|
||||
inline std::string ffi_call_(std::function<int(FfiCallback, void*)> f,
|
||||
std::chrono::milliseconds timeout) {
|
||||
auto state = std::make_shared<FfiCallState_>();
|
||||
auto* cb_ref = new std::shared_ptr<FfiCallState_>(state);
|
||||
const int ret = f(ffi_cb_, cb_ref);
|
||||
if (ret == 2) {
|
||||
delete cb_ref;
|
||||
throw std::runtime_error("RET_MISSING_CALLBACK (internal error)");
|
||||
}
|
||||
std::unique_lock<std::mutex> lock(state->mtx);
|
||||
const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; });
|
||||
if (!fired)
|
||||
throw std::runtime_error("FFI call timed out after " + std::to_string(timeout.count()) + "ms");
|
||||
if (!state->ok)
|
||||
throw std::runtime_error(state->msg);
|
||||
return state->msg;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ============================================================
|
||||
// High-level C++ context class
|
||||
// ============================================================
|
||||
|
||||
class NimTimerCtx {
|
||||
public:
|
||||
static NimTimerCtx create(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
|
||||
const auto config_json = serializeFfiArg(config);
|
||||
const auto raw = ffi_call_([&](FfiCallback cb, void* ud) {
|
||||
return nimtimer_create(config_json.c_str(), cb, ud);
|
||||
}, timeout);
|
||||
try {
|
||||
const auto addr = std::stoull(raw);
|
||||
return NimTimerCtx(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)), timeout);
|
||||
} catch (const std::exception&) {
|
||||
throw std::runtime_error("FFI create returned non-numeric address: " + raw);
|
||||
}
|
||||
}
|
||||
|
||||
static std::future<NimTimerCtx> createAsync(const TimerConfig& config, std::chrono::milliseconds timeout = std::chrono::seconds{30}) {
|
||||
return std::async(std::launch::async, [config, timeout]() { return create(config, timeout); });
|
||||
}
|
||||
|
||||
~NimTimerCtx() {
|
||||
if (ptr_) {
|
||||
nimtimer_destroy(ptr_);
|
||||
ptr_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
NimTimerCtx(const NimTimerCtx&) = delete;
|
||||
NimTimerCtx& operator=(const NimTimerCtx&) = delete;
|
||||
|
||||
NimTimerCtx(NimTimerCtx&& other) noexcept : ptr_(other.ptr_), timeout_(other.timeout_) {
|
||||
other.ptr_ = nullptr;
|
||||
}
|
||||
NimTimerCtx& operator=(NimTimerCtx&& other) noexcept {
|
||||
if (this != &other) {
|
||||
if (ptr_) nimtimer_destroy(ptr_);
|
||||
ptr_ = other.ptr_;
|
||||
timeout_ = other.timeout_;
|
||||
other.ptr_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
EchoResponse echo(const EchoRequest& req) const {
|
||||
const auto req_json = serializeFfiArg(req);
|
||||
const auto raw = ffi_call_([&](FfiCallback cb, void* ud) {
|
||||
return nimtimer_echo(ptr_, cb, ud, req_json.c_str());
|
||||
}, timeout_);
|
||||
return deserializeFfiResult<EchoResponse>(raw);
|
||||
}
|
||||
|
||||
std::future<EchoResponse> echoAsync(const EchoRequest& req) const {
|
||||
return std::async(std::launch::async, [this, req]() { return echo(req); });
|
||||
}
|
||||
|
||||
std::string version() const {
|
||||
const auto raw = ffi_call_([&](FfiCallback cb, void* ud) {
|
||||
return nimtimer_version(ptr_, cb, ud);
|
||||
}, timeout_);
|
||||
return deserializeFfiResult<std::string>(raw);
|
||||
}
|
||||
|
||||
std::future<std::string> versionAsync() const {
|
||||
return std::async(std::launch::async, [this]() { return version(); });
|
||||
}
|
||||
|
||||
ComplexResponse complex(const ComplexRequest& req) const {
|
||||
const auto req_json = serializeFfiArg(req);
|
||||
const auto raw = ffi_call_([&](FfiCallback cb, void* ud) {
|
||||
return nimtimer_complex(ptr_, cb, ud, req_json.c_str());
|
||||
}, timeout_);
|
||||
return deserializeFfiResult<ComplexResponse>(raw);
|
||||
}
|
||||
|
||||
std::future<ComplexResponse> complexAsync(const ComplexRequest& req) const {
|
||||
return std::async(std::launch::async, [this, req]() { return complex(req); });
|
||||
}
|
||||
|
||||
private:
|
||||
void* ptr_;
|
||||
std::chrono::milliseconds timeout_;
|
||||
explicit NimTimerCtx(void* p, std::chrono::milliseconds t) : ptr_(p), timeout_(t) {}
|
||||
};
|
||||
83
examples/nim_timer/nim_timer.nim
Normal file
83
examples/nim_timer/nim_timer.nim
Normal file
@ -0,0 +1,83 @@
|
||||
import ffi, chronos, options
|
||||
|
||||
type Maybe[T] = Option[T]
|
||||
|
||||
declareLibrary("nimtimer")
|
||||
|
||||
# The library's main state type. The FFI context owns one instance.
|
||||
type NimTimer = object
|
||||
name: string # set at creation time, read back in each response
|
||||
|
||||
type TimerConfig {.ffi.} = object
|
||||
name: string
|
||||
|
||||
type EchoRequest {.ffi.} = object
|
||||
message: string
|
||||
delayMs: int # how long chronos sleeps before replying
|
||||
|
||||
type EchoResponse {.ffi.} = object
|
||||
echoed: string
|
||||
timerName: string # proves that the timer's own state is accessible
|
||||
|
||||
type ComplexRequest {.ffi.} = object
|
||||
messages: seq[EchoRequest]
|
||||
tags: seq[string]
|
||||
note: Option[string]
|
||||
retries: Maybe[int]
|
||||
|
||||
type ComplexResponse {.ffi.} = object
|
||||
summary: string
|
||||
itemCount: int
|
||||
hasNote: bool
|
||||
|
||||
# --- Constructor -----------------------------------------------------------
|
||||
# Called once from Rust. Creates the FFIContext + NimTimer.
|
||||
# Uses chronos (await sleepAsync) so the body is async.
|
||||
proc nimtimerCreate*(
|
||||
config: TimerConfig
|
||||
): Future[Result[NimTimer, string]] {.ffiCtor.} =
|
||||
await sleepAsync(1.milliseconds) # proves chronos is live on the FFI thread
|
||||
return ok(NimTimer(name: config.name))
|
||||
|
||||
# --- Async method ----------------------------------------------------------
|
||||
# Waits `delayMs` milliseconds (non-blocking, on the chronos event loop)
|
||||
# then echoes the message back with a request counter.
|
||||
proc nimtimerEcho*(
|
||||
timer: NimTimer, req: EchoRequest
|
||||
): Future[Result[EchoResponse, string]] {.ffi.} =
|
||||
await sleepAsync(req.delayMs.milliseconds)
|
||||
return ok(EchoResponse(echoed: req.message, timerName: timer.name))
|
||||
|
||||
# --- Sync method -----------------------------------------------------------
|
||||
# No await — the macro detects this and fires the callback inline,
|
||||
# without going through the request channel.
|
||||
proc nimtimerVersion*(timer: NimTimer): Future[Result[string, string]] {.ffi.} =
|
||||
return ok("nim-timer v0.1.0")
|
||||
|
||||
proc nimtimerComplex*(
|
||||
timer: NimTimer, req: ComplexRequest
|
||||
): Future[Result[ComplexResponse, string]] {.ffi.} =
|
||||
let note = if req.note.isSome: req.note.get else: "<none>"
|
||||
let retries = if req.retries.isSome: req.retries.get else: 0
|
||||
let count = req.messages.len
|
||||
let summary =
|
||||
"received " & $count & " messages, note=" & note & ", retries=" & $retries
|
||||
return
|
||||
ok(ComplexResponse(summary: summary, itemCount: count, hasNote: req.note.isSome))
|
||||
|
||||
# --- genBindings() must come AFTER every {.ffi.} / {.ffiCtor.} annotation ---
|
||||
# Each pragma populates ffiProcRegistry / ffiTypeRegistry at compile time as
|
||||
# the compiler processes the AST. genBindings() reads those registries to emit
|
||||
# the binding files, so placing it any earlier would produce incomplete output.
|
||||
# In a multi-file library, import all sub-modules first and call genBindings()
|
||||
# once, at the bottom of the top-level compilation-root file.
|
||||
# This call is a no-op unless -d:ffiGenBindings is passed to the compiler.
|
||||
genBindings() # reads -d:ffiOutputDir, -d:ffiNimSrcRelPath, -d:targetLang from compile flags
|
||||
|
||||
proc nimtimer_destroy*(ctx: pointer) {.dynlib, exportc, cdecl, raises: [].} =
|
||||
## Tears down the FFI context created by nimtimer_create.
|
||||
## Blocks until the FFI thread and watchdog thread have joined.
|
||||
try:
|
||||
discard destroyFFIContext[NimTimer](cast[ptr FFIContext[NimTimer]](ctx))
|
||||
except:
|
||||
discard
|
||||
27
examples/nim_timer/nim_timer.nimble
Normal file
27
examples/nim_timer/nim_timer.nimble
Normal file
@ -0,0 +1,27 @@
|
||||
version = "0.1.0"
|
||||
packageName = "nimtimer"
|
||||
author = "Institute of Free Technology"
|
||||
description = "Example Nim timer library using nim-ffi"
|
||||
license = "MIT or Apache License 2.0"
|
||||
|
||||
requires "nim >= 2.2.4"
|
||||
requires "chronos"
|
||||
requires "chronicles"
|
||||
requires "taskpools"
|
||||
requires "https://github.com/logos-messaging/nim-ffi >= 0.1.3"
|
||||
|
||||
const nimFlags = "--mm:orc -d:chronicles_log_level=WARN"
|
||||
|
||||
task build, "Compile the nimtimer library":
|
||||
exec "nim c " & nimFlags &
|
||||
" --app:lib --noMain --nimMainPrefix:libnimtimer nim_timer.nim"
|
||||
|
||||
task genbindings_rust, "Generate Rust bindings for the nimtimer example":
|
||||
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer" &
|
||||
" -d:ffiGenBindings -d:targetLang=rust" & " -d:ffiOutputDir=rust_bindings" &
|
||||
" -d:ffiNimSrcRelPath=nim_timer.nim" & " -o:/dev/null nim_timer.nim"
|
||||
|
||||
task genbindings_cpp, "Generate C++ bindings for the nimtimer example":
|
||||
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer" &
|
||||
" -d:ffiGenBindings -d:targetLang=cpp" & " -d:ffiOutputDir=cpp_bindings" &
|
||||
" -d:ffiNimSrcRelPath=nim_timer.nim" & " -o:/dev/null nim_timer.nim"
|
||||
9
examples/nim_timer/rust_bindings/Cargo.toml
Normal file
9
examples/nim_timer/rust_bindings/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "nimtimer"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["sync"] }
|
||||
39
examples/nim_timer/rust_bindings/README.md
Normal file
39
examples/nim_timer/rust_bindings/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Rust Bindings for nim-timer
|
||||
|
||||
## Purpose
|
||||
|
||||
This folder contains **auto-generated Rust bindings** (the `nimtimer` crate) for the `nim_timer` Nim library. It is generated from `../nim_timer.nim` and provides:
|
||||
|
||||
- `src/lib.rs`: Main library exposing high-level Rust types and the `NimTimerCtx` API
|
||||
- `src/api.rs`: High-level async/sync wrapper around the FFI
|
||||
- `src/ffi.rs`: Raw `extern "C"` declarations for the Nim library
|
||||
- `src/types.rs`: Serializable Rust types matching the Nim FFI types
|
||||
- `build.rs`: Build script that compiles the Nim library to `libnimtimer.dylib` (or `.so`/`.dll`)
|
||||
- `Cargo.toml`: Package manifest with serde and serde_json dependencies
|
||||
|
||||
## How It's Generated
|
||||
|
||||
Generate or regenerate these bindings by running from the parent directory:
|
||||
|
||||
```sh
|
||||
cd examples/nim_timer
|
||||
nimble genbindings_rust
|
||||
```
|
||||
|
||||
This command:
|
||||
1. Invokes the Nim compiler with `-d:targetLang:rust` flag
|
||||
2. Triggers `genBindings("examples/nim_timer/rust_bindings", "../nim_timer.nim")` in `nim_timer.nim`
|
||||
3. Creates/updates the generated binding files
|
||||
|
||||
## Using as a Dependency
|
||||
|
||||
The `rust_client` example consumes this crate:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
nimtimer = { path = "../rust_bindings" }
|
||||
```
|
||||
|
||||
## Do Not Edit
|
||||
|
||||
The generated files in this folder are overwritten each time `nimble genbindings_rust` runs. Any manual changes will be lost.
|
||||
47
examples/nim_timer/rust_bindings/build.rs
Normal file
47
examples/nim_timer/rust_bindings/build.rs
Normal file
@ -0,0 +1,47 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let nim_src = manifest.join("../nim_timer.nim");
|
||||
let nim_src = nim_src.canonicalize().unwrap_or(manifest.join("../nim_timer.nim"));
|
||||
|
||||
// Walk up to find the nim-ffi repo root (directory containing nim_src's library)
|
||||
// The repo root is where nim c should be run from (contains config.nims).
|
||||
// We assume nim_src lives somewhere under repo_root.
|
||||
// Derive repo_root as the ancestor that contains the .nimble file or config.nims.
|
||||
let mut repo_root = nim_src.clone();
|
||||
loop {
|
||||
repo_root = match repo_root.parent() {
|
||||
Some(p) => p.to_path_buf(),
|
||||
None => break,
|
||||
};
|
||||
if repo_root.join("config.nims").exists() || repo_root.join("ffi.nimble").exists() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let lib_ext = "dylib";
|
||||
#[cfg(target_os = "linux")]
|
||||
let lib_ext = "so";
|
||||
|
||||
let out_lib = repo_root.join(format!("libnimtimer.{lib_ext}"));
|
||||
|
||||
let mut cmd = Command::new("nim");
|
||||
cmd.arg("c")
|
||||
.arg("--mm:orc")
|
||||
.arg("-d:chronicles_log_level=WARN")
|
||||
.arg("--app:lib")
|
||||
.arg("--noMain")
|
||||
.arg(format!("--nimMainPrefix:libnimtimer"))
|
||||
.arg(format!("-o:{}", out_lib.display()));
|
||||
cmd.arg(&nim_src).current_dir(&repo_root);
|
||||
|
||||
let status = cmd.status().expect("failed to run nim compiler");
|
||||
assert!(status.success(), "Nim compilation failed");
|
||||
|
||||
println!("cargo:rustc-link-search={}", repo_root.display());
|
||||
println!("cargo:rustc-link-lib=nimtimer");
|
||||
println!("cargo:rerun-if-changed={}", nim_src.display());
|
||||
}
|
||||
177
examples/nim_timer/rust_bindings/src/api.rs
Normal file
177
examples/nim_timer/rust_bindings/src/api.rs
Normal file
@ -0,0 +1,177 @@
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::{c_char, c_int, c_void};
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
use std::time::Duration;
|
||||
use super::ffi;
|
||||
use super::types::*;
|
||||
|
||||
#[derive(Default)]
|
||||
struct FfiCallbackResult {
|
||||
payload: Option<Result<String, String>>,
|
||||
}
|
||||
|
||||
type Pair = Arc<(Mutex<FfiCallbackResult>, Condvar)>;
|
||||
|
||||
unsafe extern "C" fn on_result(
|
||||
ret: c_int,
|
||||
msg: *const c_char,
|
||||
_len: usize,
|
||||
user_data: *mut c_void,
|
||||
) {
|
||||
let pair = Arc::from_raw(user_data as *const (Mutex<FfiCallbackResult>, Condvar));
|
||||
{
|
||||
let (lock, cvar) = &*pair;
|
||||
let mut state = lock.lock().unwrap();
|
||||
state.payload = Some(if ret == 0 {
|
||||
Ok(CStr::from_ptr(msg).to_string_lossy().into_owned())
|
||||
} else {
|
||||
Err(CStr::from_ptr(msg).to_string_lossy().into_owned())
|
||||
});
|
||||
cvar.notify_one();
|
||||
}
|
||||
std::mem::forget(pair);
|
||||
}
|
||||
|
||||
fn ffi_call<F>(timeout: Duration, f: F) -> Result<String, String>
|
||||
where
|
||||
F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,
|
||||
{
|
||||
let pair: Pair = Arc::new((Mutex::new(FfiCallbackResult::default()), Condvar::new()));
|
||||
let raw = Arc::into_raw(pair.clone()) as *mut c_void;
|
||||
let ret = f(on_result, raw);
|
||||
if ret == 2 {
|
||||
return Err("RET_MISSING_CALLBACK (internal error)".into());
|
||||
}
|
||||
let (lock, cvar) = &*pair;
|
||||
let guard = lock.lock().unwrap();
|
||||
let (guard, timed_out) = cvar
|
||||
.wait_timeout_while(guard, timeout, |s| s.payload.is_none())
|
||||
.unwrap();
|
||||
if timed_out.timed_out() {
|
||||
return Err(format!("timed out after {:?}", timeout));
|
||||
}
|
||||
guard.payload.clone().unwrap()
|
||||
}
|
||||
|
||||
unsafe extern "C" fn on_result_async(
|
||||
ret: c_int,
|
||||
msg: *const c_char,
|
||||
_len: usize,
|
||||
user_data: *mut c_void,
|
||||
) {
|
||||
let tx = Box::from_raw(
|
||||
user_data as *mut tokio::sync::oneshot::Sender<Result<String, String>>,
|
||||
);
|
||||
let value = if ret == 0 {
|
||||
Ok(CStr::from_ptr(msg).to_string_lossy().into_owned())
|
||||
} else {
|
||||
Err(CStr::from_ptr(msg).to_string_lossy().into_owned())
|
||||
};
|
||||
let _ = tx.send(value);
|
||||
}
|
||||
|
||||
async fn ffi_call_async<F>(f: F) -> Result<String, String>
|
||||
where
|
||||
F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,
|
||||
{
|
||||
let rx = {
|
||||
let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();
|
||||
let raw = Box::into_raw(Box::new(tx)) as *mut c_void;
|
||||
let ret = f(on_result_async, raw);
|
||||
if ret == 2 {
|
||||
drop(unsafe {
|
||||
Box::from_raw(
|
||||
raw as *mut tokio::sync::oneshot::Sender<Result<String, String>>,
|
||||
)
|
||||
});
|
||||
return Err("RET_MISSING_CALLBACK (internal error)".into());
|
||||
}
|
||||
rx
|
||||
};
|
||||
rx.await.map_err(|_| "channel closed before callback fired".to_string())?
|
||||
}
|
||||
|
||||
/// High-level context for `NimTimer`.
|
||||
pub struct NimTimerCtx {
|
||||
ptr: *mut c_void,
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
unsafe impl Send for NimTimerCtx {}
|
||||
unsafe impl Sync for NimTimerCtx {}
|
||||
|
||||
impl NimTimerCtx {
|
||||
pub fn create(config: TimerConfig, timeout: Duration) -> Result<Self, String> {
|
||||
let config_json = serde_json::to_string(&config).map_err(|e| e.to_string())?;
|
||||
let config_c = CString::new(config_json).unwrap();
|
||||
let raw = ffi_call(timeout, |cb, ud| unsafe {
|
||||
ffi::nimtimer_create(config_c.as_ptr(), cb, ud)
|
||||
})?;
|
||||
let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;
|
||||
Ok(Self { ptr: addr as *mut c_void, timeout })
|
||||
}
|
||||
|
||||
pub async fn new_async(config: TimerConfig) -> Result<Self, String> {
|
||||
let config_json = serde_json::to_string(&config).map_err(|e| e.to_string())?;
|
||||
let config_c = CString::new(config_json).unwrap();
|
||||
let raw = ffi_call_async(move |cb, ud| unsafe {
|
||||
ffi::nimtimer_create(config_c.as_ptr(), cb, ud)
|
||||
}).await?;
|
||||
let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;
|
||||
Ok(Self { ptr: addr as *mut c_void, timeout: Duration::from_secs(30) })
|
||||
}
|
||||
|
||||
pub fn echo(&self, req: EchoRequest) -> Result<EchoResponse, String> {
|
||||
let req_json = serde_json::to_string(&req).map_err(|e| e.to_string())?;
|
||||
let req_c = CString::new(req_json).unwrap();
|
||||
let raw = ffi_call(self.timeout, |cb, ud| unsafe {
|
||||
ffi::nimtimer_echo(self.ptr, cb, ud, req_c.as_ptr())
|
||||
})?;
|
||||
serde_json::from_str::<EchoResponse>(&raw).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn echo_async(&self, req: EchoRequest) -> Result<EchoResponse, String> {
|
||||
let req_json = serde_json::to_string(&req).map_err(|e| e.to_string())?;
|
||||
let req_c = CString::new(req_json).unwrap();
|
||||
let ptr = self.ptr as usize;
|
||||
let raw = ffi_call_async(move |cb, ud| unsafe {
|
||||
ffi::nimtimer_echo(ptr as *mut c_void, cb, ud, req_c.as_ptr())
|
||||
}).await?;
|
||||
serde_json::from_str::<EchoResponse>(&raw).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn version(&self) -> Result<String, String> {
|
||||
let raw = ffi_call(self.timeout, |cb, ud| unsafe {
|
||||
ffi::nimtimer_version(self.ptr, cb, ud)
|
||||
})?;
|
||||
serde_json::from_str::<String>(&raw).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn version_async(&self) -> Result<String, String> {
|
||||
let ptr = self.ptr as usize;
|
||||
let raw = ffi_call_async(move |cb, ud| unsafe {
|
||||
ffi::nimtimer_version(ptr as *mut c_void, cb, ud)
|
||||
}).await?;
|
||||
serde_json::from_str::<String>(&raw).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn complex(&self, req: ComplexRequest) -> Result<ComplexResponse, String> {
|
||||
let req_json = serde_json::to_string(&req).map_err(|e| e.to_string())?;
|
||||
let req_c = CString::new(req_json).unwrap();
|
||||
let raw = ffi_call(self.timeout, |cb, ud| unsafe {
|
||||
ffi::nimtimer_complex(self.ptr, cb, ud, req_c.as_ptr())
|
||||
})?;
|
||||
serde_json::from_str::<ComplexResponse>(&raw).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub async fn complex_async(&self, req: ComplexRequest) -> Result<ComplexResponse, String> {
|
||||
let req_json = serde_json::to_string(&req).map_err(|e| e.to_string())?;
|
||||
let req_c = CString::new(req_json).unwrap();
|
||||
let ptr = self.ptr as usize;
|
||||
let raw = ffi_call_async(move |cb, ud| unsafe {
|
||||
ffi::nimtimer_complex(ptr as *mut c_void, cb, ud, req_c.as_ptr())
|
||||
}).await?;
|
||||
serde_json::from_str::<ComplexResponse>(&raw).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
}
|
||||
16
examples/nim_timer/rust_bindings/src/ffi.rs
Normal file
16
examples/nim_timer/rust_bindings/src/ffi.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use std::os::raw::{c_char, c_int, c_void};
|
||||
|
||||
pub type FfiCallback = unsafe extern "C" fn(
|
||||
ret: c_int,
|
||||
msg: *const c_char,
|
||||
len: usize,
|
||||
user_data: *mut c_void,
|
||||
);
|
||||
|
||||
#[link(name = "nimtimer")]
|
||||
extern "C" {
|
||||
pub fn nimtimer_create(config_json: *const c_char, callback: FfiCallback, user_data: *mut c_void) -> c_int;
|
||||
pub fn nimtimer_echo(ctx: *mut c_void, callback: FfiCallback, user_data: *mut c_void, req_json: *const c_char) -> c_int;
|
||||
pub fn nimtimer_version(ctx: *mut c_void, callback: FfiCallback, user_data: *mut c_void) -> c_int;
|
||||
pub fn nimtimer_complex(ctx: *mut c_void, callback: FfiCallback, user_data: *mut c_void, req_json: *const c_char) -> c_int;
|
||||
}
|
||||
5
examples/nim_timer/rust_bindings/src/lib.rs
Normal file
5
examples/nim_timer/rust_bindings/src/lib.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod ffi;
|
||||
mod types;
|
||||
mod api;
|
||||
pub use types::*;
|
||||
pub use api::*;
|
||||
37
examples/nim_timer/rust_bindings/src/types.rs
Normal file
37
examples/nim_timer/rust_bindings/src/types.rs
Normal file
@ -0,0 +1,37 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TimerConfig {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EchoRequest {
|
||||
pub message: String,
|
||||
#[serde(rename = "delayMs")]
|
||||
pub delay_ms: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EchoResponse {
|
||||
pub echoed: String,
|
||||
#[serde(rename = "timerName")]
|
||||
pub timer_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ComplexRequest {
|
||||
pub messages: Vec<EchoRequest>,
|
||||
pub tags: Vec<String>,
|
||||
pub note: Option<String>,
|
||||
pub retries: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ComplexResponse {
|
||||
pub summary: String,
|
||||
#[serde(rename = "itemCount")]
|
||||
pub item_count: i64,
|
||||
#[serde(rename = "hasNote")]
|
||||
pub has_note: bool,
|
||||
}
|
||||
144
examples/nim_timer/rust_client/Cargo.lock
generated
Normal file
144
examples/nim_timer/rust_client/Cargo.lock
generated
Normal file
@ -0,0 +1,144 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "nimtimer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nimtimer",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tokio-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
17
examples/nim_timer/rust_client/Cargo.toml
Normal file
17
examples/nim_timer/rust_client/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "rust_client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
nimtimer = { path = "../rust_bindings" }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[[bin]]
|
||||
name = "rust_client"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "tokio_client"
|
||||
path = "src/tokio_main.rs"
|
||||
43
examples/nim_timer/rust_client/README.md
Normal file
43
examples/nim_timer/rust_client/README.md
Normal file
@ -0,0 +1,43 @@
|
||||
# Rust Client Examples
|
||||
|
||||
## Purpose
|
||||
|
||||
This folder contains **example Rust applications** that demonstrate how to use the auto-generated `nimtimer` crate (from `../rust_bindings`).
|
||||
|
||||
## What's Included
|
||||
|
||||
Two executable examples:
|
||||
|
||||
- **`rust_client`** — Synchronous example
|
||||
- Shows basic synchronous calls to the Nim timer API
|
||||
- Uses blocking wait with condition variables
|
||||
- Source: `src/main.rs`
|
||||
|
||||
- **`tokio_client`** — Asynchronous example with Tokio runtime
|
||||
- Demonstrates the Tokio async runtime integration
|
||||
- Uses `spawn_blocking` to handle the blocking FFI callbacks on a separate thread pool
|
||||
- Source: `src/tokio_main.rs`
|
||||
|
||||
## Building
|
||||
|
||||
```sh
|
||||
cd examples/nim_timer/rust_client
|
||||
cargo build
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```sh
|
||||
# Sync example
|
||||
cargo run --bin rust_client
|
||||
|
||||
# Tokio async example
|
||||
cargo run --bin tokio_client
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The `nimtimer` crate is a **local dependency** (`path = "../rust_bindings"`)
|
||||
- It is **auto-generated** — do not manually edit it
|
||||
- These examples are **not** part of the generated output; they are hand-written to show usage patterns
|
||||
- To regenerate the `nimtimer` crate, run `nimble genbindings_rust` from the parent directory
|
||||
47
examples/nim_timer/rust_client/src/main.rs
Normal file
47
examples/nim_timer/rust_client/src/main.rs
Normal file
@ -0,0 +1,47 @@
|
||||
// Rust client for the nim_timer shared library built with nim-ffi + chronos.
|
||||
//
|
||||
// This file uses the generated `nimtimer` crate, which wraps all the raw FFI
|
||||
// boilerplate (extern "C" declarations, callback machinery, JSON encode/decode).
|
||||
//
|
||||
// To regenerate the `rust_bindings` crate:
|
||||
// nim c --mm:orc -d:chronicles_log_level=WARN --nimMainPrefix:libnimtimer \
|
||||
// -d:ffiGenBindings examples/nim_timer/nim_timer.nim
|
||||
use nimtimer::{EchoRequest, NimTimerCtx, TimerConfig};
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
let timeout = Duration::from_secs(5);
|
||||
|
||||
// ── 1. Create the timer service ────────────────────────────────────────
|
||||
let ctx = NimTimerCtx::create(TimerConfig { name: "demo".into() }, timeout)
|
||||
.expect("nimtimer_create failed");
|
||||
println!("[1] Context created");
|
||||
|
||||
// ── 2. Sync call: version ──────────────────────────────────────────────
|
||||
let version = ctx.version().expect("nimtimer_version failed");
|
||||
println!("[2] Version (sync call, callback fired inline): {version}");
|
||||
|
||||
// ── 3. Async call: echo (200 ms delay) ────────────────────────────────
|
||||
let echo = ctx
|
||||
.echo(EchoRequest {
|
||||
message: "hello from Rust".into(),
|
||||
delay_ms: 200,
|
||||
})
|
||||
.expect("nimtimer_echo failed");
|
||||
println!(
|
||||
"[3] Echo (async, 200 ms chronos delay): echoed={}, timerName={}",
|
||||
echo.echoed, echo.timer_name
|
||||
);
|
||||
|
||||
// ── 4. A second echo ──────────────────────────────────────────────────
|
||||
let echo2 = ctx
|
||||
.echo(EchoRequest {
|
||||
message: "second request".into(),
|
||||
delay_ms: 50,
|
||||
})
|
||||
.expect("second nimtimer_echo failed");
|
||||
println!("[4] Echo: echoed={}, timerName={}", echo2.echoed, echo2.timer_name);
|
||||
|
||||
println!("\nDone. The Nim FFI thread and watchdog are still running.");
|
||||
println!("(In a real app, call nimtimer_destroy to join them gracefully.)");
|
||||
}
|
||||
30
examples/nim_timer/rust_client/src/tokio_main.rs
Normal file
30
examples/nim_timer/rust_client/src/tokio_main.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use nimtimer::{EchoRequest, NimTimerCtx, TimerConfig};
|
||||
|
||||
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let ctx = NimTimerCtx::new_async(TimerConfig { name: "tokio-demo".into() }).await?;
|
||||
|
||||
let version = ctx.version_async().await?;
|
||||
println!("[1] Tokio runtime started");
|
||||
println!("[2] Version: {version}");
|
||||
|
||||
let echo1 = ctx
|
||||
.echo_async(EchoRequest {
|
||||
message: "hello from tokio".into(),
|
||||
delay_ms: 200,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let echo2 = ctx
|
||||
.echo_async(EchoRequest {
|
||||
message: "second tokio request".into(),
|
||||
delay_ms: 50,
|
||||
})
|
||||
.await?;
|
||||
|
||||
println!("[3] Echo 1: echoed={}, timerName={}", echo1.echoed, echo1.timer_name);
|
||||
println!("[4] Echo 2: echoed={}, timerName={}", echo2.echoed, echo2.timer_name);
|
||||
|
||||
println!("\nDone. Tokio runtime shut down.");
|
||||
Ok(())
|
||||
}
|
||||
4
ffi.nim
4
ffi.nim
@ -2,9 +2,9 @@ import std/[atomics, tables]
|
||||
import chronos, chronicles
|
||||
import
|
||||
ffi/internal/[ffi_library, ffi_macro],
|
||||
ffi/[alloc, ffi_types, ffi_context, ffi_thread_request]
|
||||
ffi/[alloc, ffi_types, ffi_context, ffi_thread_request, serial]
|
||||
|
||||
export atomics, tables
|
||||
export chronos, chronicles
|
||||
export
|
||||
atomics, alloc, ffi_library, ffi_macro, ffi_types, ffi_context, ffi_thread_request
|
||||
atomics, alloc, ffi_library, ffi_macro, ffi_types, ffi_context, ffi_thread_request, serial
|
||||
|
||||
22
ffi.nimble
22
ffi.nimble
@ -5,7 +5,7 @@ author = "Institute of Free Technology"
|
||||
description = "FFI framework with custom header generation"
|
||||
license = "MIT or Apache License 2.0"
|
||||
|
||||
packageName = "ffi"
|
||||
packageName = "ffi"
|
||||
|
||||
requires "nim >= 2.2.4"
|
||||
requires "chronos"
|
||||
@ -19,6 +19,7 @@ task buildffi, "Compile the library":
|
||||
|
||||
task test, "Run all tests":
|
||||
exec "nim c -r " & nimFlags & " tests/test_alloc.nim"
|
||||
exec "nim c -r " & nimFlags & " tests/test_serial.nim"
|
||||
exec "nim c -r " & nimFlags & " tests/test_ffi_context.nim"
|
||||
|
||||
task test_alloc, "Run alloc unit tests":
|
||||
@ -26,3 +27,22 @@ task test_alloc, "Run alloc unit tests":
|
||||
|
||||
task test_ffi, "Run FFI context integration tests":
|
||||
exec "nim c -r " & nimFlags & " tests/test_ffi_context.nim"
|
||||
|
||||
task test_serial, "Run serial unit tests":
|
||||
exec "nim c -r " & nimFlags & " tests/test_serial.nim"
|
||||
|
||||
task genbindings_rust, "Generate Rust bindings for the nim_timer example":
|
||||
exec "nim c " & nimFlags &
|
||||
" --app:lib --noMain --nimMainPrefix:libnimtimer" &
|
||||
" -d:ffiGenBindings -d:targetLang=rust" &
|
||||
" -d:ffiOutputDir=examples/nim_timer/rust_bindings" &
|
||||
" -d:ffiNimSrcRelPath=../nim_timer.nim" &
|
||||
" -o:/dev/null examples/nim_timer/nim_timer.nim"
|
||||
|
||||
task genbindings_cpp, "Generate C++ bindings for the nim_timer example":
|
||||
exec "nim c " & nimFlags &
|
||||
" --app:lib --noMain --nimMainPrefix:libnimtimer" &
|
||||
" -d:ffiGenBindings -d:targetLang=cpp" &
|
||||
" -d:ffiOutputDir=examples/nim_timer/cpp_bindings" &
|
||||
" -d:ffiNimSrcRelPath=../nim_timer.nim" &
|
||||
" -o:/dev/null examples/nim_timer/nim_timer.nim"
|
||||
|
||||
513
ffi/codegen/cpp.nim
Normal file
513
ffi/codegen/cpp.nim
Normal file
@ -0,0 +1,513 @@
|
||||
## C++ binding generator for the nim-ffi framework.
|
||||
## Generates a header-only C++ binding and CMakeLists.txt from compile-time FFI metadata.
|
||||
|
||||
import std/[os, strutils]
|
||||
import ./meta
|
||||
|
||||
proc genericInnerType(typeName, prefix: string): string =
|
||||
if typeName.startsWith(prefix) and typeName.endsWith("]"):
|
||||
let start = prefix.len
|
||||
let lastIndex = typeName.len - 2
|
||||
return typeName[start .. lastIndex]
|
||||
return ""
|
||||
|
||||
proc nimTypeToCpp*(typeName: string): string =
|
||||
let trimmed = typeName.strip()
|
||||
if trimmed.startsWith("ptr "):
|
||||
return "void*"
|
||||
else:
|
||||
let seqInner = genericInnerType(trimmed, "seq[")
|
||||
if seqInner.len > 0:
|
||||
return "std::vector<" & nimTypeToCpp(seqInner) & ">"
|
||||
let optionInner = genericInnerType(trimmed, "Option[")
|
||||
if optionInner.len > 0:
|
||||
return "std::optional<" & nimTypeToCpp(optionInner) & ">"
|
||||
let maybeInner = genericInnerType(trimmed, "Maybe[")
|
||||
if maybeInner.len > 0:
|
||||
return "std::optional<" & nimTypeToCpp(maybeInner) & ">"
|
||||
case trimmed
|
||||
of "string", "cstring": "std::string"
|
||||
of "int", "int64": "int64_t"
|
||||
of "int32": "int32_t"
|
||||
of "bool": "bool"
|
||||
of "float": "float"
|
||||
of "float64": "double"
|
||||
of "pointer": "void*"
|
||||
else: trimmed
|
||||
|
||||
proc stripLibPrefixCpp(procName, libName: string): string =
|
||||
let prefix = libName & "_"
|
||||
if procName.startsWith(prefix):
|
||||
return procName[prefix.len .. ^1]
|
||||
return procName
|
||||
|
||||
proc generateCppHeader*(
|
||||
procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string
|
||||
): string =
|
||||
var lines: seq[string] = @[]
|
||||
|
||||
# ── Includes ───────────────────────────────────────────────────────────────
|
||||
lines.add("#pragma once")
|
||||
lines.add("#include <string>")
|
||||
lines.add("#include <cstdint>")
|
||||
lines.add("#include <chrono>")
|
||||
lines.add("#include <stdexcept>")
|
||||
lines.add("#include <mutex>")
|
||||
lines.add("#include <condition_variable>")
|
||||
lines.add("#include <memory>")
|
||||
lines.add("#include <functional>")
|
||||
lines.add("#include <future>")
|
||||
lines.add("#include <vector>")
|
||||
lines.add("#include <optional>")
|
||||
lines.add("#include <nlohmann/json.hpp>")
|
||||
lines.add("")
|
||||
|
||||
# ── nlohmann optional<T> support ──────────────────────────────────────────
|
||||
lines.add("namespace nlohmann {")
|
||||
lines.add(" template<typename T>")
|
||||
lines.add(" void to_json(json& j, const std::optional<T>& opt) {")
|
||||
lines.add(" if (opt) j = *opt;")
|
||||
lines.add(" else j = nullptr;")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
lines.add(" template<typename T>")
|
||||
lines.add(" void from_json(const json& j, std::optional<T>& opt) {")
|
||||
lines.add(" if (j.is_null()) opt = std::nullopt;")
|
||||
lines.add(" else opt = j.get<T>();")
|
||||
lines.add(" }")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
# ── Types ──────────────────────────────────────────────────────────────────
|
||||
if types.len > 0:
|
||||
lines.add("// ============================================================")
|
||||
lines.add("// Types")
|
||||
lines.add("// ============================================================")
|
||||
lines.add("")
|
||||
for t in types:
|
||||
lines.add("struct $1 {" % [t.name])
|
||||
for f in t.fields:
|
||||
lines.add(" $1 $2;" % [nimTypeToCpp(f.typeName), f.name])
|
||||
lines.add("};")
|
||||
var fieldNames: seq[string] = @[]
|
||||
for f in t.fields:
|
||||
fieldNames.add(f.name)
|
||||
lines.add(
|
||||
"NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE($1, $2)" % [t.name, fieldNames.join(", ")]
|
||||
)
|
||||
lines.add("")
|
||||
|
||||
# ── C FFI declarations ─────────────────────────────────────────────────────
|
||||
lines.add("// ============================================================")
|
||||
lines.add("// C FFI declarations")
|
||||
lines.add("// ============================================================")
|
||||
lines.add("")
|
||||
lines.add("extern \"C\" {")
|
||||
lines.add(
|
||||
"typedef void (*FfiCallback)(int ret, const char* msg, size_t len, void* user_data);"
|
||||
)
|
||||
lines.add("")
|
||||
for p in procs:
|
||||
var params: seq[string] = @[]
|
||||
if p.kind == ffiFfiKind:
|
||||
params.add("void* ctx")
|
||||
params.add("FfiCallback callback")
|
||||
params.add("void* user_data")
|
||||
for ep in p.extraParams:
|
||||
params.add("const char* $1_json" % [ep.name])
|
||||
else: # ffiCtorKind
|
||||
for ep in p.extraParams:
|
||||
params.add("const char* $1_json" % [ep.name])
|
||||
params.add("FfiCallback callback")
|
||||
params.add("void* user_data")
|
||||
lines.add("int $1($2);" % [p.procName, params.join(", ")])
|
||||
# Destroy is a plain synchronous call — no callback needed
|
||||
lines.add("void $1_destroy(void* ctx);" % [libName])
|
||||
lines.add("} // extern \"C\"")
|
||||
lines.add("")
|
||||
|
||||
# ── Serialization helpers ──────────────────────────────────────────────────
|
||||
lines.add("template<typename T>")
|
||||
lines.add("inline std::string serializeFfiArg(const T& value) {")
|
||||
lines.add(" return nlohmann::json(value).dump();")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
lines.add("inline std::string serializeFfiArg(void* value) {")
|
||||
lines.add(" return std::to_string(reinterpret_cast<uintptr_t>(value));")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
# Wrap parse + get in a single try/catch so callers get a clear FFI error
|
||||
# rather than a raw nlohmann exception with an opaque JSON pointer message.
|
||||
lines.add("template<typename T>")
|
||||
lines.add("inline T deserializeFfiResult(const std::string& raw) {")
|
||||
lines.add(" try {")
|
||||
lines.add(" return nlohmann::json::parse(raw).get<T>();")
|
||||
lines.add(" } catch (const nlohmann::json::exception& e) {")
|
||||
lines.add(
|
||||
" throw std::runtime_error(std::string(\"FFI response deserialization failed: \") + e.what());"
|
||||
)
|
||||
lines.add(" }")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
lines.add("template<>")
|
||||
lines.add("inline void* deserializeFfiResult<void*>(const std::string& raw) {")
|
||||
lines.add(" try {")
|
||||
lines.add(
|
||||
" return reinterpret_cast<void*>(static_cast<uintptr_t>(std::stoull(raw)));"
|
||||
)
|
||||
lines.add(" } catch (const std::exception& e) {")
|
||||
lines.add(
|
||||
" throw std::runtime_error(std::string(\"FFI returned non-numeric address: \") + raw);"
|
||||
)
|
||||
lines.add(" }")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
# ── Call helper (anonymous namespace, header-only) ─────────────────────────
|
||||
lines.add("// ============================================================")
|
||||
lines.add("// Synchronous call helper (anonymous namespace, header-only)")
|
||||
lines.add("// ============================================================")
|
||||
lines.add("")
|
||||
lines.add("namespace {")
|
||||
lines.add("")
|
||||
lines.add("struct FfiCallState_ {")
|
||||
lines.add(" std::mutex mtx;")
|
||||
lines.add(" std::condition_variable cv;")
|
||||
lines.add(" bool done{false};")
|
||||
lines.add(" bool ok{false};")
|
||||
lines.add(" std::string msg;")
|
||||
lines.add("};")
|
||||
lines.add("")
|
||||
# user_data is a heap-allocated shared_ptr<FfiCallState_>.
|
||||
# Ownership: ffi_call_ holds one copy; this callback holds the other.
|
||||
# When ffi_call_ times out and returns before the callback fires, the
|
||||
# state stays alive (refcount 1) until Nim eventually calls back and
|
||||
# deletes cb_ref — eliminating the UAF that a stack-allocated state has.
|
||||
lines.add("inline void ffi_cb_(int ret, const char* msg, size_t /*len*/, void* ud) {")
|
||||
lines.add(" auto* sptr = static_cast<std::shared_ptr<FfiCallState_>*>(ud);")
|
||||
lines.add(" {")
|
||||
lines.add(" auto& s = **sptr;")
|
||||
lines.add(" std::lock_guard<std::mutex> lock(s.mtx);")
|
||||
lines.add(" s.ok = (ret == 0);")
|
||||
lines.add(" s.msg = msg ? std::string(msg) : std::string{};")
|
||||
lines.add(" s.done = true;")
|
||||
lines.add(" s.cv.notify_one();")
|
||||
lines.add(" }")
|
||||
lines.add(" delete sptr;")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
lines.add(
|
||||
"inline std::string ffi_call_(std::function<int(FfiCallback, void*)> f,"
|
||||
)
|
||||
lines.add(" std::chrono::milliseconds timeout) {")
|
||||
lines.add(" auto state = std::make_shared<FfiCallState_>();")
|
||||
lines.add(" auto* cb_ref = new std::shared_ptr<FfiCallState_>(state);")
|
||||
lines.add(" const int ret = f(ffi_cb_, cb_ref);")
|
||||
lines.add(" if (ret == 2) {")
|
||||
lines.add(" delete cb_ref;")
|
||||
lines.add(
|
||||
" throw std::runtime_error(\"RET_MISSING_CALLBACK (internal error)\");"
|
||||
)
|
||||
lines.add(" }")
|
||||
lines.add(" std::unique_lock<std::mutex> lock(state->mtx);")
|
||||
lines.add(
|
||||
" const bool fired = state->cv.wait_for(lock, timeout, [&]{ return state->done; });"
|
||||
)
|
||||
lines.add(" if (!fired)")
|
||||
lines.add(
|
||||
" throw std::runtime_error(\"FFI call timed out after \" + std::to_string(timeout.count()) + \"ms\");"
|
||||
)
|
||||
lines.add(" if (!state->ok)")
|
||||
lines.add(" throw std::runtime_error(state->msg);")
|
||||
lines.add(" return state->msg;")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
lines.add("} // anonymous namespace")
|
||||
lines.add("")
|
||||
|
||||
# ── High-level C++ context class ──────────────────────────────────────────
|
||||
var ctors: seq[FFIProcMeta] = @[]
|
||||
var methods: seq[FFIProcMeta] = @[]
|
||||
for p in procs:
|
||||
if p.kind == ffiCtorKind: ctors.add(p)
|
||||
else: methods.add(p)
|
||||
|
||||
let libTypeName =
|
||||
if ctors.len > 0: ctors[0].libTypeName
|
||||
else: libName[0 .. 0].toUpperAscii() & libName[1 .. ^1]
|
||||
|
||||
let ctxTypeName = libTypeName & "Ctx"
|
||||
|
||||
lines.add("// ============================================================")
|
||||
lines.add("// High-level C++ context class")
|
||||
lines.add("// ============================================================")
|
||||
lines.add("")
|
||||
lines.add("class $1 {" % [ctxTypeName])
|
||||
lines.add("public:")
|
||||
|
||||
# ── Constructors ────────────────────────────────────────────────────────
|
||||
for ctor in ctors:
|
||||
var ctorParams: seq[string] = @[]
|
||||
var epNames: seq[string] = @[]
|
||||
for ep in ctor.extraParams:
|
||||
ctorParams.add("const $1& $2" % [nimTypeToCpp(ep.typeName), ep.name])
|
||||
epNames.add(ep.name)
|
||||
let timeoutParam = "std::chrono::milliseconds timeout = std::chrono::seconds{30}"
|
||||
let ctorParamsWithTimeout =
|
||||
if ctorParams.len > 0: ctorParams.join(", ") & ", " & timeoutParam
|
||||
else: timeoutParam
|
||||
|
||||
# -- create() factory --
|
||||
lines.add(" static $1 create($2) {" % [ctxTypeName, ctorParamsWithTimeout])
|
||||
for ep in ctor.extraParams:
|
||||
lines.add(" const auto $1_json = serializeFfiArg($1);" % [ep.name])
|
||||
var callArgs: seq[string] = @[]
|
||||
for ep in ctor.extraParams:
|
||||
callArgs.add("$1_json.c_str()" % [ep.name])
|
||||
callArgs.add("cb")
|
||||
callArgs.add("ud")
|
||||
lines.add(" const auto raw = ffi_call_([&](FfiCallback cb, void* ud) {")
|
||||
lines.add(" return $1($2);" % [ctor.procName, callArgs.join(", ")])
|
||||
lines.add(" }, timeout);")
|
||||
lines.add(" try {")
|
||||
lines.add(" const auto addr = std::stoull(raw);")
|
||||
lines.add(
|
||||
" return $1(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)), timeout);" %
|
||||
[ctxTypeName]
|
||||
)
|
||||
lines.add(" } catch (const std::exception&) {")
|
||||
lines.add(
|
||||
" throw std::runtime_error(\"FFI create returned non-numeric address: \" + raw);"
|
||||
)
|
||||
lines.add(" }")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
# -- createAsync() factory: uses actual param types, not hardcoded --
|
||||
let captureList =
|
||||
if epNames.len > 0: epNames.join(", ") & ", timeout"
|
||||
else: "timeout"
|
||||
let callList =
|
||||
if epNames.len > 0: epNames.join(", ") & ", timeout"
|
||||
else: "timeout"
|
||||
lines.add(
|
||||
" static std::future<$1> createAsync($2) {" %
|
||||
[ctxTypeName, ctorParamsWithTimeout]
|
||||
)
|
||||
lines.add(
|
||||
" return std::async(std::launch::async, [$1]() { return create($2); });" %
|
||||
[captureList, callList]
|
||||
)
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
# ── Rule of 5 ──────────────────────────────────────────────────────────
|
||||
# Destructor tears down Nim threads; copies are deleted; moves transfer ownership.
|
||||
lines.add(" ~$1() {" % [ctxTypeName])
|
||||
lines.add(" if (ptr_) {")
|
||||
lines.add(" $1_destroy(ptr_);" % [libName])
|
||||
lines.add(" ptr_ = nullptr;")
|
||||
lines.add(" }")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
lines.add(" $1(const $1&) = delete;" % [ctxTypeName])
|
||||
lines.add(" $1& operator=(const $1&) = delete;" % [ctxTypeName])
|
||||
lines.add("")
|
||||
lines.add(
|
||||
" $1($1&& other) noexcept : ptr_(other.ptr_), timeout_(other.timeout_) {" %
|
||||
[ctxTypeName]
|
||||
)
|
||||
lines.add(" other.ptr_ = nullptr;")
|
||||
lines.add(" }")
|
||||
lines.add(" $1& operator=($1&& other) noexcept {" % [ctxTypeName])
|
||||
lines.add(" if (this != &other) {")
|
||||
lines.add(" if (ptr_) $1_destroy(ptr_);" % [libName])
|
||||
lines.add(" ptr_ = other.ptr_;")
|
||||
lines.add(" timeout_ = other.timeout_;")
|
||||
lines.add(" other.ptr_ = nullptr;")
|
||||
lines.add(" }")
|
||||
lines.add(" return *this;")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
# ── Instance methods ────────────────────────────────────────────────────
|
||||
for m in methods:
|
||||
let methodName = stripLibPrefixCpp(m.procName, libName)
|
||||
let retCppType = nimTypeToCpp(m.returnTypeName)
|
||||
|
||||
var methParams: seq[string] = @[]
|
||||
var methParamNames: seq[string] = @[]
|
||||
for ep in m.extraParams:
|
||||
methParams.add("const $1& $2" % [nimTypeToCpp(ep.typeName), ep.name])
|
||||
methParamNames.add(ep.name)
|
||||
let methParamsStr = methParams.join(", ")
|
||||
let methParamNamesStr = methParamNames.join(", ")
|
||||
|
||||
lines.add(" $1 $2($3) const {" % [retCppType, methodName, methParamsStr])
|
||||
for ep in m.extraParams:
|
||||
lines.add(" const auto $1_json = serializeFfiArg($1);" % [ep.name])
|
||||
var callArgs = @["ptr_", "cb", "ud"]
|
||||
for ep in m.extraParams:
|
||||
callArgs.add("$1_json.c_str()" % [ep.name])
|
||||
lines.add(" const auto raw = ffi_call_([&](FfiCallback cb, void* ud) {")
|
||||
lines.add(" return $1($2);" % [m.procName, callArgs.join(", ")])
|
||||
lines.add(" }, timeout_);")
|
||||
if retCppType == "void*":
|
||||
lines.add(" return deserializeFfiResult<void*>(raw);")
|
||||
else:
|
||||
lines.add(" return deserializeFfiResult<$1>(raw);" % [retCppType])
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
if methParamsStr.len > 0:
|
||||
lines.add(
|
||||
" std::future<$1> $2Async($3) const {" %
|
||||
[retCppType, methodName, methParamsStr]
|
||||
)
|
||||
lines.add(
|
||||
" return std::async(std::launch::async, [this, $1]() { return $2($3); });" %
|
||||
[methParamNamesStr, methodName, methParamNamesStr]
|
||||
)
|
||||
lines.add(" }")
|
||||
else:
|
||||
lines.add(" std::future<$1> $2Async() const {" % [retCppType, methodName])
|
||||
lines.add(
|
||||
" return std::async(std::launch::async, [this]() { return $1(); });" %
|
||||
[methodName]
|
||||
)
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
lines.add("private:")
|
||||
lines.add(" void* ptr_;")
|
||||
lines.add(" std::chrono::milliseconds timeout_;")
|
||||
lines.add(
|
||||
" explicit $1(void* p, std::chrono::milliseconds t) : ptr_(p), timeout_(t) {}" %
|
||||
[ctxTypeName]
|
||||
)
|
||||
lines.add("};")
|
||||
lines.add("")
|
||||
|
||||
result = lines.join("\n")
|
||||
|
||||
proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string =
|
||||
## Generates CMakeLists.txt for the C++ bindings directory.
|
||||
## CMake uses ${...} which would clash with Nim's % format operator,
|
||||
## so we build the file line by line using string concatenation.
|
||||
let src = nimSrcRelPath.replace("\\", "/")
|
||||
let cv = "${CMAKE_CURRENT_SOURCE_DIR}" # CMake variable shorthand
|
||||
let rv = "${REPO_ROOT}"
|
||||
let lf = "${NIM_LIB_FILE}"
|
||||
let nm = "${NIM_EXECUTABLE}"
|
||||
let ns = "${NIM_SRC}"
|
||||
let sd = "${_search_dir}"
|
||||
var L: seq[string] = @[]
|
||||
L.add("cmake_minimum_required(VERSION 3.14)")
|
||||
L.add("project(" & libName & "_cpp_bindings CXX)")
|
||||
L.add("")
|
||||
L.add("set(CMAKE_CXX_STANDARD 17)")
|
||||
L.add("set(CMAKE_CXX_STANDARD_REQUIRED ON)")
|
||||
L.add("")
|
||||
L.add(
|
||||
"# ── nlohmann/json ─────────────────────────────────────────────────────────────"
|
||||
)
|
||||
L.add("include(FetchContent)")
|
||||
L.add("FetchContent_Declare(")
|
||||
L.add(" nlohmann_json")
|
||||
L.add(" GIT_REPOSITORY https://github.com/nlohmann/json.git")
|
||||
L.add(" GIT_TAG v3.11.3")
|
||||
L.add(" GIT_SHALLOW TRUE")
|
||||
L.add(")")
|
||||
L.add("FetchContent_MakeAvailable(nlohmann_json)")
|
||||
L.add("")
|
||||
L.add(
|
||||
"# ── Locate the repository root (contains ffi.nimble) ─────────────────────────"
|
||||
)
|
||||
L.add("set(_search_dir \"" & cv & "\")")
|
||||
L.add("set(REPO_ROOT \"\")")
|
||||
L.add("foreach(_i RANGE 10)")
|
||||
L.add(" if(EXISTS \"" & sd & "/ffi.nimble\")")
|
||||
L.add(" set(REPO_ROOT \"" & sd & "\")")
|
||||
L.add(" break()")
|
||||
L.add(" endif()")
|
||||
L.add(" get_filename_component(_search_dir \"" & sd & "\" DIRECTORY)")
|
||||
L.add("endforeach()")
|
||||
L.add("if(\"${REPO_ROOT}\" STREQUAL \"\")")
|
||||
L.add(
|
||||
" message(FATAL_ERROR \"Cannot find repo root (no ffi.nimble in any ancestor)\")"
|
||||
)
|
||||
L.add("endif()")
|
||||
L.add("")
|
||||
L.add(
|
||||
"# ── Nim source path ───────────────────────────────────────────────────────────"
|
||||
)
|
||||
L.add("get_filename_component(NIM_SRC")
|
||||
L.add(" \"" & cv & "/" & src & "\"")
|
||||
L.add(" ABSOLUTE)")
|
||||
L.add("")
|
||||
L.add(
|
||||
"# ── Compile the Nim shared library ───────────────────────────────────────────"
|
||||
)
|
||||
L.add("find_program(NIM_EXECUTABLE nim REQUIRED)")
|
||||
L.add("")
|
||||
L.add("if(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")")
|
||||
L.add(" set(NIM_LIB_FILE \"" & rv & "/lib" & libName & ".dylib\")")
|
||||
L.add("elseif(CMAKE_SYSTEM_NAME STREQUAL \"Windows\")")
|
||||
L.add(" set(NIM_LIB_FILE \"" & rv & "/" & libName & ".dll\")")
|
||||
L.add("else()")
|
||||
L.add(" set(NIM_LIB_FILE \"" & rv & "/lib" & libName & ".so\")")
|
||||
L.add("endif()")
|
||||
L.add("")
|
||||
L.add("add_custom_command(")
|
||||
L.add(" OUTPUT \"" & lf & "\"")
|
||||
L.add(" COMMAND \"" & nm & "\" c")
|
||||
L.add(" --mm:orc")
|
||||
L.add(" -d:chronicles_log_level=WARN")
|
||||
L.add(" --app:lib")
|
||||
L.add(" --noMain")
|
||||
L.add(" \"--nimMainPrefix:lib" & libName & "\"")
|
||||
L.add(" \"-o:" & lf & "\"")
|
||||
L.add(" \"" & ns & "\"")
|
||||
L.add(" WORKING_DIRECTORY \"" & rv & "\"")
|
||||
L.add(" DEPENDS \"" & ns & "\"")
|
||||
L.add(" COMMENT \"Compiling Nim library lib" & libName & "\"")
|
||||
L.add(" VERBATIM")
|
||||
L.add(")")
|
||||
L.add("add_custom_target(nim_lib ALL DEPENDS \"" & lf & "\")")
|
||||
L.add("")
|
||||
L.add("add_library(" & libName & " SHARED IMPORTED GLOBAL)")
|
||||
L.add(
|
||||
"set_target_properties(" & libName & " PROPERTIES IMPORTED_LOCATION \"" & lf & "\")"
|
||||
)
|
||||
L.add("add_dependencies(" & libName & " nim_lib)")
|
||||
L.add("")
|
||||
L.add(
|
||||
"# ── Interface target exposing the generated header ────────────────────────────"
|
||||
)
|
||||
L.add("add_library(" & libName & "_headers INTERFACE)")
|
||||
L.add("target_include_directories(" & libName & "_headers INTERFACE \"" & cv & "\")")
|
||||
L.add(
|
||||
"target_link_libraries(" & libName & "_headers INTERFACE " & libName &
|
||||
" nlohmann_json::nlohmann_json)"
|
||||
)
|
||||
L.add("")
|
||||
L.add(
|
||||
"# ── Optional example executable ───────────────────────────────────────────────"
|
||||
)
|
||||
L.add("if(EXISTS \"" & cv & "/main.cpp\")")
|
||||
L.add(" add_executable(example main.cpp)")
|
||||
L.add(" target_link_libraries(example PRIVATE " & libName & "_headers)")
|
||||
L.add(" add_dependencies(example nim_lib)")
|
||||
L.add("endif()")
|
||||
L.add("")
|
||||
result = L.join("\n")
|
||||
|
||||
proc generateCppBindings*(
|
||||
procs: seq[FFIProcMeta],
|
||||
types: seq[FFITypeMeta],
|
||||
libName: string,
|
||||
outputDir: string,
|
||||
nimSrcRelPath: string,
|
||||
) =
|
||||
createDir(outputDir)
|
||||
writeFile(outputDir / (libName & ".hpp"), generateCppHeader(procs, types, libName))
|
||||
writeFile(outputDir / "CMakeLists.txt", generateCppCMakeLists(libName, nimSrcRelPath))
|
||||
45
ffi/codegen/meta.nim
Normal file
45
ffi/codegen/meta.nim
Normal file
@ -0,0 +1,45 @@
|
||||
## Compile-time metadata types for FFI binding generation.
|
||||
## Populated by the {.ffiCtor.} and {.ffi.} macros and consumed by codegen.
|
||||
|
||||
type
|
||||
FFIParamMeta* = object
|
||||
name*: string # Nim param name, e.g. "req"
|
||||
typeName*: string # Nim type name, e.g. "EchoRequest"
|
||||
isPtr*: bool # true if the type is `ptr T`
|
||||
|
||||
FFIProcKind* = enum
|
||||
ffiCtorKind
|
||||
ffiFfiKind
|
||||
|
||||
FFIProcMeta* = object
|
||||
procName*: string # e.g. "nimtimer_echo"
|
||||
libName*: string # library name, e.g. "nimtimer"
|
||||
kind*: FFIProcKind
|
||||
libTypeName*: string # e.g. "NimTimer"
|
||||
extraParams*: seq[FFIParamMeta] # all params except the lib param
|
||||
returnTypeName*: string # e.g. "EchoResponse", "string", "pointer"
|
||||
returnIsPtr*: bool # true if return type is ptr T
|
||||
isAsync*: bool
|
||||
|
||||
FFIFieldMeta* = object
|
||||
name*: string # e.g. "delayMs"
|
||||
typeName*: string # e.g. "int"
|
||||
|
||||
FFITypeMeta* = object
|
||||
name*: string
|
||||
fields*: seq[FFIFieldMeta]
|
||||
|
||||
# Compile-time registries populated by the macros
|
||||
var ffiProcRegistry* {.compileTime.}: seq[FFIProcMeta]
|
||||
var ffiTypeRegistry* {.compileTime.}: seq[FFITypeMeta]
|
||||
var currentLibName* {.compileTime.}: string
|
||||
|
||||
# Target language for binding generation; override with -d:targetLang=cpp
|
||||
const targetLang* {.strdefine.} = "rust"
|
||||
|
||||
# Output directory for generated bindings; set with -d:ffiOutputDir=path/to/dir
|
||||
const ffiOutputDir* {.strdefine.} = ""
|
||||
|
||||
# Nim source path (relative to outputDir) embedded in generated build files;
|
||||
# set with -d:ffiNimSrcRelPath=../relative/path.nim
|
||||
const ffiNimSrcRelPath* {.strdefine.} = ""
|
||||
507
ffi/codegen/rust.nim
Normal file
507
ffi/codegen/rust.nim
Normal file
@ -0,0 +1,507 @@
|
||||
## Rust binding generator for the nim-ffi framework.
|
||||
## Generates a complete Rust crate from compile-time FFI metadata.
|
||||
|
||||
import std/[os, strutils]
|
||||
import ./meta
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Name conversion helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc toSnakeCase*(s: string): string =
|
||||
## Converts camelCase to snake_case.
|
||||
## e.g. "delayMs" → "delay_ms", "timerName" → "timer_name"
|
||||
result = ""
|
||||
for i, c in s:
|
||||
if c.isUpperAscii() and i > 0:
|
||||
result.add('_')
|
||||
result.add(c.toLowerAscii())
|
||||
|
||||
proc toPascalCase*(s: string): string =
|
||||
## Converts the first letter to uppercase.
|
||||
if s.len == 0:
|
||||
return s
|
||||
result = s
|
||||
result[0] = s[0].toUpperAscii()
|
||||
|
||||
proc nimTypeToRust*(typeName: string): string =
|
||||
## Maps Nim type names to Rust type names, including generics.
|
||||
let t = typeName.strip()
|
||||
if t.startsWith("seq[") and t.endsWith("]"):
|
||||
return "Vec<" & nimTypeToRust(t[4 .. ^2]) & ">"
|
||||
if t.startsWith("Option[") and t.endsWith("]"):
|
||||
return "Option<" & nimTypeToRust(t[7 .. ^2]) & ">"
|
||||
if t.startsWith("Maybe[") and t.endsWith("]"):
|
||||
return "Option<" & nimTypeToRust(t[6 .. ^2]) & ">"
|
||||
case t
|
||||
of "string", "cstring": "String"
|
||||
of "int", "int64": "i64"
|
||||
of "int32": "i32"
|
||||
of "bool": "bool"
|
||||
of "float", "float64": "f64"
|
||||
of "pointer": "usize"
|
||||
else: toPascalCase(t)
|
||||
|
||||
proc deriveLibName*(procs: seq[FFIProcMeta]): string =
|
||||
## Extracts the common prefix before the first `_` from proc names.
|
||||
## e.g. ["nimtimer_create", "nimtimer_echo"] → "nimtimer"
|
||||
if currentLibName.len > 0:
|
||||
return currentLibName
|
||||
if procs.len == 0:
|
||||
return "unknown"
|
||||
let first = procs[0].procName
|
||||
let parts = first.split('_')
|
||||
if parts.len > 0:
|
||||
return parts[0]
|
||||
return "unknown"
|
||||
|
||||
proc stripLibPrefix*(procName: string, libName: string): string =
|
||||
## Strips the library prefix from a proc name.
|
||||
## e.g. "nimtimer_echo", "nimtimer" → "echo"
|
||||
let prefix = libName & "_"
|
||||
if procName.startsWith(prefix):
|
||||
return procName[prefix.len .. ^1]
|
||||
return procName
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# File generators
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
proc generateCargoToml*(libName: string): string =
|
||||
result =
|
||||
"""[package]
|
||||
name = "$1"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["sync"] }
|
||||
""" %
|
||||
[libName]
|
||||
|
||||
proc generateBuildRs*(libName: string, nimSrcRelPath: string): string =
|
||||
## Generates build.rs that compiles the Nim library.
|
||||
## nimSrcRelPath is relative to the output (crate) directory.
|
||||
let escapedSrc = nimSrcRelPath.replace("\\", "\\\\")
|
||||
result =
|
||||
"""use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
let nim_src = manifest.join("$1");
|
||||
let nim_src = nim_src.canonicalize().unwrap_or(manifest.join("$1"));
|
||||
|
||||
// Walk up to find the nim-ffi repo root (directory containing nim_src's library)
|
||||
// The repo root is where nim c should be run from (contains config.nims).
|
||||
// We assume nim_src lives somewhere under repo_root.
|
||||
// Derive repo_root as the ancestor that contains the .nimble file or config.nims.
|
||||
let mut repo_root = nim_src.clone();
|
||||
loop {
|
||||
repo_root = match repo_root.parent() {
|
||||
Some(p) => p.to_path_buf(),
|
||||
None => break,
|
||||
};
|
||||
if repo_root.join("config.nims").exists() || repo_root.join("ffi.nimble").exists() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let lib_ext = "dylib";
|
||||
#[cfg(target_os = "linux")]
|
||||
let lib_ext = "so";
|
||||
|
||||
let out_lib = repo_root.join(format!("lib$2.{lib_ext}"));
|
||||
|
||||
let mut cmd = Command::new("nim");
|
||||
cmd.arg("c")
|
||||
.arg("--mm:orc")
|
||||
.arg("-d:chronicles_log_level=WARN")
|
||||
.arg("--app:lib")
|
||||
.arg("--noMain")
|
||||
.arg(format!("--nimMainPrefix:lib$2"))
|
||||
.arg(format!("-o:{}", out_lib.display()));
|
||||
cmd.arg(&nim_src).current_dir(&repo_root);
|
||||
|
||||
let status = cmd.status().expect("failed to run nim compiler");
|
||||
assert!(status.success(), "Nim compilation failed");
|
||||
|
||||
println!("cargo:rustc-link-search={}", repo_root.display());
|
||||
println!("cargo:rustc-link-lib=$2");
|
||||
println!("cargo:rerun-if-changed={}", nim_src.display());
|
||||
}
|
||||
""" %
|
||||
[escapedSrc, libName]
|
||||
|
||||
proc generateLibRs*(): string =
|
||||
result = """mod ffi;
|
||||
mod types;
|
||||
mod api;
|
||||
pub use types::*;
|
||||
pub use api::*;
|
||||
"""
|
||||
|
||||
proc generateFfiRs*(procs: seq[FFIProcMeta]): string =
|
||||
## Generates ffi.rs with extern "C" declarations for all procs.
|
||||
var lines: seq[string] = @[]
|
||||
lines.add("use std::os::raw::{c_char, c_int, c_void};")
|
||||
lines.add("")
|
||||
lines.add("pub type FfiCallback = unsafe extern \"C\" fn(")
|
||||
lines.add(" ret: c_int,")
|
||||
lines.add(" msg: *const c_char,")
|
||||
lines.add(" len: usize,")
|
||||
lines.add(" user_data: *mut c_void,")
|
||||
lines.add(");")
|
||||
lines.add("")
|
||||
|
||||
# Collect unique lib names for #[link(...)]
|
||||
var libNames: seq[string] = @[]
|
||||
for p in procs:
|
||||
if p.libName notin libNames:
|
||||
libNames.add(p.libName)
|
||||
|
||||
# Derive lib name from proc names if not set
|
||||
var linkLibName = ""
|
||||
if libNames.len > 0 and libNames[0].len > 0:
|
||||
linkLibName = libNames[0]
|
||||
else:
|
||||
# derive from first proc name
|
||||
if procs.len > 0:
|
||||
let parts = procs[0].procName.split('_')
|
||||
if parts.len > 0:
|
||||
linkLibName = parts[0]
|
||||
|
||||
lines.add("#[link(name = \"$1\")]" % [linkLibName])
|
||||
lines.add("extern \"C\" {")
|
||||
|
||||
for p in procs:
|
||||
var params: seq[string] = @[]
|
||||
if p.kind == ffiFfiKind:
|
||||
# Method: ctx comes first
|
||||
params.add("ctx: *mut c_void")
|
||||
params.add("callback: FfiCallback")
|
||||
params.add("user_data: *mut c_void")
|
||||
for ep in p.extraParams:
|
||||
params.add("$1_json: *const c_char" % [toSnakeCase(ep.name)])
|
||||
else:
|
||||
# Constructor: no ctx
|
||||
for ep in p.extraParams:
|
||||
params.add("$1_json: *const c_char" % [toSnakeCase(ep.name)])
|
||||
params.add("callback: FfiCallback")
|
||||
params.add("user_data: *mut c_void")
|
||||
|
||||
let paramStr = params.join(", ")
|
||||
lines.add(" pub fn $1($2) -> c_int;" % [p.procName, paramStr])
|
||||
|
||||
lines.add("}")
|
||||
result = lines.join("\n") & "\n"
|
||||
|
||||
proc generateTypesRs*(types: seq[FFITypeMeta]): string =
|
||||
## Generates types.rs with Rust structs for all FFI types.
|
||||
var lines: seq[string] = @[]
|
||||
lines.add("use serde::{Deserialize, Serialize};")
|
||||
lines.add("")
|
||||
|
||||
for t in types:
|
||||
lines.add("#[derive(Debug, Clone, Serialize, Deserialize)]")
|
||||
lines.add("pub struct $1 {" % [t.name])
|
||||
for f in t.fields:
|
||||
let snakeName = toSnakeCase(f.name)
|
||||
let rustType = nimTypeToRust(f.typeName)
|
||||
# Add serde rename if camelCase name differs from snake_case
|
||||
if snakeName != f.name:
|
||||
lines.add(" #[serde(rename = \"$1\")]" % [f.name])
|
||||
lines.add(" pub $1: $2," % [snakeName, rustType])
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
result = lines.join("\n")
|
||||
|
||||
proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string =
|
||||
## Generates api.rs with both a blocking and a tokio-async high-level API.
|
||||
##
|
||||
## Blocking: ctx.echo(req) — thread-blocks via Condvar
|
||||
## Async: ctx.echo_async(req).await — non-blocking via oneshot channel;
|
||||
## the FFI callback fires from the Nim/chronos thread and wakes
|
||||
## the awaiting task without ever blocking a thread.
|
||||
var lines: seq[string] = @[]
|
||||
|
||||
var ctors: seq[FFIProcMeta] = @[]
|
||||
var methods: seq[FFIProcMeta] = @[]
|
||||
for p in procs:
|
||||
if p.kind == ffiCtorKind: ctors.add(p)
|
||||
else: methods.add(p)
|
||||
|
||||
var libTypeName = ""
|
||||
if ctors.len > 0: libTypeName = ctors[0].libTypeName
|
||||
else: libTypeName = toPascalCase(libName)
|
||||
|
||||
let ctxTypeName = libTypeName & "Ctx"
|
||||
|
||||
# ── Imports ────────────────────────────────────────────────────────────────
|
||||
lines.add("use std::ffi::{CStr, CString};")
|
||||
lines.add("use std::os::raw::{c_char, c_int, c_void};")
|
||||
lines.add("use std::sync::{Arc, Condvar, Mutex};")
|
||||
lines.add("use std::time::Duration;")
|
||||
lines.add("use super::ffi;")
|
||||
lines.add("use super::types::*;")
|
||||
lines.add("")
|
||||
|
||||
# ── Blocking trampoline ────────────────────────────────────────────────────
|
||||
lines.add("#[derive(Default)]")
|
||||
lines.add("struct FfiCallbackResult {")
|
||||
lines.add(" payload: Option<Result<String, String>>,")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
lines.add("type Pair = Arc<(Mutex<FfiCallbackResult>, Condvar)>;")
|
||||
lines.add("")
|
||||
lines.add("unsafe extern \"C\" fn on_result(")
|
||||
lines.add(" ret: c_int,")
|
||||
lines.add(" msg: *const c_char,")
|
||||
lines.add(" _len: usize,")
|
||||
lines.add(" user_data: *mut c_void,")
|
||||
lines.add(") {")
|
||||
lines.add(" let pair = Arc::from_raw(user_data as *const (Mutex<FfiCallbackResult>, Condvar));")
|
||||
lines.add(" {")
|
||||
lines.add(" let (lock, cvar) = &*pair;")
|
||||
lines.add(" let mut state = lock.lock().unwrap();")
|
||||
lines.add(" state.payload = Some(if ret == 0 {")
|
||||
lines.add(" Ok(CStr::from_ptr(msg).to_string_lossy().into_owned())")
|
||||
lines.add(" } else {")
|
||||
lines.add(" Err(CStr::from_ptr(msg).to_string_lossy().into_owned())")
|
||||
lines.add(" });")
|
||||
lines.add(" cvar.notify_one();")
|
||||
lines.add(" }")
|
||||
lines.add(" std::mem::forget(pair);")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
lines.add("fn ffi_call<F>(timeout: Duration, f: F) -> Result<String, String>")
|
||||
lines.add("where")
|
||||
lines.add(" F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,")
|
||||
lines.add("{")
|
||||
lines.add(" let pair: Pair = Arc::new((Mutex::new(FfiCallbackResult::default()), Condvar::new()));")
|
||||
lines.add(" let raw = Arc::into_raw(pair.clone()) as *mut c_void;")
|
||||
lines.add(" let ret = f(on_result, raw);")
|
||||
lines.add(" if ret == 2 {")
|
||||
lines.add(" return Err(\"RET_MISSING_CALLBACK (internal error)\".into());")
|
||||
lines.add(" }")
|
||||
lines.add(" let (lock, cvar) = &*pair;")
|
||||
lines.add(" let guard = lock.lock().unwrap();")
|
||||
lines.add(" let (guard, timed_out) = cvar")
|
||||
lines.add(" .wait_timeout_while(guard, timeout, |s| s.payload.is_none())")
|
||||
lines.add(" .unwrap();")
|
||||
lines.add(" if timed_out.timed_out() {")
|
||||
lines.add(" return Err(format!(\"timed out after {:?}\", timeout));")
|
||||
lines.add(" }")
|
||||
lines.add(" guard.payload.clone().unwrap()")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
# ── Async (tokio oneshot) trampoline ───────────────────────────────────────
|
||||
# The callback is invoked from the Nim/chronos thread and sends the result
|
||||
# through the oneshot channel, waking the awaiting tokio task without
|
||||
# blocking any thread.
|
||||
lines.add("unsafe extern \"C\" fn on_result_async(")
|
||||
lines.add(" ret: c_int,")
|
||||
lines.add(" msg: *const c_char,")
|
||||
lines.add(" _len: usize,")
|
||||
lines.add(" user_data: *mut c_void,")
|
||||
lines.add(") {")
|
||||
lines.add(" let tx = Box::from_raw(")
|
||||
lines.add(" user_data as *mut tokio::sync::oneshot::Sender<Result<String, String>>,")
|
||||
lines.add(" );")
|
||||
lines.add(" let value = if ret == 0 {")
|
||||
lines.add(" Ok(CStr::from_ptr(msg).to_string_lossy().into_owned())")
|
||||
lines.add(" } else {")
|
||||
lines.add(" Err(CStr::from_ptr(msg).to_string_lossy().into_owned())")
|
||||
lines.add(" };")
|
||||
lines.add(" let _ = tx.send(value);")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
# Scoped block keeps raw/tx/F dead at the single await point so the
|
||||
# returned future is Send regardless of whether F itself is Send.
|
||||
lines.add("async fn ffi_call_async<F>(f: F) -> Result<String, String>")
|
||||
lines.add("where")
|
||||
lines.add(" F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,")
|
||||
lines.add("{")
|
||||
lines.add(" let rx = {")
|
||||
lines.add(" let (tx, rx) = tokio::sync::oneshot::channel::<Result<String, String>>();")
|
||||
lines.add(" let raw = Box::into_raw(Box::new(tx)) as *mut c_void;")
|
||||
lines.add(" let ret = f(on_result_async, raw);")
|
||||
lines.add(" if ret == 2 {")
|
||||
lines.add(" drop(unsafe {")
|
||||
lines.add(" Box::from_raw(")
|
||||
lines.add(" raw as *mut tokio::sync::oneshot::Sender<Result<String, String>>,")
|
||||
lines.add(" )")
|
||||
lines.add(" });")
|
||||
lines.add(" return Err(\"RET_MISSING_CALLBACK (internal error)\".into());")
|
||||
lines.add(" }")
|
||||
lines.add(" rx")
|
||||
lines.add(" };")
|
||||
lines.add(" rx.await.map_err(|_| \"channel closed before callback fired\".to_string())?")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
|
||||
# ── Context struct ─────────────────────────────────────────────────────────
|
||||
lines.add("/// High-level context for `$1`." % [libTypeName])
|
||||
lines.add("pub struct $1 {" % [ctxTypeName])
|
||||
lines.add(" ptr: *mut c_void,")
|
||||
lines.add(" timeout: Duration,")
|
||||
lines.add("}")
|
||||
lines.add("")
|
||||
lines.add("unsafe impl Send for $1 {}" % [ctxTypeName])
|
||||
lines.add("unsafe impl Sync for $1 {}" % [ctxTypeName])
|
||||
lines.add("")
|
||||
lines.add("impl $1 {" % [ctxTypeName])
|
||||
|
||||
# ── Constructors ───────────────────────────────────────────────────────────
|
||||
for ctor in ctors:
|
||||
var asyncParamsList: seq[string] = @[]
|
||||
for ep in ctor.extraParams:
|
||||
asyncParamsList.add(
|
||||
"$1: $2" % [toSnakeCase(ep.name), nimTypeToRust(ep.typeName)]
|
||||
)
|
||||
let asyncParamsStr = asyncParamsList.join(", ")
|
||||
let blockingParamsStr =
|
||||
if asyncParamsList.len > 0: asyncParamsList.join(", ") & ", timeout: Duration"
|
||||
else: "timeout: Duration"
|
||||
|
||||
# Helper: emit JSON serialization lines for extra params
|
||||
template emitSerialize(snakeName, rustType: string) =
|
||||
if rustType == "String":
|
||||
lines.add(
|
||||
" let $1_json_str = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" %
|
||||
[snakeName]
|
||||
)
|
||||
lines.add(" let $1_c = CString::new($1_json_str).unwrap();" % [snakeName])
|
||||
else:
|
||||
lines.add(
|
||||
" let $1_json = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" %
|
||||
[snakeName]
|
||||
)
|
||||
lines.add(" let $1_c = CString::new($1_json).unwrap();" % [snakeName])
|
||||
|
||||
# Build the ordered arg list for the raw FFI call (ctor: params, cb, ud)
|
||||
var ffiCallArgs: seq[string] = @[]
|
||||
for ep in ctor.extraParams:
|
||||
ffiCallArgs.add("$1_c.as_ptr()" % [toSnakeCase(ep.name)])
|
||||
ffiCallArgs.add("cb")
|
||||
ffiCallArgs.add("ud")
|
||||
let ffiCallArgsStr = ffiCallArgs.join(", ")
|
||||
|
||||
# -- blocking create --
|
||||
lines.add(" pub fn create($1) -> Result<Self, String> {" % [blockingParamsStr])
|
||||
for ep in ctor.extraParams:
|
||||
emitSerialize(toSnakeCase(ep.name), nimTypeToRust(ep.typeName))
|
||||
lines.add(" let raw = ffi_call(timeout, |cb, ud| unsafe {")
|
||||
lines.add(" ffi::$1($2)" % [ctor.procName, ffiCallArgsStr])
|
||||
lines.add(" })?;")
|
||||
lines.add(" let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;")
|
||||
lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout })")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
# -- async new_async --
|
||||
# move closure: each CString is moved in (Send), no raw ptr escapes the block
|
||||
lines.add(" pub async fn new_async($1) -> Result<Self, String> {" % [asyncParamsStr])
|
||||
for ep in ctor.extraParams:
|
||||
emitSerialize(toSnakeCase(ep.name), nimTypeToRust(ep.typeName))
|
||||
lines.add(" let raw = ffi_call_async(move |cb, ud| unsafe {")
|
||||
lines.add(" ffi::$1($2)" % [ctor.procName, ffiCallArgsStr])
|
||||
lines.add(" }).await?;")
|
||||
lines.add(" let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;")
|
||||
lines.add(" Ok(Self { ptr: addr as *mut c_void, timeout: Duration::from_secs(30) })")
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
# ── Methods ────────────────────────────────────────────────────────────────
|
||||
for m in methods:
|
||||
let methodName = stripLibPrefix(m.procName, libName)
|
||||
let retRustType = nimTypeToRust(m.returnTypeName)
|
||||
|
||||
var paramsList: seq[string] = @[]
|
||||
for ep in m.extraParams:
|
||||
paramsList.add("$1: $2" % [toSnakeCase(ep.name), nimTypeToRust(ep.typeName)])
|
||||
let paramsStr = if paramsList.len > 0: ", " & paramsList.join(", ") else: ""
|
||||
|
||||
template emitSerialize(snakeName, rustType: string) =
|
||||
if rustType == "String":
|
||||
lines.add(
|
||||
" let $1_json_str = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" %
|
||||
[snakeName]
|
||||
)
|
||||
lines.add(" let $1_c = CString::new($1_json_str).unwrap();" % [snakeName])
|
||||
else:
|
||||
lines.add(
|
||||
" let $1_json = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" %
|
||||
[snakeName]
|
||||
)
|
||||
lines.add(" let $1_c = CString::new($1_json).unwrap();" % [snakeName])
|
||||
|
||||
template emitDeserialize(retRustType: string) =
|
||||
if retRustType == "String":
|
||||
lines.add(" serde_json::from_str::<String>(&raw).map_err(|e| e.to_string())")
|
||||
elif retRustType == "usize":
|
||||
lines.add(" raw.parse::<usize>().map_err(|e| e.to_string())")
|
||||
else:
|
||||
lines.add(
|
||||
" serde_json::from_str::<$1>(&raw).map_err(|e| e.to_string())" % [retRustType]
|
||||
)
|
||||
|
||||
# -- blocking method --
|
||||
lines.add(" pub fn $1(&self$2) -> Result<$3, String> {" % [methodName, paramsStr, retRustType])
|
||||
for ep in m.extraParams:
|
||||
emitSerialize(toSnakeCase(ep.name), nimTypeToRust(ep.typeName))
|
||||
var ffiArgs: seq[string] = @["self.ptr", "cb", "ud"]
|
||||
for ep in m.extraParams:
|
||||
ffiArgs.add("$1_c.as_ptr()" % [toSnakeCase(ep.name)])
|
||||
let ffiArgsStr = ffiArgs.join(", ")
|
||||
lines.add(" let raw = ffi_call(self.timeout, |cb, ud| unsafe {")
|
||||
lines.add(" ffi::$1($2)" % [m.procName, ffiArgsStr])
|
||||
lines.add(" })?;")
|
||||
emitDeserialize(retRustType)
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
# -- async method --
|
||||
# ptr is cast to usize (Copy + Send) so the move closure is Send,
|
||||
# keeping the returned future Send for multi-threaded tokio runtimes.
|
||||
lines.add(" pub async fn $1_async(&self$2) -> Result<$3, String> {" %
|
||||
[methodName, paramsStr, retRustType])
|
||||
for ep in m.extraParams:
|
||||
emitSerialize(toSnakeCase(ep.name), nimTypeToRust(ep.typeName))
|
||||
lines.add(" let ptr = self.ptr as usize;")
|
||||
var asyncFfiArgs: seq[string] = @["ptr as *mut c_void", "cb", "ud"]
|
||||
for ep in m.extraParams:
|
||||
asyncFfiArgs.add("$1_c.as_ptr()" % [toSnakeCase(ep.name)])
|
||||
let asyncFfiArgsStr = asyncFfiArgs.join(", ")
|
||||
lines.add(" let raw = ffi_call_async(move |cb, ud| unsafe {")
|
||||
lines.add(" ffi::$1($2)" % [m.procName, asyncFfiArgsStr])
|
||||
lines.add(" }).await?;")
|
||||
emitDeserialize(retRustType)
|
||||
lines.add(" }")
|
||||
lines.add("")
|
||||
|
||||
lines.add("}")
|
||||
result = lines.join("\n") & "\n"
|
||||
|
||||
proc generateRustCrate*(
|
||||
procs: seq[FFIProcMeta],
|
||||
types: seq[FFITypeMeta],
|
||||
libName: string,
|
||||
outputDir: string,
|
||||
nimSrcRelPath: string,
|
||||
) =
|
||||
## Generates a complete Rust crate in outputDir.
|
||||
createDir(outputDir)
|
||||
createDir(outputDir / "src")
|
||||
|
||||
writeFile(outputDir / "Cargo.toml", generateCargoToml(libName))
|
||||
writeFile(outputDir / "build.rs", generateBuildRs(libName, nimSrcRelPath))
|
||||
writeFile(outputDir / "src" / "lib.rs", generateLibRs())
|
||||
writeFile(outputDir / "src" / "ffi.rs", generateFfiRs(procs))
|
||||
writeFile(outputDir / "src" / "types.rs", generateTypesRs(types))
|
||||
writeFile(outputDir / "src" / "api.rs", generateApiRs(procs, libName))
|
||||
@ -197,7 +197,8 @@ proc ffiThreadBody[T](ctx: ptr FFIContext[T]) {.thread.} =
|
||||
chronicles.error "ffi thread could not receive a request"
|
||||
continue
|
||||
|
||||
ctx.myLib = addr ffiReqHandler
|
||||
if ctx.myLib.isNil():
|
||||
ctx.myLib = addr ffiReqHandler
|
||||
|
||||
## Handle the request
|
||||
asyncSpawn processRequest(request, ctx)
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import std/[macros, atomics], strformat, chronicles, chronos
|
||||
import ../codegen/meta
|
||||
|
||||
macro declareLibrary*(libraryName: static[string]): untyped =
|
||||
# Record the library name for binding generation
|
||||
currentLibName = libraryName
|
||||
|
||||
var res = newStmtList()
|
||||
|
||||
## Generate {.pragma: exported, exportc, cdecl, raises: [].}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
165
ffi/serial.nim
Normal file
165
ffi/serial.nim
Normal file
@ -0,0 +1,165 @@
|
||||
import std/[json, macros, options]
|
||||
import results
|
||||
import ./codegen/meta
|
||||
|
||||
proc ffiSerialize*(x: string): string =
|
||||
$(%*x)
|
||||
|
||||
proc ffiSerialize*(x: cstring): string =
|
||||
if x.isNil:
|
||||
"null"
|
||||
else:
|
||||
ffiSerialize($x)
|
||||
|
||||
proc ffiSerialize*(x: int): string =
|
||||
$x
|
||||
|
||||
proc ffiSerialize*(x: int32): string =
|
||||
$x
|
||||
|
||||
proc ffiSerialize*(x: bool): string =
|
||||
if x: "true" else: "false"
|
||||
|
||||
proc ffiSerialize*(x: float): string =
|
||||
$(%*x)
|
||||
|
||||
proc ffiSerialize*(x: pointer): string =
|
||||
$cast[uint](x)
|
||||
|
||||
proc ffiDeserialize*(s: cstring, _: typedesc[string]): Result[string, string] =
|
||||
try:
|
||||
let node = parseJson($s)
|
||||
if node.kind != JString:
|
||||
return err("expected JSON string")
|
||||
ok(node.getStr())
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiDeserialize*(s: cstring, _: typedesc[int]): Result[int, string] =
|
||||
try:
|
||||
ok(int(parseJson($s).getBiggestInt()))
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiDeserialize*(s: cstring, _: typedesc[int32]): Result[int32, string] =
|
||||
try:
|
||||
ok(int32(parseJson($s).getBiggestInt()))
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiDeserialize*(s: cstring, _: typedesc[bool]): Result[bool, string] =
|
||||
try:
|
||||
ok(parseJson($s).getBool())
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiDeserialize*(s: cstring, _: typedesc[float]): Result[float, string] =
|
||||
try:
|
||||
ok(parseJson($s).getFloat())
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiDeserialize*(s: cstring, _: typedesc[pointer]): Result[pointer, string] =
|
||||
try:
|
||||
let address = cast[pointer](uint(parseJson($s).getBiggestInt()))
|
||||
ok(address)
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiSerialize*[T](x: ptr T): string =
|
||||
$cast[uint](x)
|
||||
|
||||
proc ffiSerialize*[T](x: seq[T]): string =
|
||||
var arr = newJArray()
|
||||
for item in x:
|
||||
arr.add(parseJson(ffiSerialize(item)))
|
||||
result = $arr
|
||||
|
||||
proc ffiSerialize*[T](x: Option[T]): string =
|
||||
if x.isSome:
|
||||
ffiSerialize(x.get)
|
||||
else:
|
||||
"null"
|
||||
|
||||
proc ffiSerialize*[T: object](x: T): string =
|
||||
$(%*x)
|
||||
|
||||
proc ffiDeserialize*[T](s: cstring, _: typedesc[ptr T]): Result[ptr T, string] =
|
||||
try:
|
||||
let address = cast[ptr T](uint(parseJson($s).getBiggestInt()))
|
||||
ok(address)
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiDeserialize*[T](s: cstring, _: typedesc[seq[T]]): Result[seq[T], string] =
|
||||
try:
|
||||
let node = parseJson($s)
|
||||
if node.kind != JArray:
|
||||
return err("expected JSON array")
|
||||
var resultSeq: seq[T] = @[]
|
||||
for item in node:
|
||||
let itemJson = $item
|
||||
let parsed = ffiDeserialize(itemJson.cstring, typedesc[T])
|
||||
if parsed.isOk:
|
||||
resultSeq.add(parsed.get)
|
||||
else:
|
||||
return err(parsed.error)
|
||||
ok(resultSeq)
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiDeserialize*[T](s: cstring, _: typedesc[Option[T]]): Result[Option[T], string] =
|
||||
try:
|
||||
let node = parseJson($s)
|
||||
if node.kind == JNull:
|
||||
ok(none(T))
|
||||
else:
|
||||
let itemJson = $node
|
||||
let parsed = ffiDeserialize(itemJson.cstring, typedesc[T])
|
||||
if parsed.isOk:
|
||||
ok(some(parsed.get))
|
||||
else:
|
||||
err(parsed.error)
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
proc ffiDeserialize*[T: object](s: cstring, _: typedesc[T]): Result[T, string] =
|
||||
try:
|
||||
ok(parseJson($s).to(T))
|
||||
except Exception as e:
|
||||
err(e.msg)
|
||||
|
||||
macro ffiType*(body: untyped): untyped =
|
||||
## Statement macro applied to a type declaration block.
|
||||
## Registers the type in ffiTypeRegistry for binding generation.
|
||||
## Serialization is handled by the generic ffiSerialize/ffiDeserialize overloads.
|
||||
## Usage:
|
||||
## ffiType:
|
||||
## type Foo = object
|
||||
## field: int
|
||||
let typeSection = body[0]
|
||||
let typeDef = typeSection[0]
|
||||
let typeName =
|
||||
if typeDef[0].kind == nnkPostfix:
|
||||
typeDef[0][1]
|
||||
else:
|
||||
typeDef[0]
|
||||
|
||||
let typeNameStr = $typeName
|
||||
var fieldMetas: seq[FFIFieldMeta] = @[]
|
||||
let objTy = typeDef[2]
|
||||
if objTy.kind == nnkObjectTy and objTy.len >= 3:
|
||||
let recList = objTy[2]
|
||||
if recList.kind == nnkRecList:
|
||||
for identDef in recList:
|
||||
if identDef.kind == nnkIdentDefs:
|
||||
let fieldType = identDef[^2]
|
||||
let fieldTypeName =
|
||||
if fieldType.kind == nnkIdent: $fieldType
|
||||
elif fieldType.kind == nnkPtrTy: "ptr " & $fieldType[0]
|
||||
else: fieldType.repr
|
||||
for i in 0 ..< identDef.len - 2:
|
||||
fieldMetas.add(FFIFieldMeta(name: $identDef[i], typeName: fieldTypeName))
|
||||
|
||||
ffiTypeRegistry.add(FFITypeMeta(name: typeNameStr, fields: fieldMetas))
|
||||
result = body
|
||||
@ -1,4 +1,4 @@
|
||||
import std/locks
|
||||
import std/[locks, strutils, os]
|
||||
import unittest2
|
||||
import results
|
||||
import ../ffi
|
||||
@ -135,3 +135,247 @@ suite "sendRequestToFFIThread":
|
||||
deinitCallbackData(d)
|
||||
check d.retCode == RET_OK
|
||||
check callbackMsg(d) == "pong:" & msg
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ffiCtor macro integration test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
type SimpleLib = object
|
||||
value: int
|
||||
|
||||
ffiType:
|
||||
type SimpleConfig = object
|
||||
initialValue: int
|
||||
|
||||
proc testlib_create*(
|
||||
config: SimpleConfig
|
||||
): Future[Result[SimpleLib, string]] {.ffiCtor.} =
|
||||
return ok(SimpleLib(value: config.initialValue))
|
||||
|
||||
suite "ffiCtor macro":
|
||||
test "creates context and returns pointer via callback":
|
||||
var d: CallbackData
|
||||
initCallbackData(d)
|
||||
defer: deinitCallbackData(d)
|
||||
|
||||
let configJson = ffiSerialize(SimpleConfig(initialValue: 42))
|
||||
let ret = testlib_create(configJson.cstring, testCallback, addr d)
|
||||
|
||||
check ret == RET_OK
|
||||
|
||||
waitCallback(d)
|
||||
|
||||
check d.retCode == RET_OK
|
||||
|
||||
# The callback message is the ctx address as a decimal string
|
||||
let addrStr = callbackMsg(d)
|
||||
check addrStr.len > 0
|
||||
|
||||
let ctxAddr = cast[uint](parseBiggestUInt(addrStr))
|
||||
check ctxAddr != 0
|
||||
let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr)
|
||||
|
||||
# Verify the library was properly initialized
|
||||
check not ctx[].myLib.isNil
|
||||
check ctx[].myLib[].value == 42
|
||||
|
||||
check destroyFFIContext(ctx).isOk()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Simplified .ffi. macro integration test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ffiType:
|
||||
type SendConfig = object
|
||||
message: string
|
||||
|
||||
proc testlib_send*(
|
||||
lib: SimpleLib, cfg: SendConfig
|
||||
): Future[Result[string, string]] {.ffi.} =
|
||||
return ok("echo:" & cfg.message & ":" & $lib.value)
|
||||
|
||||
suite "simplified .ffi. macro":
|
||||
test "sends request and gets serialized response via callback":
|
||||
# First create a context using ffiCtor
|
||||
var ctorD: CallbackData
|
||||
initCallbackData(ctorD)
|
||||
defer: deinitCallbackData(ctorD)
|
||||
|
||||
let configJson = ffiSerialize(SimpleConfig(initialValue: 7))
|
||||
let ctorRet = testlib_create(configJson.cstring, testCallback, addr ctorD)
|
||||
check ctorRet == RET_OK
|
||||
|
||||
waitCallback(ctorD)
|
||||
check ctorD.retCode == RET_OK
|
||||
|
||||
let addrStr = callbackMsg(ctorD)
|
||||
check addrStr.len > 0
|
||||
|
||||
let ctxAddr = cast[uint](parseBiggestUInt(addrStr))
|
||||
check ctxAddr != 0
|
||||
let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr)
|
||||
defer: check destroyFFIContext(ctx).isOk()
|
||||
|
||||
# Now call the .ffi. proc
|
||||
var d: CallbackData
|
||||
initCallbackData(d)
|
||||
defer: deinitCallbackData(d)
|
||||
|
||||
let cfgJson = ffiSerialize(SendConfig(message: "hello"))
|
||||
let ret = testlib_send(ctx, testCallback, addr d, cfgJson.cstring)
|
||||
check ret == RET_OK
|
||||
|
||||
waitCallback(d)
|
||||
check d.retCode == RET_OK
|
||||
|
||||
let receivedMsg = callbackMsg(d)
|
||||
let decoded = ffiDeserialize(receivedMsg.cstring, string).valueOr:
|
||||
check false
|
||||
""
|
||||
check decoded == "echo:hello:7"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# async/sync detection in .ffi. macro integration test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Sync proc (no await in body) — macro detects this and bypasses thread machinery
|
||||
proc testlib_version*(
|
||||
lib: SimpleLib
|
||||
): Future[Result[string, string]] {.ffi.} =
|
||||
return ok("v" & $lib.value)
|
||||
|
||||
suite "async/sync detection in .ffi.":
|
||||
test "sync proc invokes callback without thread hop":
|
||||
# Create a context using ffiCtor
|
||||
var ctorD: CallbackData
|
||||
initCallbackData(ctorD)
|
||||
defer: deinitCallbackData(ctorD)
|
||||
|
||||
let configJson = ffiSerialize(SimpleConfig(initialValue: 3))
|
||||
let ctorRet = testlib_create(configJson.cstring, testCallback, addr ctorD)
|
||||
check ctorRet == RET_OK
|
||||
|
||||
waitCallback(ctorD)
|
||||
check ctorD.retCode == RET_OK
|
||||
|
||||
let addrStr = callbackMsg(ctorD)
|
||||
check addrStr.len > 0
|
||||
|
||||
let ctxAddr = cast[uint](parseBiggestUInt(addrStr))
|
||||
check ctxAddr != 0
|
||||
let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr)
|
||||
defer: check destroyFFIContext(ctx).isOk()
|
||||
|
||||
var d2: CallbackData
|
||||
initCallbackData(d2)
|
||||
defer: deinitCallbackData(d2)
|
||||
|
||||
# Call sync proc — callback should fire before the proc returns (no thread hop)
|
||||
let ret = testlib_version(ctx, testCallback, addr d2)
|
||||
# No sleep needed: sync path fires callback inline before returning
|
||||
check ret == RET_OK
|
||||
check d2.called # fires synchronously — no waitCallback needed
|
||||
check d2.retCode == RET_OK
|
||||
let receivedMsg = callbackMsg(d2)
|
||||
let decoded = ffiDeserialize(receivedMsg.cstring, string).valueOr:
|
||||
check false
|
||||
""
|
||||
check decoded == "v3"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ptr T return type in .ffi. macro integration test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
type Handle = object
|
||||
data: string
|
||||
|
||||
ffiType:
|
||||
type NameParam = object
|
||||
name: string
|
||||
|
||||
proc testlib_alloc_handle*(
|
||||
lib: SimpleLib, np: NameParam
|
||||
): Future[Result[ptr Handle, string]] {.ffi.} =
|
||||
let h = createShared(Handle)
|
||||
h[] = Handle(data: np.name & ":" & $lib.value)
|
||||
return ok(h)
|
||||
|
||||
proc testlib_read_handle*(
|
||||
lib: SimpleLib, handle: pointer
|
||||
): Future[Result[string, string]] {.ffi.} =
|
||||
let h = cast[ptr Handle](handle)
|
||||
return ok(h[].data)
|
||||
|
||||
proc testlib_free_handle*(
|
||||
lib: SimpleLib, handle: pointer
|
||||
): Future[Result[string, string]] {.ffi.} =
|
||||
let h = cast[ptr Handle](handle)
|
||||
deallocShared(h)
|
||||
return ok("freed")
|
||||
|
||||
suite "ptr return type in .ffi.":
|
||||
test "returns a heap-allocated handle and reads it back":
|
||||
# Create context via ffiCtor
|
||||
var ctorD: CallbackData
|
||||
initCallbackData(ctorD)
|
||||
defer: deinitCallbackData(ctorD)
|
||||
|
||||
let configJson = ffiSerialize(SimpleConfig(initialValue: 5))
|
||||
let ctorRet = testlib_create(configJson.cstring, testCallback, addr ctorD)
|
||||
check ctorRet == RET_OK
|
||||
|
||||
waitCallback(ctorD)
|
||||
check ctorD.retCode == RET_OK
|
||||
|
||||
let ctxAddrStr = callbackMsg(ctorD)
|
||||
check ctxAddrStr.len > 0
|
||||
let ctxAddr = cast[uint](parseBiggestUInt(ctxAddrStr))
|
||||
check ctxAddr != 0
|
||||
let ctx = cast[ptr FFIContext[SimpleLib]](ctxAddr)
|
||||
defer: check destroyFFIContext(ctx).isOk()
|
||||
|
||||
# Alloc a handle
|
||||
var allocD: CallbackData
|
||||
initCallbackData(allocD)
|
||||
defer: deinitCallbackData(allocD)
|
||||
|
||||
let npJson = ffiSerialize(NameParam(name: "test"))
|
||||
let allocRet = testlib_alloc_handle(ctx, testCallback, addr allocD, npJson.cstring)
|
||||
check allocRet == RET_OK
|
||||
|
||||
waitCallback(allocD)
|
||||
check allocD.retCode == RET_OK
|
||||
|
||||
let handleAddrStr = callbackMsg(allocD)
|
||||
check handleAddrStr.len > 0
|
||||
let handleAddr = parseBiggestUInt(handleAddrStr)
|
||||
check handleAddr != 0
|
||||
|
||||
# Read the handle back
|
||||
var readD: CallbackData
|
||||
initCallbackData(readD)
|
||||
defer: deinitCallbackData(readD)
|
||||
|
||||
let handleJson = ffiSerialize(cast[pointer](handleAddr))
|
||||
let readRet = testlib_read_handle(ctx, testCallback, addr readD, handleJson.cstring)
|
||||
check readRet == RET_OK
|
||||
|
||||
waitCallback(readD)
|
||||
check readD.retCode == RET_OK
|
||||
|
||||
let readMsg = callbackMsg(readD)
|
||||
let decodedStr = ffiDeserialize(readMsg.cstring, string).valueOr:
|
||||
check false
|
||||
""
|
||||
check decodedStr == "test:5"
|
||||
|
||||
# Free the handle
|
||||
var freeD: CallbackData
|
||||
initCallbackData(freeD)
|
||||
defer: deinitCallbackData(freeD)
|
||||
|
||||
let freeRet = testlib_free_handle(ctx, testCallback, addr freeD, handleJson.cstring)
|
||||
check freeRet == RET_OK
|
||||
|
||||
waitCallback(freeD)
|
||||
check freeD.retCode == RET_OK
|
||||
|
||||
113
tests/test_serial.nim
Normal file
113
tests/test_serial.nim
Normal file
@ -0,0 +1,113 @@
|
||||
import unittest
|
||||
import results
|
||||
import ../ffi/serial
|
||||
|
||||
ffiType:
|
||||
type Point = object
|
||||
x: int
|
||||
y: int
|
||||
|
||||
ffiType:
|
||||
type Nested = object
|
||||
label: string
|
||||
point: Point
|
||||
|
||||
suite "ffiSerialize / ffiDeserialize primitives":
|
||||
test "string round-trip":
|
||||
let s = "hello world"
|
||||
let serialized = ffiSerialize(s)
|
||||
let back = ffiDeserialize(serialized.cstring, string)
|
||||
check back.isOk()
|
||||
check back.value == s
|
||||
|
||||
test "string with special chars":
|
||||
let s = "tab\there"
|
||||
let serialized = ffiSerialize(s)
|
||||
let back = ffiDeserialize(serialized.cstring, string)
|
||||
check back.isOk()
|
||||
check back.value == s
|
||||
|
||||
test "int round-trip":
|
||||
let v = 42
|
||||
let serialized = ffiSerialize(v)
|
||||
let back = ffiDeserialize(serialized.cstring, int)
|
||||
check back.isOk()
|
||||
check back.value == v
|
||||
|
||||
test "int negative round-trip":
|
||||
let v = -100
|
||||
let serialized = ffiSerialize(v)
|
||||
let back = ffiDeserialize(serialized.cstring, int)
|
||||
check back.isOk()
|
||||
check back.value == v
|
||||
|
||||
test "bool true round-trip":
|
||||
let serialized = ffiSerialize(true)
|
||||
let back = ffiDeserialize(serialized.cstring, bool)
|
||||
check back.isOk()
|
||||
check back.value == true
|
||||
|
||||
test "bool false round-trip":
|
||||
let serialized = ffiSerialize(false)
|
||||
let back = ffiDeserialize(serialized.cstring, bool)
|
||||
check back.isOk()
|
||||
check back.value == false
|
||||
|
||||
test "float round-trip":
|
||||
let v = 3.14
|
||||
let serialized = ffiSerialize(v)
|
||||
let back = ffiDeserialize(serialized.cstring, float)
|
||||
check back.isOk()
|
||||
check abs(back.value - v) < 1e-9
|
||||
|
||||
test "float negative round-trip":
|
||||
let v = -2.718
|
||||
let serialized = ffiSerialize(v)
|
||||
let back = ffiDeserialize(serialized.cstring, float)
|
||||
check back.isOk()
|
||||
check abs(back.value - v) < 1e-9
|
||||
|
||||
suite "pointer serialization":
|
||||
test "pointer serialize and recover address":
|
||||
var x = 12345
|
||||
let p = addr x
|
||||
let serialized = ffiSerialize(cast[pointer](p))
|
||||
let back = ffiDeserialize(serialized.cstring, pointer)
|
||||
check back.isOk()
|
||||
check back.value == cast[pointer](p)
|
||||
|
||||
test "nil pointer serializes as 0":
|
||||
let p: pointer = nil
|
||||
let serialized = ffiSerialize(p)
|
||||
check serialized == "0"
|
||||
|
||||
suite "ffiType macro — object round-trip":
|
||||
test "Point round-trip":
|
||||
let pt = Point(x: 10, y: 20)
|
||||
let serialized = ffiSerialize(pt)
|
||||
let back = ffiDeserialize(serialized.cstring, Point)
|
||||
check back.isOk()
|
||||
check back.value.x == 10
|
||||
check back.value.y == 20
|
||||
|
||||
test "Nested object round-trip":
|
||||
let n = Nested(label: "origin", point: Point(x: 0, y: 0))
|
||||
let serialized = ffiSerialize(n)
|
||||
let back = ffiDeserialize(serialized.cstring, Nested)
|
||||
check back.isOk()
|
||||
check back.value.label == "origin"
|
||||
check back.value.point.x == 0
|
||||
check back.value.point.y == 0
|
||||
|
||||
suite "ffiDeserialize error handling":
|
||||
test "malformed JSON returns err":
|
||||
let back = ffiDeserialize("not json at all".cstring, int)
|
||||
check back.isErr()
|
||||
|
||||
test "wrong JSON type returns err for string":
|
||||
let back = ffiDeserialize("42".cstring, string)
|
||||
check back.isErr()
|
||||
|
||||
test "malformed JSON for object returns err":
|
||||
let back = ffiDeserialize("{bad json".cstring, Point)
|
||||
check back.isErr()
|
||||
Loading…
x
Reference in New Issue
Block a user