91e05e3c26
## 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.
65 lines
2.3 KiB
C
65 lines
2.3 KiB
C
/* 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;
|
|
}
|