mirror of
https://github.com/logos-messaging/nim-ffi.git
synced 2026-05-12 21:29:32 +00:00
enhance cpp and rust tokio examples
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
aa22b982be
commit
d87fe8b104
5
.gitignore
vendored
5
.gitignore
vendored
@ -11,7 +11,10 @@ tests/test_ffi_context
|
|||||||
tests/test_serial
|
tests/test_serial
|
||||||
|
|
||||||
# Generated binding crates (regenerated by `nimble genbindings_*`)
|
# Generated binding crates (regenerated by `nimble genbindings_*`)
|
||||||
examples/**/nim_bindings/
|
examples/**/rust_bindings/target/
|
||||||
|
|
||||||
|
# Example build artifacts
|
||||||
|
examples/**/cpp_bindings/build/
|
||||||
|
|
||||||
# Cargo build artifacts
|
# Cargo build artifacts
|
||||||
examples/**/rust_client/target/
|
examples/**/rust_client/target/
|
||||||
|
|||||||
@ -1,2 +1,7 @@
|
|||||||
# nim-ffi
|
# nim-ffi
|
||||||
Allows exposing Nim projects to other languages
|
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.
|
||||||
37
examples/nim_timer/cpp_bindings/main.cpp
Normal file
37
examples/nim_timer/cpp_bindings/main.cpp
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#include "nimtimer.hpp"
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
try {
|
||||||
|
auto ctx = NimTimerCtx::create(TimerConfig{"cpp-demo"});
|
||||||
|
std::cout << "[1] Context created\n";
|
||||||
|
|
||||||
|
auto version = ctx.version();
|
||||||
|
std::cout << "[2] Version: " << version << "\n";
|
||||||
|
|
||||||
|
auto echo = ctx.echo(EchoRequest{"hello from C++", 200});
|
||||||
|
std::cout << "[3] Echo 1: echoed=" << echo.echoed
|
||||||
|
<< ", timerName=" << echo.timerName << "\n";
|
||||||
|
|
||||||
|
auto echo2 = ctx.echo(EchoRequest{"second C++ request", 50});
|
||||||
|
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 complex = ctx.complex(complexReq);
|
||||||
|
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;
|
||||||
|
}
|
||||||
174
examples/nim_timer/cpp_bindings/nimtimer.hpp
Normal file
174
examples/nim_timer/cpp_bindings/nimtimer.hpp
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <functional>
|
||||||
|
#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);
|
||||||
|
} // 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) {
|
||||||
|
return nlohmann::json::parse(raw).get<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
template<>
|
||||||
|
inline void* deserializeFfiResult<void*>(const std::string& raw) {
|
||||||
|
return reinterpret_cast<void*>(static_cast<uintptr_t>(std::stoull(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* s = static_cast<FfiCallState_*>(ud);
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string ffi_call_(std::function<int(FfiCallback, void*)> f) {
|
||||||
|
FfiCallState_ state;
|
||||||
|
const int ret = f(ffi_cb_, &state);
|
||||||
|
if (ret == 2)
|
||||||
|
throw std::runtime_error("RET_MISSING_CALLBACK (internal error)");
|
||||||
|
std::unique_lock<std::mutex> lock(state.mtx);
|
||||||
|
state.cv.wait(lock, [&state]{ return state.done; });
|
||||||
|
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) {
|
||||||
|
const auto config_json = serializeFfiArg(config);
|
||||||
|
const auto raw = ffi_call_([&](FfiCallback cb, void* ud) {
|
||||||
|
return nimtimer_create(config_json.c_str(), cb, ud);
|
||||||
|
});
|
||||||
|
// ctor returns the context address as a plain decimal string
|
||||||
|
const auto addr = std::stoull(raw);
|
||||||
|
return NimTimerCtx(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)));
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
});
|
||||||
|
return deserializeFfiResult<EchoResponse>(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string version() const {
|
||||||
|
const auto raw = ffi_call_([&](FfiCallback cb, void* ud) {
|
||||||
|
return nimtimer_version(ptr_, cb, ud);
|
||||||
|
});
|
||||||
|
return deserializeFfiResult<std::string>(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
});
|
||||||
|
return deserializeFfiResult<ComplexResponse>(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void* ptr_;
|
||||||
|
explicit NimTimerCtx(void* p) : ptr_(p) {}
|
||||||
|
};
|
||||||
@ -1,10 +1,12 @@
|
|||||||
import ffi, chronos
|
import ffi, chronos, options
|
||||||
|
|
||||||
|
type Maybe[T] = Option[T]
|
||||||
|
|
||||||
declareLibrary("nimtimer")
|
declareLibrary("nimtimer")
|
||||||
|
|
||||||
# The library's main state type. The FFI context owns one instance.
|
# The library's main state type. The FFI context owns one instance.
|
||||||
type NimTimer = object
|
type NimTimer = object
|
||||||
name: string # set at creation time, read back in each response
|
name: string # set at creation time, read back in each response
|
||||||
|
|
||||||
ffiType:
|
ffiType:
|
||||||
type TimerConfig = object
|
type TimerConfig = object
|
||||||
@ -13,12 +15,25 @@ ffiType:
|
|||||||
ffiType:
|
ffiType:
|
||||||
type EchoRequest = object
|
type EchoRequest = object
|
||||||
message: string
|
message: string
|
||||||
delayMs: int # how long chronos sleeps before replying
|
delayMs: int # how long chronos sleeps before replying
|
||||||
|
|
||||||
ffiType:
|
ffiType:
|
||||||
type EchoResponse = object
|
type EchoResponse = object
|
||||||
echoed: string
|
echoed: string
|
||||||
timerName: string # proves that the timer's own state is accessible
|
timerName: string # proves that the timer's own state is accessible
|
||||||
|
|
||||||
|
ffiType:
|
||||||
|
type ComplexRequest = object
|
||||||
|
messages: seq[EchoRequest]
|
||||||
|
tags: seq[string]
|
||||||
|
note: Option[string]
|
||||||
|
retries: Maybe[int]
|
||||||
|
|
||||||
|
ffiType:
|
||||||
|
type ComplexResponse = object
|
||||||
|
summary: string
|
||||||
|
itemCount: int
|
||||||
|
hasNote: bool
|
||||||
|
|
||||||
# --- Constructor -----------------------------------------------------------
|
# --- Constructor -----------------------------------------------------------
|
||||||
# Called once from Rust. Creates the FFIContext + NimTimer.
|
# Called once from Rust. Creates the FFIContext + NimTimer.
|
||||||
@ -26,7 +41,7 @@ ffiType:
|
|||||||
proc nimtimer_create*(
|
proc nimtimer_create*(
|
||||||
config: TimerConfig
|
config: TimerConfig
|
||||||
): Future[Result[NimTimer, string]] {.ffiCtor.} =
|
): Future[Result[NimTimer, string]] {.ffiCtor.} =
|
||||||
await sleepAsync(1.milliseconds) # proves chronos is live on the FFI thread
|
await sleepAsync(1.milliseconds) # proves chronos is live on the FFI thread
|
||||||
return ok(NimTimer(name: config.name))
|
return ok(NimTimer(name: config.name))
|
||||||
|
|
||||||
# --- Async method ----------------------------------------------------------
|
# --- Async method ----------------------------------------------------------
|
||||||
@ -41,10 +56,18 @@ proc nimtimer_echo*(
|
|||||||
# --- Sync method -----------------------------------------------------------
|
# --- Sync method -----------------------------------------------------------
|
||||||
# No await — the macro detects this and fires the callback inline,
|
# No await — the macro detects this and fires the callback inline,
|
||||||
# without going through the request channel.
|
# without going through the request channel.
|
||||||
proc nimtimer_version*(
|
proc nimtimer_version*(timer: NimTimer): Future[Result[string, string]] {.ffi.} =
|
||||||
timer: NimTimer
|
|
||||||
): Future[Result[string, string]] {.ffi.} =
|
|
||||||
return ok("nim-timer v0.1.0")
|
return ok("nim-timer v0.1.0")
|
||||||
|
|
||||||
when defined(ffiGenBindings):
|
proc nimtimer_complex*(
|
||||||
genBindings("examples/nim_timer/nim_bindings", "../nim_timer.nim")
|
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() # reads -d:ffiOutputDir, -d:ffiNimSrcRelPath, -d:targetLang from compile flags
|
||||||
|
|||||||
21
examples/nim_timer/nim_timer.nimble
Normal file
21
examples/nim_timer/nim_timer.nimble
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
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 "ffi >= 0.1.3"
|
||||||
|
|
||||||
|
# Build the example library and optionally generate bindings.
|
||||||
|
task build, "Compile the nimtimer library":
|
||||||
|
exec "nim c --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:targetLang=rust nim_timer.nim"
|
||||||
|
|
||||||
|
task genbindings_rust, "Generate Rust bindings for the nimtimer example":
|
||||||
|
exec "nim c --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:targetLang=rust nim_timer.nim"
|
||||||
|
|
||||||
|
task genbindings_cpp, "Generate C++ bindings for the nimtimer example":
|
||||||
|
exec "nim c --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:targetLang=cpp nim_timer.nim"
|
||||||
8
examples/nim_timer/rust_bindings/Cargo.toml
Normal file
8
examples/nim_timer/rust_bindings/Cargo.toml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "nimtimer"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
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());
|
||||||
|
}
|
||||||
102
examples/nim_timer/rust_bindings/src/api.rs
Normal file
102
examples/nim_timer/rust_bindings/src/api.rs
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
|
})?;
|
||||||
|
// ctor returns the context address as a plain decimal string
|
||||||
|
let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;
|
||||||
|
Ok(Self { ptr: addr as *mut c_void, timeout })
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
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,
|
||||||
|
}
|
||||||
28
examples/nim_timer/rust_client/Cargo.lock
generated
28
examples/nim_timer/rust_client/Cargo.lock
generated
@ -22,6 +22,12 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@ -46,6 +52,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"nimtimer",
|
"nimtimer",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -102,6 +109,27 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"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]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
|
|||||||
@ -4,5 +4,14 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
nimtimer = { path = "../nim_bindings" }
|
nimtimer = { path = "../rust_bindings" }
|
||||||
serde_json = "1"
|
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
|
||||||
@ -3,7 +3,7 @@
|
|||||||
// This file uses the generated `nimtimer` crate, which wraps all the raw FFI
|
// This file uses the generated `nimtimer` crate, which wraps all the raw FFI
|
||||||
// boilerplate (extern "C" declarations, callback machinery, JSON encode/decode).
|
// boilerplate (extern "C" declarations, callback machinery, JSON encode/decode).
|
||||||
//
|
//
|
||||||
// To regenerate the `nim_bindings` crate:
|
// To regenerate the `rust_bindings` crate:
|
||||||
// nim c --mm:orc -d:chronicles_log_level=WARN --nimMainPrefix:libnimtimer \
|
// nim c --mm:orc -d:chronicles_log_level=WARN --nimMainPrefix:libnimtimer \
|
||||||
// -d:ffiGenBindings examples/nim_timer/nim_timer.nim
|
// -d:ffiGenBindings examples/nim_timer/nim_timer.nim
|
||||||
use nimtimer::{EchoRequest, NimTimerCtx, TimerConfig};
|
use nimtimer::{EchoRequest, NimTimerCtx, TimerConfig};
|
||||||
|
|||||||
71
examples/nim_timer/rust_client/src/tokio_main.rs
Normal file
71
examples/nim_timer/rust_client/src/tokio_main.rs
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
use nimtimer::{EchoRequest, NimTimerCtx, TimerConfig};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tokio::task;
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
|
||||||
|
async fn main() {
|
||||||
|
let timeout = Duration::from_secs(5);
|
||||||
|
|
||||||
|
let ctx = task::spawn_blocking(move || {
|
||||||
|
NimTimerCtx::create(TimerConfig { name: "tokio-demo".into() }, timeout)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("failed to join create task")
|
||||||
|
.expect("nimtimer_create failed");
|
||||||
|
|
||||||
|
let ctx = Arc::new(Mutex::new(ctx));
|
||||||
|
|
||||||
|
let version = task::spawn_blocking({
|
||||||
|
let ctx = Arc::clone(&ctx);
|
||||||
|
move || {
|
||||||
|
let ctx = ctx.lock().unwrap();
|
||||||
|
ctx.version()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("failed to join version task")
|
||||||
|
.expect("nimtimer_version failed");
|
||||||
|
|
||||||
|
println!("[1] Tokio runtime started");
|
||||||
|
println!("[2] Version: {version}");
|
||||||
|
|
||||||
|
let req1 = EchoRequest {
|
||||||
|
message: "hello from tokio".into(),
|
||||||
|
delay_ms: 200,
|
||||||
|
};
|
||||||
|
let req2 = EchoRequest {
|
||||||
|
message: "second tokio request".into(),
|
||||||
|
delay_ms: 50,
|
||||||
|
};
|
||||||
|
|
||||||
|
let fut1 = task::spawn_blocking({
|
||||||
|
let ctx = Arc::clone(&ctx);
|
||||||
|
move || {
|
||||||
|
let ctx = ctx.lock().unwrap();
|
||||||
|
ctx.echo(req1)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let fut2 = task::spawn_blocking({
|
||||||
|
let ctx = Arc::clone(&ctx);
|
||||||
|
move || {
|
||||||
|
let ctx = ctx.lock().unwrap();
|
||||||
|
ctx.echo(req2)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let echo1 = fut1
|
||||||
|
.await
|
||||||
|
.expect("failed to join tokio blocking task")
|
||||||
|
.expect("nimtimer_echo failed");
|
||||||
|
let echo2 = fut2
|
||||||
|
.await
|
||||||
|
.expect("failed to join tokio blocking task")
|
||||||
|
.expect("nimtimer_echo failed");
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
16
ffi.nimble
16
ffi.nimble
@ -5,7 +5,7 @@ author = "Institute of Free Technology"
|
|||||||
description = "FFI framework with custom header generation"
|
description = "FFI framework with custom header generation"
|
||||||
license = "MIT or Apache License 2.0"
|
license = "MIT or Apache License 2.0"
|
||||||
|
|
||||||
packageName = "ffi"
|
packageName = "ffi"
|
||||||
|
|
||||||
requires "nim >= 2.2.4"
|
requires "nim >= 2.2.4"
|
||||||
requires "chronos"
|
requires "chronos"
|
||||||
@ -41,7 +41,17 @@ task genbindings_example, "Generate Rust bindings for the nim_timer example":
|
|||||||
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -o:/dev/null examples/nim_timer/nim_timer.nim"
|
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -o:/dev/null examples/nim_timer/nim_timer.nim"
|
||||||
|
|
||||||
task genbindings_rust, "Generate Rust bindings for the nim_timer example":
|
task genbindings_rust, "Generate Rust bindings for the nim_timer example":
|
||||||
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:ffiTargetLang=rust -o:/dev/null examples/nim_timer/nim_timer.nim"
|
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":
|
task genbindings_cpp, "Generate C++ bindings for the nim_timer example":
|
||||||
exec "nim c " & nimFlags & " --app:lib --noMain --nimMainPrefix:libnimtimer -d:ffiGenBindings -d:ffiTargetLang=cpp -o:/dev/null examples/nim_timer/nim_timer.nim"
|
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"
|
||||||
|
|||||||
@ -4,15 +4,36 @@
|
|||||||
import std/[os, strutils]
|
import std/[os, strutils]
|
||||||
import ./meta
|
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 =
|
proc nimTypeToCpp*(typeName: string): string =
|
||||||
case typeName
|
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 "string", "cstring": "std::string"
|
||||||
of "int", "int64": "int64_t"
|
of "int", "int64": "int64_t"
|
||||||
of "int32": "int32_t"
|
of "int32": "int32_t"
|
||||||
of "bool": "bool"
|
of "bool": "bool"
|
||||||
of "float", "float64": "double"
|
of "float": "float"
|
||||||
|
of "float64": "double"
|
||||||
of "pointer": "void*"
|
of "pointer": "void*"
|
||||||
else: typeName
|
else: trimmed
|
||||||
|
|
||||||
proc stripLibPrefixCpp(procName, libName: string): string =
|
proc stripLibPrefixCpp(procName, libName: string): string =
|
||||||
let prefix = libName & "_"
|
let prefix = libName & "_"
|
||||||
@ -21,9 +42,7 @@ proc stripLibPrefixCpp(procName, libName: string): string =
|
|||||||
return procName
|
return procName
|
||||||
|
|
||||||
proc generateCppHeader*(
|
proc generateCppHeader*(
|
||||||
procs: seq[FFIProcMeta],
|
procs: seq[FFIProcMeta], types: seq[FFITypeMeta], libName: string
|
||||||
types: seq[FFITypeMeta],
|
|
||||||
libName: string,
|
|
||||||
): string =
|
): string =
|
||||||
var lines: seq[string] = @[]
|
var lines: seq[string] = @[]
|
||||||
|
|
||||||
@ -34,8 +53,24 @@ proc generateCppHeader*(
|
|||||||
lines.add("#include <mutex>")
|
lines.add("#include <mutex>")
|
||||||
lines.add("#include <condition_variable>")
|
lines.add("#include <condition_variable>")
|
||||||
lines.add("#include <functional>")
|
lines.add("#include <functional>")
|
||||||
|
lines.add("#include <vector>")
|
||||||
|
lines.add("#include <optional>")
|
||||||
lines.add("#include <nlohmann/json.hpp>")
|
lines.add("#include <nlohmann/json.hpp>")
|
||||||
lines.add("")
|
lines.add("")
|
||||||
|
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
|
# Types
|
||||||
if types.len > 0:
|
if types.len > 0:
|
||||||
@ -52,7 +87,9 @@ proc generateCppHeader*(
|
|||||||
var fieldNames: seq[string] = @[]
|
var fieldNames: seq[string] = @[]
|
||||||
for f in t.fields:
|
for f in t.fields:
|
||||||
fieldNames.add(f.name)
|
fieldNames.add(f.name)
|
||||||
lines.add("NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE($1, $2)" % [t.name, fieldNames.join(", ")])
|
lines.add(
|
||||||
|
"NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE($1, $2)" % [t.name, fieldNames.join(", ")]
|
||||||
|
)
|
||||||
lines.add("")
|
lines.add("")
|
||||||
|
|
||||||
# C extern declarations
|
# C extern declarations
|
||||||
@ -61,7 +98,9 @@ proc generateCppHeader*(
|
|||||||
lines.add("// ============================================================")
|
lines.add("// ============================================================")
|
||||||
lines.add("")
|
lines.add("")
|
||||||
lines.add("extern \"C\" {")
|
lines.add("extern \"C\" {")
|
||||||
lines.add("typedef void (*FfiCallback)(int ret, const char* msg, size_t len, void* user_data);")
|
lines.add(
|
||||||
|
"typedef void (*FfiCallback)(int ret, const char* msg, size_t len, void* user_data);"
|
||||||
|
)
|
||||||
lines.add("")
|
lines.add("")
|
||||||
|
|
||||||
for p in procs:
|
for p in procs:
|
||||||
@ -82,6 +121,30 @@ proc generateCppHeader*(
|
|||||||
lines.add("} // extern \"C\"")
|
lines.add("} // extern \"C\"")
|
||||||
lines.add("")
|
lines.add("")
|
||||||
|
|
||||||
|
# Transport serialization helpers
|
||||||
|
lines.add("")
|
||||||
|
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("")
|
||||||
|
lines.add("template<typename T>")
|
||||||
|
lines.add("inline T deserializeFfiResult(const std::string& raw) {")
|
||||||
|
lines.add(" return nlohmann::json::parse(raw).get<T>();")
|
||||||
|
lines.add("}")
|
||||||
|
lines.add("")
|
||||||
|
lines.add("template<>")
|
||||||
|
lines.add("inline void* deserializeFfiResult<void*>(const std::string& raw) {")
|
||||||
|
lines.add(
|
||||||
|
" return reinterpret_cast<void*>(static_cast<uintptr_t>(std::stoull(raw)));"
|
||||||
|
)
|
||||||
|
lines.add("}")
|
||||||
|
lines.add("")
|
||||||
|
|
||||||
# Anonymous namespace with synchronous call helper
|
# Anonymous namespace with synchronous call helper
|
||||||
lines.add("// ============================================================")
|
lines.add("// ============================================================")
|
||||||
lines.add("// Synchronous call helper (anonymous namespace, header-only)")
|
lines.add("// Synchronous call helper (anonymous namespace, header-only)")
|
||||||
@ -110,7 +173,9 @@ proc generateCppHeader*(
|
|||||||
lines.add(" FfiCallState_ state;")
|
lines.add(" FfiCallState_ state;")
|
||||||
lines.add(" const int ret = f(ffi_cb_, &state);")
|
lines.add(" const int ret = f(ffi_cb_, &state);")
|
||||||
lines.add(" if (ret == 2)")
|
lines.add(" if (ret == 2)")
|
||||||
lines.add(" throw std::runtime_error(\"RET_MISSING_CALLBACK (internal error)\");")
|
lines.add(
|
||||||
|
" throw std::runtime_error(\"RET_MISSING_CALLBACK (internal error)\");"
|
||||||
|
)
|
||||||
lines.add(" std::unique_lock<std::mutex> lock(state.mtx);")
|
lines.add(" std::unique_lock<std::mutex> lock(state.mtx);")
|
||||||
lines.add(" state.cv.wait(lock, [&state]{ return state.done; });")
|
lines.add(" state.cv.wait(lock, [&state]{ return state.done; });")
|
||||||
lines.add(" if (!state.ok)")
|
lines.add(" if (!state.ok)")
|
||||||
@ -125,12 +190,16 @@ proc generateCppHeader*(
|
|||||||
var ctors: seq[FFIProcMeta] = @[]
|
var ctors: seq[FFIProcMeta] = @[]
|
||||||
var methods: seq[FFIProcMeta] = @[]
|
var methods: seq[FFIProcMeta] = @[]
|
||||||
for p in procs:
|
for p in procs:
|
||||||
if p.kind == ffiCtorKind: ctors.add(p)
|
if p.kind == ffiCtorKind:
|
||||||
else: methods.add(p)
|
ctors.add(p)
|
||||||
|
else:
|
||||||
|
methods.add(p)
|
||||||
|
|
||||||
let libTypeName =
|
let libTypeName =
|
||||||
if ctors.len > 0: ctors[0].libTypeName
|
if ctors.len > 0:
|
||||||
else: libName[0 .. 0].toUpperAscii() & libName[1 .. ^1]
|
ctors[0].libTypeName
|
||||||
|
else:
|
||||||
|
libName[0 .. 0].toUpperAscii() & libName[1 .. ^1]
|
||||||
|
|
||||||
let ctxTypeName = libTypeName & "Ctx"
|
let ctxTypeName = libTypeName & "Ctx"
|
||||||
|
|
||||||
@ -150,7 +219,7 @@ proc generateCppHeader*(
|
|||||||
|
|
||||||
lines.add(" static $1 create($2) {" % [ctxTypeName, ctorParams.join(", ")])
|
lines.add(" static $1 create($2) {" % [ctxTypeName, ctorParams.join(", ")])
|
||||||
for ep in ctor.extraParams:
|
for ep in ctor.extraParams:
|
||||||
lines.add(" const auto $1_json = nlohmann::json($1).dump();" % [ep.name])
|
lines.add(" const auto $1_json = serializeFfiArg($1);" % [ep.name])
|
||||||
|
|
||||||
var callArgs: seq[string] = @[]
|
var callArgs: seq[string] = @[]
|
||||||
for ep in ctor.extraParams:
|
for ep in ctor.extraParams:
|
||||||
@ -163,7 +232,10 @@ proc generateCppHeader*(
|
|||||||
lines.add(" });")
|
lines.add(" });")
|
||||||
lines.add(" // ctor returns the context address as a plain decimal string")
|
lines.add(" // ctor returns the context address as a plain decimal string")
|
||||||
lines.add(" const auto addr = std::stoull(raw);")
|
lines.add(" const auto addr = std::stoull(raw);")
|
||||||
lines.add(" return $1(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)));" % [ctxTypeName])
|
lines.add(
|
||||||
|
" return $1(reinterpret_cast<void*>(static_cast<uintptr_t>(addr)));" %
|
||||||
|
[ctxTypeName]
|
||||||
|
)
|
||||||
lines.add(" }")
|
lines.add(" }")
|
||||||
lines.add("")
|
lines.add("")
|
||||||
|
|
||||||
@ -180,7 +252,7 @@ proc generateCppHeader*(
|
|||||||
|
|
||||||
lines.add(" $1 $2($3) const {" % [retCppType, methodName, methParamsStr])
|
lines.add(" $1 $2($3) const {" % [retCppType, methodName, methParamsStr])
|
||||||
for ep in m.extraParams:
|
for ep in m.extraParams:
|
||||||
lines.add(" const auto $1_json = nlohmann::json($1).dump();" % [ep.name])
|
lines.add(" const auto $1_json = serializeFfiArg($1);" % [ep.name])
|
||||||
|
|
||||||
var callArgs = @["ptr_", "cb", "ud"]
|
var callArgs = @["ptr_", "cb", "ud"]
|
||||||
for ep in m.extraParams:
|
for ep in m.extraParams:
|
||||||
@ -190,10 +262,10 @@ proc generateCppHeader*(
|
|||||||
lines.add(" return $1($2);" % [m.procName, callArgs.join(", ")])
|
lines.add(" return $1($2);" % [m.procName, callArgs.join(", ")])
|
||||||
lines.add(" });")
|
lines.add(" });")
|
||||||
|
|
||||||
if retCppType == "std::string":
|
if retCppType == "void*":
|
||||||
lines.add(" return nlohmann::json::parse(raw).get<std::string>();")
|
lines.add(" return deserializeFfiResult<void*>(raw);")
|
||||||
else:
|
else:
|
||||||
lines.add(" return nlohmann::json::parse(raw).get<$1>();" % [retCppType])
|
lines.add(" return deserializeFfiResult<$1>(raw);" % [retCppType])
|
||||||
lines.add(" }")
|
lines.add(" }")
|
||||||
lines.add("")
|
lines.add("")
|
||||||
|
|
||||||
@ -210,12 +282,12 @@ proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string =
|
|||||||
## CMake uses ${...} which would clash with Nim's % format operator,
|
## CMake uses ${...} which would clash with Nim's % format operator,
|
||||||
## so we build the file line by line using string concatenation.
|
## so we build the file line by line using string concatenation.
|
||||||
let src = nimSrcRelPath.replace("\\", "/")
|
let src = nimSrcRelPath.replace("\\", "/")
|
||||||
let cv = "${CMAKE_CURRENT_SOURCE_DIR}" # CMake variable shorthand
|
let cv = "${CMAKE_CURRENT_SOURCE_DIR}" # CMake variable shorthand
|
||||||
let rv = "${REPO_ROOT}"
|
let rv = "${REPO_ROOT}"
|
||||||
let lf = "${NIM_LIB_FILE}"
|
let lf = "${NIM_LIB_FILE}"
|
||||||
let nm = "${NIM_EXECUTABLE}"
|
let nm = "${NIM_EXECUTABLE}"
|
||||||
let ns = "${NIM_SRC}"
|
let ns = "${NIM_SRC}"
|
||||||
let sd = "${_search_dir}"
|
let sd = "${_search_dir}"
|
||||||
var L: seq[string] = @[]
|
var L: seq[string] = @[]
|
||||||
L.add("cmake_minimum_required(VERSION 3.14)")
|
L.add("cmake_minimum_required(VERSION 3.14)")
|
||||||
L.add("project(" & libName & "_cpp_bindings CXX)")
|
L.add("project(" & libName & "_cpp_bindings CXX)")
|
||||||
@ -223,7 +295,9 @@ proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string =
|
|||||||
L.add("set(CMAKE_CXX_STANDARD 17)")
|
L.add("set(CMAKE_CXX_STANDARD 17)")
|
||||||
L.add("set(CMAKE_CXX_STANDARD_REQUIRED ON)")
|
L.add("set(CMAKE_CXX_STANDARD_REQUIRED ON)")
|
||||||
L.add("")
|
L.add("")
|
||||||
L.add("# ── nlohmann/json ─────────────────────────────────────────────────────────────")
|
L.add(
|
||||||
|
"# ── nlohmann/json ─────────────────────────────────────────────────────────────"
|
||||||
|
)
|
||||||
L.add("include(FetchContent)")
|
L.add("include(FetchContent)")
|
||||||
L.add("FetchContent_Declare(")
|
L.add("FetchContent_Declare(")
|
||||||
L.add(" nlohmann_json")
|
L.add(" nlohmann_json")
|
||||||
@ -233,7 +307,9 @@ proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string =
|
|||||||
L.add(")")
|
L.add(")")
|
||||||
L.add("FetchContent_MakeAvailable(nlohmann_json)")
|
L.add("FetchContent_MakeAvailable(nlohmann_json)")
|
||||||
L.add("")
|
L.add("")
|
||||||
L.add("# ── Locate the repository root (contains ffi.nimble) ─────────────────────────")
|
L.add(
|
||||||
|
"# ── Locate the repository root (contains ffi.nimble) ─────────────────────────"
|
||||||
|
)
|
||||||
L.add("set(_search_dir \"" & cv & "\")")
|
L.add("set(_search_dir \"" & cv & "\")")
|
||||||
L.add("set(REPO_ROOT \"\")")
|
L.add("set(REPO_ROOT \"\")")
|
||||||
L.add("foreach(_i RANGE 10)")
|
L.add("foreach(_i RANGE 10)")
|
||||||
@ -244,15 +320,21 @@ proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string =
|
|||||||
L.add(" get_filename_component(_search_dir \"" & sd & "\" DIRECTORY)")
|
L.add(" get_filename_component(_search_dir \"" & sd & "\" DIRECTORY)")
|
||||||
L.add("endforeach()")
|
L.add("endforeach()")
|
||||||
L.add("if(\"${REPO_ROOT}\" STREQUAL \"\")")
|
L.add("if(\"${REPO_ROOT}\" STREQUAL \"\")")
|
||||||
L.add(" message(FATAL_ERROR \"Cannot find repo root (no ffi.nimble in any ancestor)\")")
|
L.add(
|
||||||
|
" message(FATAL_ERROR \"Cannot find repo root (no ffi.nimble in any ancestor)\")"
|
||||||
|
)
|
||||||
L.add("endif()")
|
L.add("endif()")
|
||||||
L.add("")
|
L.add("")
|
||||||
L.add("# ── Nim source path ───────────────────────────────────────────────────────────")
|
L.add(
|
||||||
|
"# ── Nim source path ───────────────────────────────────────────────────────────"
|
||||||
|
)
|
||||||
L.add("get_filename_component(NIM_SRC")
|
L.add("get_filename_component(NIM_SRC")
|
||||||
L.add(" \"" & cv & "/" & src & "\"")
|
L.add(" \"" & cv & "/" & src & "\"")
|
||||||
L.add(" ABSOLUTE)")
|
L.add(" ABSOLUTE)")
|
||||||
L.add("")
|
L.add("")
|
||||||
L.add("# ── Compile the Nim shared library ───────────────────────────────────────────")
|
L.add(
|
||||||
|
"# ── Compile the Nim shared library ───────────────────────────────────────────"
|
||||||
|
)
|
||||||
L.add("find_program(NIM_EXECUTABLE nim REQUIRED)")
|
L.add("find_program(NIM_EXECUTABLE nim REQUIRED)")
|
||||||
L.add("")
|
L.add("")
|
||||||
L.add("if(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")")
|
L.add("if(CMAKE_SYSTEM_NAME STREQUAL \"Darwin\")")
|
||||||
@ -281,15 +363,24 @@ proc generateCppCMakeLists*(libName: string, nimSrcRelPath: string): string =
|
|||||||
L.add("add_custom_target(nim_lib ALL DEPENDS \"" & lf & "\")")
|
L.add("add_custom_target(nim_lib ALL DEPENDS \"" & lf & "\")")
|
||||||
L.add("")
|
L.add("")
|
||||||
L.add("add_library(" & libName & " SHARED IMPORTED GLOBAL)")
|
L.add("add_library(" & libName & " SHARED IMPORTED GLOBAL)")
|
||||||
L.add("set_target_properties(" & libName & " PROPERTIES IMPORTED_LOCATION \"" & lf & "\")")
|
L.add(
|
||||||
|
"set_target_properties(" & libName & " PROPERTIES IMPORTED_LOCATION \"" & lf & "\")"
|
||||||
|
)
|
||||||
L.add("add_dependencies(" & libName & " nim_lib)")
|
L.add("add_dependencies(" & libName & " nim_lib)")
|
||||||
L.add("")
|
L.add("")
|
||||||
L.add("# ── Interface target exposing the generated header ────────────────────────────")
|
L.add(
|
||||||
|
"# ── Interface target exposing the generated header ────────────────────────────"
|
||||||
|
)
|
||||||
L.add("add_library(" & libName & "_headers INTERFACE)")
|
L.add("add_library(" & libName & "_headers INTERFACE)")
|
||||||
L.add("target_include_directories(" & libName & "_headers INTERFACE \"" & cv & "\")")
|
L.add("target_include_directories(" & libName & "_headers INTERFACE \"" & cv & "\")")
|
||||||
L.add("target_link_libraries(" & libName & "_headers INTERFACE " & libName & " nlohmann_json::nlohmann_json)")
|
L.add(
|
||||||
|
"target_link_libraries(" & libName & "_headers INTERFACE " & libName &
|
||||||
|
" nlohmann_json::nlohmann_json)"
|
||||||
|
)
|
||||||
L.add("")
|
L.add("")
|
||||||
L.add("# ── Optional example executable ───────────────────────────────────────────────")
|
L.add(
|
||||||
|
"# ── Optional example executable ───────────────────────────────────────────────"
|
||||||
|
)
|
||||||
L.add("if(EXISTS \"" & cv & "/main.cpp\")")
|
L.add("if(EXISTS \"" & cv & "/main.cpp\")")
|
||||||
L.add(" add_executable(example main.cpp)")
|
L.add(" add_executable(example main.cpp)")
|
||||||
L.add(" target_link_libraries(example PRIVATE " & libName & "_headers)")
|
L.add(" target_link_libraries(example PRIVATE " & libName & "_headers)")
|
||||||
|
|||||||
@ -3,27 +3,27 @@
|
|||||||
|
|
||||||
type
|
type
|
||||||
FFIParamMeta* = object
|
FFIParamMeta* = object
|
||||||
name*: string # Nim param name, e.g. "req"
|
name*: string # Nim param name, e.g. "req"
|
||||||
typeName*: string # Nim type name, e.g. "EchoRequest"
|
typeName*: string # Nim type name, e.g. "EchoRequest"
|
||||||
isPtr*: bool # true if the type is `ptr T`
|
isPtr*: bool # true if the type is `ptr T`
|
||||||
|
|
||||||
FFIProcKind* = enum
|
FFIProcKind* = enum
|
||||||
ffiCtorKind
|
ffiCtorKind
|
||||||
ffiFfiKind
|
ffiFfiKind
|
||||||
|
|
||||||
FFIProcMeta* = object
|
FFIProcMeta* = object
|
||||||
procName*: string # e.g. "nimtimer_echo"
|
procName*: string # e.g. "nimtimer_echo"
|
||||||
libName*: string # library name, e.g. "nimtimer"
|
libName*: string # library name, e.g. "nimtimer"
|
||||||
kind*: FFIProcKind
|
kind*: FFIProcKind
|
||||||
libTypeName*: string # e.g. "NimTimer"
|
libTypeName*: string # e.g. "NimTimer"
|
||||||
extraParams*: seq[FFIParamMeta] # all params except the lib param
|
extraParams*: seq[FFIParamMeta] # all params except the lib param
|
||||||
returnTypeName*: string # e.g. "EchoResponse", "string", "pointer"
|
returnTypeName*: string # e.g. "EchoResponse", "string", "pointer"
|
||||||
returnIsPtr*: bool # true if return type is ptr T
|
returnIsPtr*: bool # true if return type is ptr T
|
||||||
isAsync*: bool
|
isAsync*: bool
|
||||||
|
|
||||||
FFIFieldMeta* = object
|
FFIFieldMeta* = object
|
||||||
name*: string # e.g. "delayMs"
|
name*: string # e.g. "delayMs"
|
||||||
typeName*: string # e.g. "int"
|
typeName*: string # e.g. "int"
|
||||||
|
|
||||||
FFITypeMeta* = object
|
FFITypeMeta* = object
|
||||||
name*: string
|
name*: string
|
||||||
@ -34,5 +34,12 @@ var ffiProcRegistry* {.compileTime.}: seq[FFIProcMeta]
|
|||||||
var ffiTypeRegistry* {.compileTime.}: seq[FFITypeMeta]
|
var ffiTypeRegistry* {.compileTime.}: seq[FFITypeMeta]
|
||||||
var currentLibName* {.compileTime.}: string
|
var currentLibName* {.compileTime.}: string
|
||||||
|
|
||||||
# Target language for binding generation; override with -d:ffiTargetLang=cpp
|
# Target language for binding generation; override with -d:targetLang=cpp
|
||||||
const ffiTargetLang* {.strdefine.} = "rust"
|
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.} = ""
|
||||||
|
|||||||
@ -19,20 +19,28 @@ proc toSnakeCase*(s: string): string =
|
|||||||
|
|
||||||
proc toPascalCase*(s: string): string =
|
proc toPascalCase*(s: string): string =
|
||||||
## Converts the first letter to uppercase.
|
## Converts the first letter to uppercase.
|
||||||
if s.len == 0: return s
|
if s.len == 0:
|
||||||
|
return s
|
||||||
result = s
|
result = s
|
||||||
result[0] = s[0].toUpperAscii()
|
result[0] = s[0].toUpperAscii()
|
||||||
|
|
||||||
proc nimTypeToRust*(typeName: string): string =
|
proc nimTypeToRust*(typeName: string): string =
|
||||||
## Maps Nim type names to Rust type names.
|
## Maps Nim type names to Rust type names, including generics.
|
||||||
case typeName
|
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 "string", "cstring": "String"
|
||||||
of "int", "int64": "i64"
|
of "int", "int64": "i64"
|
||||||
of "int32": "i32"
|
of "int32": "i32"
|
||||||
of "bool": "bool"
|
of "bool": "bool"
|
||||||
of "float", "float64": "f64"
|
of "float", "float64": "f64"
|
||||||
of "pointer": "usize"
|
of "pointer": "usize"
|
||||||
else: toPascalCase(typeName)
|
else: toPascalCase(t)
|
||||||
|
|
||||||
proc deriveLibName*(procs: seq[FFIProcMeta]): string =
|
proc deriveLibName*(procs: seq[FFIProcMeta]): string =
|
||||||
## Extracts the common prefix before the first `_` from proc names.
|
## Extracts the common prefix before the first `_` from proc names.
|
||||||
@ -60,7 +68,8 @@ proc stripLibPrefix*(procName: string, libName: string): string =
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
proc generateCargoToml*(libName: string): string =
|
proc generateCargoToml*(libName: string): string =
|
||||||
result = """[package]
|
result =
|
||||||
|
"""[package]
|
||||||
name = "$1"
|
name = "$1"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@ -68,13 +77,15 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
""" % [libName]
|
""" %
|
||||||
|
[libName]
|
||||||
|
|
||||||
proc generateBuildRs*(libName: string, nimSrcRelPath: string): string =
|
proc generateBuildRs*(libName: string, nimSrcRelPath: string): string =
|
||||||
## Generates build.rs that compiles the Nim library.
|
## Generates build.rs that compiles the Nim library.
|
||||||
## nimSrcRelPath is relative to the output (crate) directory.
|
## nimSrcRelPath is relative to the output (crate) directory.
|
||||||
let escapedSrc = nimSrcRelPath.replace("\\", "\\\\")
|
let escapedSrc = nimSrcRelPath.replace("\\", "\\\\")
|
||||||
result = """use std::path::PathBuf;
|
result =
|
||||||
|
"""use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@ -121,7 +132,8 @@ fn main() {
|
|||||||
println!("cargo:rustc-link-lib=$2");
|
println!("cargo:rustc-link-lib=$2");
|
||||||
println!("cargo:rerun-if-changed={}", nim_src.display());
|
println!("cargo:rerun-if-changed={}", nim_src.display());
|
||||||
}
|
}
|
||||||
""" % [escapedSrc, libName]
|
""" %
|
||||||
|
[escapedSrc, libName]
|
||||||
|
|
||||||
proc generateLibRs*(): string =
|
proc generateLibRs*(): string =
|
||||||
result = """mod ffi;
|
result = """mod ffi;
|
||||||
@ -239,7 +251,7 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string =
|
|||||||
lines.add("use super::types::*;")
|
lines.add("use super::types::*;")
|
||||||
lines.add("")
|
lines.add("")
|
||||||
|
|
||||||
# FfiCallbackResult struct
|
# FfiCallbackResult + Pair
|
||||||
lines.add("#[derive(Default)]")
|
lines.add("#[derive(Default)]")
|
||||||
lines.add("struct FfiCallbackResult {")
|
lines.add("struct FfiCallbackResult {")
|
||||||
lines.add(" payload: Option<Result<String, String>>,")
|
lines.add(" payload: Option<Result<String, String>>,")
|
||||||
@ -248,7 +260,7 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string =
|
|||||||
lines.add("type Pair = Arc<(Mutex<FfiCallbackResult>, Condvar)>;")
|
lines.add("type Pair = Arc<(Mutex<FfiCallbackResult>, Condvar)>;")
|
||||||
lines.add("")
|
lines.add("")
|
||||||
|
|
||||||
# on_result callback
|
# on_result callback (Arc-based, blocking)
|
||||||
lines.add("unsafe extern \"C\" fn on_result(")
|
lines.add("unsafe extern \"C\" fn on_result(")
|
||||||
lines.add(" ret: c_int,")
|
lines.add(" ret: c_int,")
|
||||||
lines.add(" msg: *const c_char,")
|
lines.add(" msg: *const c_char,")
|
||||||
@ -270,7 +282,7 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string =
|
|||||||
lines.add("}")
|
lines.add("}")
|
||||||
lines.add("")
|
lines.add("")
|
||||||
|
|
||||||
# ffi_call helper
|
# Blocking ffi_call helper using Condvar::wait_timeout_while
|
||||||
lines.add("fn ffi_call<F>(timeout: Duration, f: F) -> Result<String, String>")
|
lines.add("fn ffi_call<F>(timeout: Duration, f: F) -> Result<String, String>")
|
||||||
lines.add("where")
|
lines.add("where")
|
||||||
lines.add(" F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,")
|
lines.add(" F: FnOnce(ffi::FfiCallback, *mut c_void) -> c_int,")
|
||||||
@ -325,10 +337,18 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string =
|
|||||||
let rustType = nimTypeToRust(ep.typeName)
|
let rustType = nimTypeToRust(ep.typeName)
|
||||||
if rustType == "String":
|
if rustType == "String":
|
||||||
# Primitive string — wrap it in JSON
|
# Primitive string — wrap it in JSON
|
||||||
lines.add(" let $1_json_str = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" % [snakeName])
|
lines.add(
|
||||||
lines.add(" let $1_c = CString::new($1_json_str).unwrap();" % [snakeName])
|
" 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:
|
else:
|
||||||
lines.add(" let $1_json = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" % [snakeName])
|
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])
|
lines.add(" let $1_c = CString::new($1_json).unwrap();" % [snakeName])
|
||||||
|
|
||||||
# Build the ffi_call closure
|
# Build the ffi_call closure
|
||||||
@ -344,7 +364,9 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string =
|
|||||||
lines.add(" ffi::$1($2)" % [ctor.procName, ffiCallArgsStr])
|
lines.add(" ffi::$1($2)" % [ctor.procName, ffiCallArgsStr])
|
||||||
lines.add(" })?;")
|
lines.add(" })?;")
|
||||||
lines.add(" // ctor returns the context address as a plain decimal string")
|
lines.add(" // ctor returns the context address as a plain decimal string")
|
||||||
lines.add(" let addr: usize = raw.parse().map_err(|e: std::num::ParseIntError| e.to_string())?;")
|
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(" Ok(Self { ptr: addr as *mut c_void, timeout })")
|
||||||
lines.add(" }")
|
lines.add(" }")
|
||||||
lines.add("")
|
lines.add("")
|
||||||
@ -359,19 +381,34 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string =
|
|||||||
let rustType = nimTypeToRust(ep.typeName)
|
let rustType = nimTypeToRust(ep.typeName)
|
||||||
let snakeName = toSnakeCase(ep.name)
|
let snakeName = toSnakeCase(ep.name)
|
||||||
paramsList.add("$1: $2" % [snakeName, rustType])
|
paramsList.add("$1: $2" % [snakeName, rustType])
|
||||||
let paramsStr = if paramsList.len > 0: ", " & paramsList.join(", ") else: ""
|
let paramsStr =
|
||||||
|
if paramsList.len > 0:
|
||||||
|
", " & paramsList.join(", ")
|
||||||
|
else:
|
||||||
|
""
|
||||||
|
|
||||||
lines.add(" pub fn $1(&self$2) -> Result<$3, String> {" % [methodName, paramsStr, retRustType])
|
lines.add(
|
||||||
|
" pub fn $1(&self$2) -> Result<$3, String> {" %
|
||||||
|
[methodName, paramsStr, retRustType]
|
||||||
|
)
|
||||||
|
|
||||||
# Serialize extra params
|
# Serialize extra params
|
||||||
for ep in m.extraParams:
|
for ep in m.extraParams:
|
||||||
let snakeName = toSnakeCase(ep.name)
|
let snakeName = toSnakeCase(ep.name)
|
||||||
let rustType = nimTypeToRust(ep.typeName)
|
let rustType = nimTypeToRust(ep.typeName)
|
||||||
if 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(
|
||||||
lines.add(" let $1_c = CString::new($1_json_str).unwrap();" % [snakeName])
|
" 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:
|
else:
|
||||||
lines.add(" let $1_json = serde_json::to_string(&$1).map_err(|e| e.to_string())?;" % [snakeName])
|
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])
|
lines.add(" let $1_c = CString::new($1_json).unwrap();" % [snakeName])
|
||||||
|
|
||||||
# Build ffi call args: ctx first, then callback/ud, then json args
|
# Build ffi call args: ctx first, then callback/ud, then json args
|
||||||
@ -387,11 +424,16 @@ proc generateApiRs*(procs: seq[FFIProcMeta], libName: string): string =
|
|||||||
|
|
||||||
# Deserialize return value
|
# Deserialize return value
|
||||||
if retRustType == "String":
|
if retRustType == "String":
|
||||||
lines.add(" serde_json::from_str::<String>(&raw).map_err(|e| e.to_string())")
|
lines.add(
|
||||||
|
" serde_json::from_str::<String>(&raw).map_err(|e| e.to_string())"
|
||||||
|
)
|
||||||
elif retRustType == "usize":
|
elif retRustType == "usize":
|
||||||
lines.add(" raw.parse::<usize>().map_err(|e| e.to_string())")
|
lines.add(" raw.parse::<usize>().map_err(|e| e.to_string())")
|
||||||
else:
|
else:
|
||||||
lines.add(" serde_json::from_str::<$1>(&raw).map_err(|e| e.to_string())" % [retRustType])
|
lines.add(
|
||||||
|
" serde_json::from_str::<$1>(&raw).map_err(|e| e.to_string())" %
|
||||||
|
[retRustType]
|
||||||
|
)
|
||||||
lines.add(" }")
|
lines.add(" }")
|
||||||
lines.add("")
|
lines.add("")
|
||||||
|
|
||||||
|
|||||||
@ -200,6 +200,7 @@ proc buildFfiNewReqProc(reqTypeName, body: NimNode): NimNode =
|
|||||||
FFIThreadRequest.init(callback, userData, typeStr.cstring, `reqObjIdent`)
|
FFIThreadRequest.init(callback, userData, typeStr.cstring, `reqObjIdent`)
|
||||||
proc destroyContent(content: pointer) {.nimcall.} =
|
proc destroyContent(content: pointer) {.nimcall.} =
|
||||||
ffiDeleteReq(cast[ptr `reqTypeName`](content))
|
ffiDeleteReq(cast[ptr `reqTypeName`](content))
|
||||||
|
|
||||||
ret[].deleteReqContent = destroyContent
|
ret[].deleteReqContent = destroyContent
|
||||||
return ret
|
return ret
|
||||||
)
|
)
|
||||||
@ -363,12 +364,11 @@ proc addNewRequestToRegistry(reqTypeName, reqHandler: NimNode): NimNode =
|
|||||||
# CreateNodeRequest.processFFIRequest(request, reqHandler)
|
# CreateNodeRequest.processFFIRequest(request, reqHandler)
|
||||||
let asyncProc = newProc(
|
let asyncProc = newProc(
|
||||||
name = newEmptyNode(), # anonymous proc
|
name = newEmptyNode(), # anonymous proc
|
||||||
params =
|
params = @[
|
||||||
@[
|
returnType,
|
||||||
returnType,
|
newIdentDefs(ident("request"), ident("pointer")),
|
||||||
newIdentDefs(ident("request"), ident("pointer")),
|
newIdentDefs(ident("reqHandler"), ident("pointer")),
|
||||||
newIdentDefs(ident("reqHandler"), ident("pointer")),
|
],
|
||||||
],
|
|
||||||
body = newBody,
|
body = newBody,
|
||||||
pragmas = nnkPragma.newTree(ident("async")),
|
pragmas = nnkPragma.newTree(ident("async")),
|
||||||
)
|
)
|
||||||
@ -587,19 +587,27 @@ macro ffi*(prc: untyped): untyped =
|
|||||||
|
|
||||||
# Extract LibType from the first parameter
|
# Extract LibType from the first parameter
|
||||||
let firstParam = formalParams[1]
|
let firstParam = formalParams[1]
|
||||||
let libParamName = firstParam[0] # e.g. `w`
|
let libParamName = firstParam[0] # e.g. `w`
|
||||||
let libTypeName = firstParam[1] # e.g. `Waku`
|
let libTypeName = firstParam[1] # e.g. `Waku`
|
||||||
|
|
||||||
# Extract the return type: Future[Result[RetType, string]]
|
# Extract the return type: Future[Result[RetType, string]]
|
||||||
# RetType is used in the body helper proc signature
|
# RetType is used in the body helper proc signature
|
||||||
let retTypeNode = formalParams[0]
|
let retTypeNode = formalParams[0]
|
||||||
if retTypeNode.kind == nnkEmpty:
|
if retTypeNode.kind == nnkEmpty:
|
||||||
error("`.ffi.` proc must have an explicit return type Future[Result[RetType, string]]")
|
error(
|
||||||
|
"`.ffi.` proc must have an explicit return type Future[Result[RetType, string]]"
|
||||||
|
)
|
||||||
if retTypeNode.kind != nnkBracketExpr or $retTypeNode[0] != "Future":
|
if retTypeNode.kind != nnkBracketExpr or $retTypeNode[0] != "Future":
|
||||||
error("`.ffi.` return type must be Future[Result[RetType, string]], got: " & retTypeNode.repr)
|
error(
|
||||||
|
"`.ffi.` return type must be Future[Result[RetType, string]], got: " &
|
||||||
|
retTypeNode.repr
|
||||||
|
)
|
||||||
let resultInner = retTypeNode[1] # Result[RetType, string]
|
let resultInner = retTypeNode[1] # Result[RetType, string]
|
||||||
if resultInner.kind != nnkBracketExpr or $resultInner[0] != "Result":
|
if resultInner.kind != nnkBracketExpr or $resultInner[0] != "Result":
|
||||||
error("`.ffi.` return type must be Future[Result[RetType, string]], got: " & retTypeNode.repr)
|
error(
|
||||||
|
"`.ffi.` return type must be Future[Result[RetType, string]], got: " &
|
||||||
|
retTypeNode.repr
|
||||||
|
)
|
||||||
|
|
||||||
# Collect additional param names and types (everything after the first param)
|
# Collect additional param names and types (everything after the first param)
|
||||||
var extraParamNames: seq[string] = @[]
|
var extraParamNames: seq[string] = @[]
|
||||||
@ -613,7 +621,10 @@ macro ffi*(prc: untyped): untyped =
|
|||||||
# Generate type/proc names from proc name
|
# Generate type/proc names from proc name
|
||||||
let procNameStr = block:
|
let procNameStr = block:
|
||||||
let raw = $procName
|
let raw = $procName
|
||||||
if raw.endsWith("*"): raw[0 ..^ 2] else: raw
|
if raw.endsWith("*"):
|
||||||
|
raw[0 ..^ 2]
|
||||||
|
else:
|
||||||
|
raw
|
||||||
let camelName = toCamelCase(procNameStr)
|
let camelName = toCamelCase(procNameStr)
|
||||||
|
|
||||||
# Names of generated things
|
# Names of generated things
|
||||||
@ -631,9 +642,8 @@ macro ffi*(prc: untyped): untyped =
|
|||||||
procName
|
procName
|
||||||
|
|
||||||
# Common exported params (needed for both branches)
|
# Common exported params (needed for both branches)
|
||||||
let ctxType = nnkPtrTy.newTree(
|
let ctxType =
|
||||||
nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)
|
nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName))
|
||||||
)
|
|
||||||
|
|
||||||
if isAsync:
|
if isAsync:
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
@ -668,9 +678,8 @@ macro ffi*(prc: untyped): untyped =
|
|||||||
)
|
)
|
||||||
|
|
||||||
let ctxHandlerName = ident("ffiCtxHandler")
|
let ctxHandlerName = ident("ffiCtxHandler")
|
||||||
let ptrFfiCtx = nnkPtrTy.newTree(
|
let ptrFfiCtx =
|
||||||
nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)
|
nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName))
|
||||||
)
|
|
||||||
|
|
||||||
var lambdaParams = newSeq[NimNode]()
|
var lambdaParams = newSeq[NimNode]()
|
||||||
lambdaParams.add(futStrStr)
|
lambdaParams.add(futStrStr)
|
||||||
@ -785,8 +794,10 @@ macro ffi*(prc: untyped): untyped =
|
|||||||
tn = $ptype[0]
|
tn = $ptype[0]
|
||||||
else:
|
else:
|
||||||
tn = $ptype
|
tn = $ptype
|
||||||
ffiExtraParams.add(FFIParamMeta(name: extraParamNames[i], typeName: tn, isPtr: isPtr))
|
ffiExtraParams.add(
|
||||||
let retTypeInner = resultInner[1] # RetType from Result[RetType, string]
|
FFIParamMeta(name: extraParamNames[i], typeName: tn, isPtr: isPtr)
|
||||||
|
)
|
||||||
|
let retTypeInner = resultInner[1] # RetType from Result[RetType, string]
|
||||||
var retIsPtr = false
|
var retIsPtr = false
|
||||||
var retTn = ""
|
var retTn = ""
|
||||||
if retTypeInner.kind == nnkPtrTy:
|
if retTypeInner.kind == nnkPtrTy:
|
||||||
@ -794,19 +805,20 @@ macro ffi*(prc: untyped): untyped =
|
|||||||
retTn = $retTypeInner[0]
|
retTn = $retTypeInner[0]
|
||||||
else:
|
else:
|
||||||
retTn = $retTypeInner
|
retTn = $retTypeInner
|
||||||
ffiProcRegistry.add(FFIProcMeta(
|
ffiProcRegistry.add(
|
||||||
procName: procNameStr,
|
FFIProcMeta(
|
||||||
libName: currentLibName,
|
procName: procNameStr,
|
||||||
kind: ffiFfiKind,
|
libName: currentLibName,
|
||||||
libTypeName: $libTypeName,
|
kind: ffiFfiKind,
|
||||||
extraParams: ffiExtraParams,
|
libTypeName: $libTypeName,
|
||||||
returnTypeName: retTn,
|
extraParams: ffiExtraParams,
|
||||||
returnIsPtr: retIsPtr,
|
returnTypeName: retTn,
|
||||||
isAsync: true,
|
returnIsPtr: retIsPtr,
|
||||||
))
|
isAsync: true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
result = newStmtList(helperProc, registerReq, ffiProc)
|
result = newStmtList(helperProc, registerReq, ffiProc)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# -------------------------------------------------------------------------
|
# -------------------------------------------------------------------------
|
||||||
# SYNC PATH — no await/waitFor in body; bypass thread-channel machinery
|
# SYNC PATH — no await/waitFor in body; bypass thread-channel machinery
|
||||||
@ -881,7 +893,9 @@ macro ffi*(prc: untyped): untyped =
|
|||||||
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
callback(RET_ERR, unsafeAddr errStr[0], cast[csize_t](errStr.len), userData)
|
||||||
return RET_ERR
|
return RET_ERR
|
||||||
let serialized = ffiSerialize(`retValOrErrIdent`.value)
|
let serialized = ffiSerialize(`retValOrErrIdent`.value)
|
||||||
callback(RET_OK, unsafeAddr serialized[0], cast[csize_t](serialized.len), userData)
|
callback(
|
||||||
|
RET_OK, unsafeAddr serialized[0], cast[csize_t](serialized.len), userData
|
||||||
|
)
|
||||||
return RET_OK
|
return RET_OK
|
||||||
|
|
||||||
let syncFfiProc = newProc(
|
let syncFfiProc = newProc(
|
||||||
@ -909,7 +923,9 @@ macro ffi*(prc: untyped): untyped =
|
|||||||
tn = $ptype[0]
|
tn = $ptype[0]
|
||||||
else:
|
else:
|
||||||
tn = $ptype
|
tn = $ptype
|
||||||
ffiExtraParamsSync.add(FFIParamMeta(name: extraParamNames[i], typeName: tn, isPtr: isPtr))
|
ffiExtraParamsSync.add(
|
||||||
|
FFIParamMeta(name: extraParamNames[i], typeName: tn, isPtr: isPtr)
|
||||||
|
)
|
||||||
let retTypeInnerSync = resultInner[1]
|
let retTypeInnerSync = resultInner[1]
|
||||||
var retIsPtrSync = false
|
var retIsPtrSync = false
|
||||||
var retTnSync = ""
|
var retTnSync = ""
|
||||||
@ -918,16 +934,18 @@ macro ffi*(prc: untyped): untyped =
|
|||||||
retTnSync = $retTypeInnerSync[0]
|
retTnSync = $retTypeInnerSync[0]
|
||||||
else:
|
else:
|
||||||
retTnSync = $retTypeInnerSync
|
retTnSync = $retTypeInnerSync
|
||||||
ffiProcRegistry.add(FFIProcMeta(
|
ffiProcRegistry.add(
|
||||||
procName: procNameStr,
|
FFIProcMeta(
|
||||||
libName: currentLibName,
|
procName: procNameStr,
|
||||||
kind: ffiFfiKind,
|
libName: currentLibName,
|
||||||
libTypeName: $libTypeName,
|
kind: ffiFfiKind,
|
||||||
extraParams: ffiExtraParamsSync,
|
libTypeName: $libTypeName,
|
||||||
returnTypeName: retTnSync,
|
extraParams: ffiExtraParamsSync,
|
||||||
returnIsPtr: retIsPtrSync,
|
returnTypeName: retTnSync,
|
||||||
isAsync: false,
|
returnIsPtr: retIsPtrSync,
|
||||||
))
|
isAsync: false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
result = newStmtList(syncHelperProc, syncFfiProc)
|
result = newStmtList(syncHelperProc, syncFfiProc)
|
||||||
|
|
||||||
@ -953,11 +971,15 @@ proc buildCtorRequestType(reqTypeName: NimNode, paramNames: seq[string]): NimNod
|
|||||||
if fields.len > 0:
|
if fields.len > 0:
|
||||||
newTree(nnkRecList, fields)
|
newTree(nnkRecList, fields)
|
||||||
else:
|
else:
|
||||||
newTree(nnkRecList, newTree(nnkIdentDefs, ident("_placeholder"), ident("pointer"), newEmptyNode()))
|
newTree(
|
||||||
|
nnkRecList,
|
||||||
|
newTree(nnkIdentDefs, ident("_placeholder"), ident("pointer"), newEmptyNode()),
|
||||||
|
)
|
||||||
|
|
||||||
let objTy = newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), recList)
|
let objTy = newTree(nnkObjectTy, newEmptyNode(), newEmptyNode(), recList)
|
||||||
let typeName = postfix(reqTypeName, "*")
|
let typeName = postfix(reqTypeName, "*")
|
||||||
result = newNimNode(nnkTypeSection).add(newTree(nnkTypeDef, typeName, newEmptyNode(), objTy))
|
result =
|
||||||
|
newNimNode(nnkTypeSection).add(newTree(nnkTypeDef, typeName, newEmptyNode(), objTy))
|
||||||
|
|
||||||
when defined(ffiDumpMacros):
|
when defined(ffiDumpMacros):
|
||||||
echo result.repr
|
echo result.repr
|
||||||
@ -990,9 +1012,8 @@ proc buildCtorFfiNewReqProc(reqTypeName: NimNode, paramNames: seq[string]): NimN
|
|||||||
|
|
||||||
var formalParams = newSeq[NimNode]()
|
var formalParams = newSeq[NimNode]()
|
||||||
|
|
||||||
let typedescParam = newIdentDefs(
|
let typedescParam =
|
||||||
ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName)
|
newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName))
|
||||||
)
|
|
||||||
formalParams.add(typedescParam)
|
formalParams.add(typedescParam)
|
||||||
formalParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
|
formalParams.add(newIdentDefs(ident("callback"), ident("FFICallBack")))
|
||||||
formalParams.add(newIdentDefs(ident("userData"), ident("pointer")))
|
formalParams.add(newIdentDefs(ident("userData"), ident("pointer")))
|
||||||
@ -1018,6 +1039,7 @@ proc buildCtorFfiNewReqProc(reqTypeName: NimNode, paramNames: seq[string]): NimN
|
|||||||
var ret = FFIThreadRequest.init(callback, userData, typeStr.cstring, `reqObjIdent`)
|
var ret = FFIThreadRequest.init(callback, userData, typeStr.cstring, `reqObjIdent`)
|
||||||
proc destroyContent(content: pointer) {.nimcall.} =
|
proc destroyContent(content: pointer) {.nimcall.} =
|
||||||
ffiDeleteReq(cast[ptr `reqTypeName`](content))
|
ffiDeleteReq(cast[ptr `reqTypeName`](content))
|
||||||
|
|
||||||
ret[].deleteReqContent = destroyContent
|
ret[].deleteReqContent = destroyContent
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
@ -1084,9 +1106,8 @@ proc buildCtorProcessFFIRequestProc(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# The ctx param type: ptr FFIContext[LibType]
|
# The ctx param type: ptr FFIContext[LibType]
|
||||||
let ctxType = nnkPtrTy.newTree(
|
let ctxType =
|
||||||
nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)
|
nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName))
|
||||||
)
|
|
||||||
|
|
||||||
let typedescParam =
|
let typedescParam =
|
||||||
newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName))
|
newIdentDefs(ident("T"), nnkBracketExpr.newTree(ident("typedesc"), reqTypeName))
|
||||||
@ -1152,9 +1173,8 @@ proc addCtorRequestToRegistry(reqTypeName, libTypeName: NimNode): NimNode =
|
|||||||
## Registers the ctor request in the registeredRequests table.
|
## Registers the ctor request in the registeredRequests table.
|
||||||
## The handler casts reqHandler to ptr FFIContext[LibType] and calls processFFIRequest.
|
## The handler casts reqHandler to ptr FFIContext[LibType] and calls processFFIRequest.
|
||||||
|
|
||||||
let ctxType = nnkPtrTy.newTree(
|
let ctxType =
|
||||||
nnkBracketExpr.newTree(ident("FFIContext"), libTypeName)
|
nnkPtrTy.newTree(nnkBracketExpr.newTree(ident("FFIContext"), libTypeName))
|
||||||
)
|
|
||||||
|
|
||||||
let returnType = nnkBracketExpr.newTree(
|
let returnType = nnkBracketExpr.newTree(
|
||||||
ident("Future"),
|
ident("Future"),
|
||||||
@ -1173,20 +1193,18 @@ proc addCtorRequestToRegistry(reqTypeName, libTypeName: NimNode): NimNode =
|
|||||||
|
|
||||||
let asyncProc = newProc(
|
let asyncProc = newProc(
|
||||||
name = newEmptyNode(),
|
name = newEmptyNode(),
|
||||||
params =
|
params = @[
|
||||||
@[
|
returnType,
|
||||||
returnType,
|
newIdentDefs(ident("request"), ident("pointer")),
|
||||||
newIdentDefs(ident("request"), ident("pointer")),
|
newIdentDefs(ident("reqHandler"), ident("pointer")),
|
||||||
newIdentDefs(ident("reqHandler"), ident("pointer")),
|
],
|
||||||
],
|
|
||||||
body = newBody,
|
body = newBody,
|
||||||
pragmas = nnkPragma.newTree(ident("async")),
|
pragmas = nnkPragma.newTree(ident("async")),
|
||||||
)
|
)
|
||||||
|
|
||||||
let key = newLit($reqTypeName)
|
let key = newLit($reqTypeName)
|
||||||
result = newAssignment(
|
result =
|
||||||
newTree(nnkBracketExpr, ident("registeredRequests"), key), asyncProc
|
newAssignment(newTree(nnkBracketExpr, ident("registeredRequests"), key), asyncProc)
|
||||||
)
|
|
||||||
|
|
||||||
when defined(ffiDumpMacros):
|
when defined(ffiDumpMacros):
|
||||||
echo result.repr
|
echo result.repr
|
||||||
@ -1219,13 +1237,21 @@ macro ffiCtor*(prc: untyped): untyped =
|
|||||||
let retTypeNode = formalParams[0]
|
let retTypeNode = formalParams[0]
|
||||||
# retTypeNode should be Future[Result[LibType, string]]
|
# retTypeNode should be Future[Result[LibType, string]]
|
||||||
if retTypeNode.kind == nnkEmpty:
|
if retTypeNode.kind == nnkEmpty:
|
||||||
error("ffiCtor: proc must have an explicit return type Future[Result[LibType, string]]")
|
error(
|
||||||
|
"ffiCtor: proc must have an explicit return type Future[Result[LibType, string]]"
|
||||||
|
)
|
||||||
# retTypeNode: BracketExpr(Future, BracketExpr(Result, LibType, string))
|
# retTypeNode: BracketExpr(Future, BracketExpr(Result, LibType, string))
|
||||||
if retTypeNode.kind != nnkBracketExpr or $retTypeNode[0] != "Future":
|
if retTypeNode.kind != nnkBracketExpr or $retTypeNode[0] != "Future":
|
||||||
error("ffiCtor: return type must be Future[Result[LibType, string]], got: " & retTypeNode.repr)
|
error(
|
||||||
|
"ffiCtor: return type must be Future[Result[LibType, string]], got: " &
|
||||||
|
retTypeNode.repr
|
||||||
|
)
|
||||||
let resultInner = retTypeNode[1] # Result[LibType, string]
|
let resultInner = retTypeNode[1] # Result[LibType, string]
|
||||||
if resultInner.kind != nnkBracketExpr or $resultInner[0] != "Result":
|
if resultInner.kind != nnkBracketExpr or $resultInner[0] != "Result":
|
||||||
error("ffiCtor: return type must be Future[Result[LibType, string]], got: " & retTypeNode.repr)
|
error(
|
||||||
|
"ffiCtor: return type must be Future[Result[LibType, string]], got: " &
|
||||||
|
retTypeNode.repr
|
||||||
|
)
|
||||||
let libTypeName = resultInner[1] # LibType
|
let libTypeName = resultInner[1] # LibType
|
||||||
|
|
||||||
# Collect param names and types (skip return type at index 0)
|
# Collect param names and types (skip return type at index 0)
|
||||||
@ -1256,9 +1282,11 @@ macro ffiCtor*(prc: untyped): untyped =
|
|||||||
# Helper proc name: e.g., TestlibCreateCtorReq -> TestlibCreateCtorBody
|
# Helper proc name: e.g., TestlibCreateCtorReq -> TestlibCreateCtorBody
|
||||||
let helperProcNameStr = reqTypeNameStr[0 ..^ ("CtorReq".len + 1)] & "CtorBody"
|
let helperProcNameStr = reqTypeNameStr[0 ..^ ("CtorReq".len + 1)] & "CtorBody"
|
||||||
let helperProcName = ident(helperProcNameStr)
|
let helperProcName = ident(helperProcNameStr)
|
||||||
let helperProc = buildCtorBodyProc(helperProcName, paramNames, paramTypes, libTypeName, bodyNode)
|
let helperProc =
|
||||||
let processProc =
|
buildCtorBodyProc(helperProcName, paramNames, paramTypes, libTypeName, bodyNode)
|
||||||
buildCtorProcessFFIRequestProc(reqTypeName, helperProcName, paramNames, paramTypes, libTypeName)
|
let processProc = buildCtorProcessFFIRequestProc(
|
||||||
|
reqTypeName, helperProcName, paramNames, paramTypes, libTypeName
|
||||||
|
)
|
||||||
let addToReg = addCtorRequestToRegistry(reqTypeName, libTypeName)
|
let addToReg = addCtorRequestToRegistry(reqTypeName, libTypeName)
|
||||||
|
|
||||||
# Build the C-exported proc params:
|
# Build the C-exported proc params:
|
||||||
@ -1311,9 +1339,8 @@ macro ffiCtor*(prc: untyped): untyped =
|
|||||||
return RET_ERR
|
return RET_ERR
|
||||||
|
|
||||||
# sendRequestToFFIThread using the gensym'd ctx
|
# sendRequestToFFIThread using the gensym'd ctx
|
||||||
let sendCall = newCall(
|
let sendCall =
|
||||||
newDotExpr(ctxSym, ident("sendRequestToFFIThread")), newReqCall
|
newCall(newDotExpr(ctxSym, ident("sendRequestToFFIThread")), newReqCall)
|
||||||
)
|
|
||||||
|
|
||||||
let sendResIdent = genSym(nskLet, "sendRes")
|
let sendResIdent = genSym(nskLet, "sendRes")
|
||||||
ffiBody.add quote do:
|
ffiBody.add quote do:
|
||||||
@ -1363,18 +1390,22 @@ macro ffiCtor*(prc: untyped): untyped =
|
|||||||
else:
|
else:
|
||||||
tn = $ptype
|
tn = $ptype
|
||||||
ctorExtraParams.add(FFIParamMeta(name: paramNames[i], typeName: tn, isPtr: isPtr))
|
ctorExtraParams.add(FFIParamMeta(name: paramNames[i], typeName: tn, isPtr: isPtr))
|
||||||
ffiProcRegistry.add(FFIProcMeta(
|
ffiProcRegistry.add(
|
||||||
procName: cleanName,
|
FFIProcMeta(
|
||||||
libName: currentLibName,
|
procName: cleanName,
|
||||||
kind: ffiCtorKind,
|
libName: currentLibName,
|
||||||
libTypeName: $libTypeName,
|
kind: ffiCtorKind,
|
||||||
extraParams: ctorExtraParams,
|
libTypeName: $libTypeName,
|
||||||
returnTypeName: $libTypeName,
|
extraParams: ctorExtraParams,
|
||||||
returnIsPtr: false,
|
returnTypeName: $libTypeName,
|
||||||
isAsync: true,
|
returnIsPtr: false,
|
||||||
))
|
isAsync: true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
result = newStmtList(typeDef, deleteProc, ffiNewReqProc, helperProc, processProc, addToReg, ffiProc)
|
result = newStmtList(
|
||||||
|
typeDef, deleteProc, ffiNewReqProc, helperProc, processProc, addToReg, ffiProc
|
||||||
|
)
|
||||||
|
|
||||||
when defined(ffiDumpMacros):
|
when defined(ffiDumpMacros):
|
||||||
echo result.repr
|
echo result.repr
|
||||||
@ -1384,28 +1415,39 @@ macro ffiCtor*(prc: untyped): untyped =
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
macro genBindings*(
|
macro genBindings*(
|
||||||
outputDir: static[string],
|
outputDir: static[string] = ffiOutputDir,
|
||||||
nimSrcRelPath: static[string] = "",
|
nimSrcRelPath: static[string] = ffiNimSrcRelPath,
|
||||||
): untyped =
|
): untyped =
|
||||||
## Generates binding files for the target language set by -d:ffiTargetLang=<lang>.
|
## Generates binding files for the language set by -d:targetLang=<lang>.
|
||||||
## Supported values: "rust" (default), "cpp" (case-insensitive).
|
## Supported values: "rust" (default), "cpp" (case-insensitive).
|
||||||
## Call at the END of your library file, after all {.ffiCtor.} and {.ffi.} procs.
|
## Output path and nim source path default to -d:ffiOutputDir and
|
||||||
|
## -d:ffiNimSrcRelPath, or can be passed as explicit arguments.
|
||||||
|
## This macro is a no-op unless -d:ffiGenBindings is set.
|
||||||
##
|
##
|
||||||
## Example:
|
## Example (all via compile flags):
|
||||||
## genBindings("examples/nim_timer/nim_bindings", "../nim_timer.nim")
|
## genBindings()
|
||||||
##
|
## # nim c -d:ffiGenBindings -d:targetLang=rust \
|
||||||
## Activate with: nim c -d:ffiGenBindings -d:ffiTargetLang=rust mylib.nim
|
## # -d:ffiOutputDir=examples/nim_timer/rust_bindings \
|
||||||
## or: nim c -d:ffiGenBindings -d:ffiTargetLang=cpp mylib.nim
|
## # -d:ffiNimSrcRelPath=../nim_timer.nim mylib.nim
|
||||||
|
|
||||||
when defined(ffiGenBindings):
|
when defined(ffiGenBindings):
|
||||||
let lang = ffiTargetLang.toLowerAscii()
|
if outputDir.len == 0:
|
||||||
|
error(
|
||||||
|
"genBindings: output directory is empty." &
|
||||||
|
" Pass it as an argument or set -d:ffiOutputDir=path/to/output"
|
||||||
|
)
|
||||||
|
let lang = targetLang.toLowerAscii()
|
||||||
let libName = deriveLibName(ffiProcRegistry)
|
let libName = deriveLibName(ffiProcRegistry)
|
||||||
case lang
|
case lang
|
||||||
of "rust":
|
of "rust":
|
||||||
generateRustCrate(ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath)
|
generateRustCrate(
|
||||||
|
ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath
|
||||||
|
)
|
||||||
of "cpp", "c++":
|
of "cpp", "c++":
|
||||||
generateCppBindings(ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath)
|
generateCppBindings(
|
||||||
|
ffiProcRegistry, ffiTypeRegistry, libName, outputDir, nimSrcRelPath
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
error("genBindings: unknown ffiTargetLang '" & lang & "'. Use 'rust' or 'cpp'.")
|
error("genBindings: unknown targetLang '" & lang & "'. Use 'rust' or 'cpp'.")
|
||||||
|
|
||||||
result = newEmptyNode()
|
result = newEmptyNode()
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
import std/[json, macros]
|
import std/[json, macros, sequtils, options]
|
||||||
import results
|
import results
|
||||||
import ./codegen/meta
|
import ./codegen/meta
|
||||||
|
|
||||||
proc ffiSerialize*(x: string): string =
|
proc ffiSerialize*(x: string): string =
|
||||||
$(%* x)
|
$(%*x)
|
||||||
|
|
||||||
proc ffiSerialize*(x: cstring): string =
|
proc ffiSerialize*(x: cstring): string =
|
||||||
if x.isNil: "null"
|
if x.isNil:
|
||||||
else: ffiSerialize($x)
|
"null"
|
||||||
|
else:
|
||||||
|
ffiSerialize($x)
|
||||||
|
|
||||||
proc ffiSerialize*(x: int): string =
|
proc ffiSerialize*(x: int): string =
|
||||||
$x
|
$x
|
||||||
@ -19,7 +21,7 @@ proc ffiSerialize*(x: bool): string =
|
|||||||
if x: "true" else: "false"
|
if x: "true" else: "false"
|
||||||
|
|
||||||
proc ffiSerialize*(x: float): string =
|
proc ffiSerialize*(x: float): string =
|
||||||
$(%* x)
|
$(%*x)
|
||||||
|
|
||||||
proc ffiSerialize*(x: pointer): string =
|
proc ffiSerialize*(x: pointer): string =
|
||||||
$cast[uint](x)
|
$cast[uint](x)
|
||||||
@ -67,6 +69,18 @@ proc ffiDeserialize*(s: cstring, _: typedesc[pointer]): Result[pointer, string]
|
|||||||
proc ffiSerialize*[T](x: ptr T): string =
|
proc ffiSerialize*[T](x: ptr T): string =
|
||||||
$cast[uint](x)
|
$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 ffiDeserialize*[T](s: cstring, _: typedesc[ptr T]): Result[ptr T, string] =
|
proc ffiDeserialize*[T](s: cstring, _: typedesc[ptr T]): Result[ptr T, string] =
|
||||||
try:
|
try:
|
||||||
let address = cast[ptr T](uint(parseJson($s).getBiggestInt()))
|
let address = cast[ptr T](uint(parseJson($s).getBiggestInt()))
|
||||||
@ -74,6 +88,38 @@ proc ffiDeserialize*[T](s: cstring, _: typedesc[ptr T]): Result[ptr T, string] =
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
err(e.msg)
|
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)
|
||||||
|
|
||||||
macro ffiType*(body: untyped): untyped =
|
macro ffiType*(body: untyped): untyped =
|
||||||
## Statement macro applied to a type declaration block.
|
## Statement macro applied to a type declaration block.
|
||||||
## Generates ffiSerialize and ffiDeserialize overloads for each type,
|
## Generates ffiSerialize and ffiDeserialize overloads for each type,
|
||||||
@ -116,15 +162,30 @@ macro ffiType*(body: untyped): untyped =
|
|||||||
|
|
||||||
ffiTypeRegistry.add(FFITypeMeta(name: typeNameStr, fields: fieldMetas))
|
ffiTypeRegistry.add(FFITypeMeta(name: typeNameStr, fields: fieldMetas))
|
||||||
|
|
||||||
let serializeProc = quote do:
|
let serializeProc = quote:
|
||||||
proc ffiSerialize*(x: `typeName`): string =
|
proc ffiSerialize*(x: `typeName`): string =
|
||||||
$(%* x)
|
$(%*x)
|
||||||
|
|
||||||
let deserializeProc = quote do:
|
var assignmentText = ""
|
||||||
proc ffiDeserialize*(s: cstring, _: typedesc[`typeName`]): Result[`typeName`, string] =
|
for field in fieldMetas:
|
||||||
|
if assignmentText.len > 0:
|
||||||
|
assignmentText &= "\n"
|
||||||
|
assignmentText &=
|
||||||
|
" result[\"" & field.name & "\"] = parseJson(ffiSerialize(x." & field.name & "))"
|
||||||
|
let jsonProc = parseStmt(
|
||||||
|
"proc `%`*(x: " & typeNameStr & "): JsonNode =\n var result = newJObject()\n" &
|
||||||
|
assignmentText & "\n return result\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
let importJson = quote:
|
||||||
|
import json
|
||||||
|
let deserializeProc = quote:
|
||||||
|
proc ffiDeserialize*(
|
||||||
|
s: cstring, _: typedesc[`typeName`]
|
||||||
|
): Result[`typeName`, string] =
|
||||||
try:
|
try:
|
||||||
ok(parseJson($s).to(`typeName`))
|
ok(parseJson($s).to(`typeName`))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err(e.msg)
|
err(e.msg)
|
||||||
|
|
||||||
result = newStmtList(body, serializeProc, deserializeProc)
|
result = newStmtList(importJson, body, serializeProc, jsonProc, deserializeProc)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user