C ABI hub crate (bindings/c) foundation (#222)
## What
Introduces `wickra-c` — a `cdylib` + `staticlib` that exposes the Rust core over a **C ABI**. This is the hub every C-capable language (C, C++, Go, C#, Java, R) links against, instead of re-wiring each indicator natively. The native Python/Node/WASM bindings are untouched; this is purely additive, for ecosystems without first-class Rust tooling.
## Scope (foundation slice)
This PR deliberately validates the **whole pipeline end to end with one indicator (SMA)** before scaling to all 514, so the CI / cross-OS / header-drift mechanics are proven green first.
- Opaque `*mut T` handles; `wickra_<ind>_{new,update,batch,reset,free}`.
- NaN sentinel for warmup / NULL handles; caller-owned batch buffers; every function NULL-safe.
- cbindgen generates and commits `bindings/c/include/wickra.h` with opaque handle typedefs.
- A C smoke example (`examples/c/`) links the header + compiled library and runs (CMake + ctest).
- A `c-abi` CI job builds the library and runs the smoke test on **Linux, macOS and Windows**, plus a header drift check on Linux.
## Notes
- The per-indicator FFI blocks are plain `#[no_mangle]` functions, **not** a macro: cbindgen cannot see macro-generated functions on stable Rust (macro expansion needs nightly), so the blocks are written literally and will be generated mechanically by the ScriptHelpers `capi` wrapper in a follow-up (same model as the committed-but-generated Node `index.js`).
- `bindings/c` cannot inherit the workspace `forbid(unsafe_code)` lint (the C boundary needs raw pointers), so it mirrors every workspace lint and only relaxes `unsafe_code`. The Rust core stays `unsafe`-forbidden.
## Follow-ups (separate PRs)
- ScriptHelpers `capi` generator + wire the scalar family (~235).
- Hand-written blocks for multi-output / custom-input / bars (~279).
- Docs consistency wave (README / docs / webpage: Python·Node·WASM·Rust → +C).
- Release wiring (native-lib matrix + header/lib GH-release assets) — gated.
This commit is contained in:
@@ -21,6 +21,19 @@ The Rust examples live in the `wickra-examples` workspace member crate.
|
||||
| `strategy_macd_adx.rs` | Hourly BTCUSDT trend-follower: MACD crossover entries gated by ADX(14) > 20. | `cargo run --release -p wickra-examples --bin strategy_macd_adx` |
|
||||
| `strategy_bollinger_squeeze.rs` | Daily BTCUSDT Bollinger-squeeze breakout with ATR(14) trailing stop. | `cargo run --release -p wickra-examples --bin strategy_bollinger_squeeze` |
|
||||
|
||||
## C / C++ — `examples/c/`
|
||||
|
||||
Build the library first (`cargo build -p wickra-c --release`), then build and run
|
||||
the examples via CMake:
|
||||
`cmake -S examples/c -B examples/c/build -DWICKRA_LIB_DIR="$PWD/target/release"` →
|
||||
`cmake --build examples/c/build` → `ctest --test-dir examples/c/build`.
|
||||
|
||||
| Example | What it does | CMake target |
|
||||
| --- | --- | --- |
|
||||
| `smoke.c` | Links the generated header + library and asserts SMA streaming / batch values across the boundary. | `smoke` |
|
||||
| `streaming.c` | Feed a tick stream through an EMA, printing each value (NaN during warmup). | `streaming` |
|
||||
| `smoke.cpp` | C++ RAII via `wickra::Handle` from [`wickra.hpp`](../bindings/c/include/wickra.hpp): construct, move, auto-free. | `cpp_smoke` |
|
||||
|
||||
## Python — `examples/python/`
|
||||
|
||||
| Example | What it does | Run |
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(wickra_c_examples C CXX)
|
||||
|
||||
# Directory holding the compiled Wickra C library (cargo output), e.g.
|
||||
# <workspace>/target/release. Override with -DWICKRA_LIB_DIR=/path/to/target/release.
|
||||
if(NOT DEFINED WICKRA_LIB_DIR)
|
||||
set(WICKRA_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../target/release")
|
||||
endif()
|
||||
set(WICKRA_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../bindings/c/include")
|
||||
|
||||
# Pick the right link target per platform/toolchain.
|
||||
# - MSVC links the generated import library (wickra.dll.lib).
|
||||
# - MinGW/gcc on Windows links the DLL directly.
|
||||
# - Unix links the shared object / dylib.
|
||||
if(WIN32)
|
||||
set(WICKRA_RUNTIME "${WICKRA_LIB_DIR}/wickra.dll")
|
||||
if(MSVC)
|
||||
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/wickra.dll.lib")
|
||||
else()
|
||||
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/wickra.dll")
|
||||
endif()
|
||||
elseif(APPLE)
|
||||
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/libwickra.dylib")
|
||||
else()
|
||||
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/libwickra.so")
|
||||
endif()
|
||||
|
||||
enable_testing()
|
||||
|
||||
# Build one example, link it to the Wickra library, run it as a ctest. On Windows
|
||||
# the DLL is copied next to the executable so the loader finds it at run time.
|
||||
function(add_wickra_example name source)
|
||||
add_executable(${name} ${source})
|
||||
target_include_directories(${name} PRIVATE "${WICKRA_INCLUDE_DIR}")
|
||||
target_link_libraries(${name} PRIVATE "${WICKRA_LINK_LIB}")
|
||||
if(UNIX AND NOT APPLE)
|
||||
target_link_libraries(${name} PRIVATE m)
|
||||
endif()
|
||||
if(WIN32)
|
||||
add_custom_command(TARGET ${name} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${WICKRA_RUNTIME}" "$<TARGET_FILE_DIR:${name}>")
|
||||
endif()
|
||||
add_test(NAME ${name} COMMAND ${name})
|
||||
if(NOT WIN32)
|
||||
set_tests_properties(${name} PROPERTIES
|
||||
ENVIRONMENT "LD_LIBRARY_PATH=${WICKRA_LIB_DIR};DYLD_LIBRARY_PATH=${WICKRA_LIB_DIR}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
add_wickra_example(smoke smoke.c) # links the boundary, asserts values
|
||||
add_wickra_example(streaming streaming.c) # runnable streaming demo
|
||||
add_wickra_example(cpp_smoke smoke.cpp) # C++ RAII wrapper (wickra.hpp)
|
||||
@@ -0,0 +1,68 @@
|
||||
# Wickra — C / C++ examples
|
||||
|
||||
The Wickra C ABI is a single shared/static library plus a generated header
|
||||
([`bindings/c/include/wickra.h`](../../bindings/c/include/wickra.h)). Any
|
||||
C-capable language links against the same artifact; these examples show the
|
||||
plain-C path.
|
||||
|
||||
## Build the library
|
||||
|
||||
From the workspace root:
|
||||
|
||||
```sh
|
||||
cargo build -p wickra-c --release
|
||||
```
|
||||
|
||||
This produces, in `target/release/`:
|
||||
|
||||
| Platform | Shared library | Link target |
|
||||
|----------|----------------|-------------|
|
||||
| Linux | `libwickra.so` | `-lwickra` |
|
||||
| macOS | `libwickra.dylib` | `-lwickra` |
|
||||
| Windows (MSVC) | `wickra.dll` | `wickra.dll.lib` (import lib) |
|
||||
|
||||
A static library (`libwickra.a` / `wickra.lib`) is emitted alongside.
|
||||
|
||||
## Build and run the smoke example
|
||||
|
||||
### With CMake (portable, used by CI)
|
||||
|
||||
```sh
|
||||
cmake -S examples/c -B examples/c/build -DWICKRA_LIB_DIR="$PWD/target/release"
|
||||
cmake --build examples/c/build
|
||||
ctest --test-dir examples/c/build --output-on-failure
|
||||
```
|
||||
|
||||
### Directly with a compiler
|
||||
|
||||
```sh
|
||||
# Linux / macOS
|
||||
cc examples/c/smoke.c -I bindings/c/include -L target/release -lwickra -lm -o smoke
|
||||
LD_LIBRARY_PATH=target/release ./smoke # macOS: DYLD_LIBRARY_PATH
|
||||
|
||||
# Windows (MinGW gcc, linking the DLL directly)
|
||||
gcc examples/c/smoke.c -I bindings/c/include target/release/wickra.dll -lm -o smoke.exe
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```
|
||||
OK: wickra C ABI smoke passed (SMA streaming + batch + reset + NULL-safety + free)
|
||||
```
|
||||
|
||||
## Usage shape
|
||||
|
||||
Every indicator follows the same five-function pattern over an opaque handle:
|
||||
|
||||
```c
|
||||
#include "wickra.h"
|
||||
|
||||
struct Sma *sma = wickra_sma_new(14); /* NULL on invalid params */
|
||||
double v = wickra_sma_update(sma, 42.0); /* NaN during warmup */
|
||||
wickra_sma_reset(sma); /* back to fresh state */
|
||||
wickra_sma_free(sma); /* exactly once per _new */
|
||||
```
|
||||
|
||||
There is no RAII across the C boundary: every `wickra_<ind>_new` must be paired
|
||||
with exactly one `wickra_<ind>_free`. All functions are NULL-safe (a NULL handle
|
||||
yields `NaN` / a no-op, never a crash).
|
||||
@@ -0,0 +1,64 @@
|
||||
/* Smoke test for the Wickra C ABI.
|
||||
*
|
||||
* This is the one test the Rust unit tests structurally cannot do: it links a
|
||||
* foreign C consumer against the generated `wickra.h` + the compiled library and
|
||||
* exercises the real FFI boundary (symbol export, header correctness, opaque
|
||||
* handle, pointer ownership, `_free`). If this passes, every C-capable language
|
||||
* (C, C++, Go, C#, Java, R) can link the same way.
|
||||
*
|
||||
* Build (from the workspace root, after `cargo build -p wickra-c --release`):
|
||||
* cc examples/c/smoke.c -I bindings/c/include target/release/<lib> -lm -o smoke
|
||||
*/
|
||||
|
||||
#include "wickra.h"
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static int near(double a, double b) { return fabs(a - b) < 1e-9; }
|
||||
|
||||
int main(void) {
|
||||
struct Sma *sma = wickra_sma_new(3);
|
||||
if (sma == NULL) {
|
||||
printf("FAIL: wickra_sma_new returned NULL\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* SMA(3): first two outputs are warmup (NaN), then the trailing mean. */
|
||||
double in[5] = {1.0, 2.0, 3.0, 4.0, 5.0};
|
||||
double r0 = wickra_sma_update(sma, in[0]); /* NaN (1/3) */
|
||||
double r1 = wickra_sma_update(sma, in[1]); /* NaN (2/3) */
|
||||
double r2 = wickra_sma_update(sma, in[2]); /* 2.0 (1+2+3)/3 */
|
||||
double r3 = wickra_sma_update(sma, in[3]); /* 3.0 (2+3+4)/3 */
|
||||
|
||||
if (!isnan(r0) || !isnan(r1)) {
|
||||
printf("FAIL: warmup not NaN (%f %f)\n", r0, r1);
|
||||
return 1;
|
||||
}
|
||||
if (!near(r2, 2.0) || !near(r3, 3.0)) {
|
||||
printf("FAIL: streaming values (%f %f), expected (2.0 3.0)\n", r2, r3);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Batch over a reset instance must reproduce the streaming result. */
|
||||
wickra_sma_reset(sma);
|
||||
double out[5];
|
||||
wickra_sma_batch(sma, in, out, 5);
|
||||
if (!isnan(out[0]) || !isnan(out[1]) ||
|
||||
!near(out[2], 2.0) || !near(out[3], 3.0) || !near(out[4], 4.0)) {
|
||||
printf("FAIL: batch mismatch (%f %f %f %f %f)\n",
|
||||
out[0], out[1], out[2], out[3], out[4]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* NULL handle is a defined no-op / NaN, never a crash. */
|
||||
if (!isnan(wickra_sma_update(NULL, 1.0))) {
|
||||
printf("FAIL: NULL update did not return NaN\n");
|
||||
return 1;
|
||||
}
|
||||
wickra_sma_reset(NULL);
|
||||
wickra_sma_free(NULL);
|
||||
|
||||
wickra_sma_free(sma);
|
||||
printf("OK: wickra C ABI smoke passed (SMA streaming + batch + reset + NULL-safety + free)\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// C++ smoke test for the Wickra C ABI via the optional RAII wrapper (`wickra.hpp`).
|
||||
//
|
||||
// Validates that the header compiles as C++ and that `wickra::Handle` constructs,
|
||||
// moves, and frees correctly across the boundary.
|
||||
|
||||
#include "wickra.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <utility>
|
||||
|
||||
int main() {
|
||||
wickra::Handle<Sma, wickra_sma_free> sma(wickra_sma_new(3));
|
||||
if (!sma) {
|
||||
std::puts("FAIL: wickra_sma_new returned null");
|
||||
return 1;
|
||||
}
|
||||
|
||||
(void)wickra_sma_update(sma.get(), 1.0);
|
||||
(void)wickra_sma_update(sma.get(), 2.0);
|
||||
double value = wickra_sma_update(sma.get(), 3.0);
|
||||
if (std::fabs(value - 2.0) > 1e-9) {
|
||||
std::printf("FAIL: SMA(3) value %.6f, expected 2.0\n", value);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Move transfers ownership; the moved-from handle must not double-free.
|
||||
wickra::Handle<Sma, wickra_sma_free> moved(std::move(sma));
|
||||
if (static_cast<bool>(sma) || !static_cast<bool>(moved)) {
|
||||
std::puts("FAIL: move semantics");
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::puts("OK: wickra C++ RAII smoke passed (Handle construct + move + auto-free)");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/* Streaming usage example for the Wickra C ABI.
|
||||
*
|
||||
* The same five-function shape (new / update / batch / reset / free) drives every
|
||||
* scalar indicator. Here an EMA consumes a live tick stream one value at a time;
|
||||
* `update` is O(1) per tick and returns NaN until the indicator has warmed up.
|
||||
*
|
||||
* Build (after `cargo build -p wickra-c --release`):
|
||||
* cc examples/c/streaming.c -I bindings/c/include -L target/release -lwickra -lm -o streaming
|
||||
*/
|
||||
|
||||
#include "wickra.h"
|
||||
#include <stdio.h>
|
||||
|
||||
int main(void) {
|
||||
struct Ema *ema = wickra_ema_new(5);
|
||||
if (ema == NULL) {
|
||||
fprintf(stderr, "failed to create EMA\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const double prices[] = {10.0, 10.5, 11.0, 10.8, 11.2, 11.5, 11.3, 11.8};
|
||||
const size_t n = sizeof(prices) / sizeof(prices[0]);
|
||||
|
||||
printf("EMA(5) streaming:\n");
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
double value = wickra_ema_update(ema, prices[i]);
|
||||
if (value != value) { /* NaN during warmup */
|
||||
printf(" tick %zu price %.2f -> (warming up)\n", i, prices[i]);
|
||||
} else {
|
||||
printf(" tick %zu price %.2f -> %.4f\n", i, prices[i], value);
|
||||
}
|
||||
}
|
||||
|
||||
wickra_ema_free(ema);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user