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:
@@ -1,3 +1,8 @@
|
||||
# Shell scripts must keep LF line endings so they run on Linux/macOS CI and
|
||||
# local shells regardless of the committer's platform autocrlf setting.
|
||||
*.sh text eol=lf
|
||||
|
||||
# The cbindgen-generated C header is committed; pin it to LF so its CI drift
|
||||
# check (regenerate + `git diff`) never trips on a CRLF normalization.
|
||||
bindings/c/include/wickra.h text eol=lf
|
||||
|
||||
|
||||
@@ -622,6 +622,61 @@ jobs:
|
||||
working-directory: bindings/node
|
||||
run: node --test __tests__/
|
||||
|
||||
c-abi:
|
||||
name: C ABI on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
timeout-minutes: 6
|
||||
|
||||
- name: Install cbindgen
|
||||
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
||||
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
|
||||
with:
|
||||
tool: cbindgen
|
||||
|
||||
- name: Build the C ABI library (cdylib + staticlib)
|
||||
run: cargo build -p wickra-c --release
|
||||
|
||||
- name: Rust unit tests
|
||||
run: cargo test -p wickra-c
|
||||
|
||||
# The generated header is platform-independent, so checking drift on one OS
|
||||
# is enough — and avoids a spurious CRLF/LF diff on the Windows runner.
|
||||
- name: Check the committed header is in sync with cbindgen
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
cbindgen --config bindings/c/cbindgen.toml --crate wickra-c --output bindings/c/include/wickra.h
|
||||
if ! git diff --quiet -- bindings/c/include/wickra.h; then
|
||||
echo "::error::bindings/c/include/wickra.h is out of sync — run cbindgen and commit the result"
|
||||
git --no-pager diff -- bindings/c/include/wickra.h
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The real cross-language test: a foreign C consumer links the generated
|
||||
# header + the compiled library and runs. If this passes on all three OSes,
|
||||
# every C-capable language can link the same way.
|
||||
- name: Build and run the C smoke example (CMake + ctest)
|
||||
shell: bash
|
||||
run: |
|
||||
cmake -S examples/c -B examples/c/build
|
||||
cmake --build examples/c/build --config Release
|
||||
ctest --test-dir examples/c/build -C Release --output-on-failure
|
||||
|
||||
# The cross-library benchmark has moved to a dedicated scheduled workflow
|
||||
# (.github/workflows/bench.yml) — see audit finding R10. It runs nightly
|
||||
# at 03:00 UTC and on-demand via `workflow_dispatch`, and is no longer on
|
||||
|
||||
@@ -569,9 +569,64 @@ jobs:
|
||||
# the old "publish, then upload provenance" order would have the provenance
|
||||
# upload rejected once immutability is enabled.
|
||||
# --------------------------------------------------------------------------
|
||||
# --------------------------------------------------------------------------
|
||||
# C ABI native libraries (bindings/c) — built per target on a native runner
|
||||
# (no cross toolchain needed) and attached to the GitHub Release as the
|
||||
# distribution channel. There is no package registry for the C ABI.
|
||||
# --------------------------------------------------------------------------
|
||||
c-abi-build:
|
||||
name: C ABI library (${{ matrix.target }})
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { host: ubuntu-latest, target: x86_64-unknown-linux-gnu }
|
||||
- { host: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu }
|
||||
- { host: macos-latest, target: x86_64-apple-darwin }
|
||||
- { host: macos-latest, target: aarch64-apple-darwin }
|
||||
- { host: windows-latest, target: x86_64-pc-windows-msvc }
|
||||
- { host: windows-11-arm, target: aarch64-pc-windows-msvc }
|
||||
runs-on: ${{ matrix.host }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
timeout-minutes: 6
|
||||
|
||||
- name: Build the C ABI library (cdylib + staticlib)
|
||||
run: cargo build -p wickra-c --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Package header + libraries
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
dir="wickra-c-${{ matrix.target }}"
|
||||
mkdir -p "$dir/include" "$dir/lib"
|
||||
cp bindings/c/include/wickra.h bindings/c/include/wickra.hpp "$dir/include/"
|
||||
for f in libwickra.so libwickra.a libwickra.dylib wickra.dll wickra.dll.lib wickra.lib; do
|
||||
src="target/${{ matrix.target }}/release/$f"
|
||||
[ -f "$src" ] && cp "$src" "$dir/lib/"
|
||||
done
|
||||
tar -czf "$dir.tar.gz" "$dir"
|
||||
echo "packaged $dir:"; ls -lR "$dir"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: c-abi-${{ matrix.target }}
|
||||
path: wickra-c-${{ matrix.target }}.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
github-release:
|
||||
name: Attach assets to the draft GitHub Release
|
||||
needs: [cargo-publish, python-publish, node-publish, wasm-publish]
|
||||
needs: [cargo-publish, python-publish, node-publish, wasm-publish, c-abi-build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -644,7 +699,7 @@ jobs:
|
||||
# the provenance bundle is attached (P24, immutability-ready).
|
||||
draft: true
|
||||
body: |
|
||||
Wickra ${{ github.ref_name }} — streaming-first technical indicators across 4 language registries.
|
||||
Wickra ${{ github.ref_name }} — streaming-first technical indicators across 4 language registries plus a C ABI.
|
||||
|
||||
### Install
|
||||
|
||||
@@ -665,6 +720,9 @@ jobs:
|
||||
darwin-arm64, win32-x64-msvc)
|
||||
- `wickra-*.tgz` — npm-pack tarballs (main package + per-platform subpackages + WASM)
|
||||
- `*.crate` — cargo source crates (wickra-core, wickra-data, wickra)
|
||||
- `wickra-c-<target>.tar.gz` — C ABI: `include/wickra.h` + `wickra.hpp`
|
||||
and the cdylib/staticlib per target (linux/macos/windows × x64/arm64),
|
||||
the hub for C / C++ / Go / C# / Java / R
|
||||
|
||||
### Auto-generated changelog
|
||||
|
||||
|
||||
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- **C ABI (`bindings/c`)** — a `cdylib` + `staticlib` plus a generated
|
||||
`include/wickra.h` exposing all 514 indicators and 10 bar builders over an
|
||||
opaque-handle C ABI: the hub any C-capable language (C, C++, Go, C#, Java, R)
|
||||
links against, complementing the native Python/Node/WASM bindings. Ships C
|
||||
smoke + streaming examples and an optional `wickra.hpp` C++ RAII wrapper.
|
||||
|
||||
## [0.7.4] - 2026-06-08
|
||||
- **Three-Line Break** — Three-line-break bars (reversal needs N-line break) (`THREE_LINE_BREAK_BARS`).
|
||||
|
||||
+4
-1
@@ -21,6 +21,7 @@ licensed as above, without any additional terms or conditions.
|
||||
| `bindings/python` | PyO3 bindings (`wickra` on PyPI). |
|
||||
| `bindings/node` | napi-rs bindings (`wickra` on npm). |
|
||||
| `bindings/wasm` | wasm-bindgen bindings (`wickra-wasm` on npm). |
|
||||
| `bindings/c` | C ABI — `cdylib` + `staticlib` + generated `include/wickra.h`. The hub for C / C++ and any C-capable language. |
|
||||
| `examples/` | Runnable examples. |
|
||||
| `docs/` | Pointer to the documentation site (docs.wickra.org); the docs live in the `wickra-lib/wickra-docs` repo. |
|
||||
|
||||
@@ -102,7 +103,9 @@ installed. Dependabot also keeps the `.github/requirements` pins current.
|
||||
- **Streaming parity.** An indicator's `batch` output must equal the sequence
|
||||
of `update` calls.
|
||||
- **Bindings.** A change to a public indicator API must be mirrored across the
|
||||
Python, Node, and WASM bindings, including their type stubs / `.d.ts`.
|
||||
Python, Node, and WASM bindings, including their type stubs / `.d.ts`. The C ABI
|
||||
(`bindings/c`) is generated from the core, so regenerate it (the ScriptHelpers
|
||||
`capi` wrapper) and commit `src/lib.rs` + `include/wickra.h`.
|
||||
- **Docs.** Update the relevant page on the
|
||||
[documentation site](https://docs.wickra.org) and the
|
||||
`README.md` when behaviour or the public API changes. The docs live in
|
||||
|
||||
Generated
+7
@@ -1965,6 +1965,13 @@ dependencies = [
|
||||
"yata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-c"
|
||||
version = "0.7.4"
|
||||
dependencies = [
|
||||
"wickra-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.7.4"
|
||||
|
||||
@@ -7,6 +7,7 @@ members = [
|
||||
"bindings/python",
|
||||
"bindings/wasm",
|
||||
"bindings/node",
|
||||
"bindings/c",
|
||||
"examples/rust",
|
||||
"crates/wickra-bench",
|
||||
]
|
||||
|
||||
@@ -18,8 +18,9 @@
|
||||
**Streaming-first technical indicators. Install with `pip install wickra` — no system dependencies.**
|
||||
|
||||
Wickra is a multi-language technical-analysis library with a Rust core and
|
||||
bindings for Python, Node.js, and WebAssembly. Every indicator is a state
|
||||
machine that updates in O(1) per new data point, so live trading bots and
|
||||
native bindings for Python, Node.js and WebAssembly, plus a C ABI that any
|
||||
C-capable language (C, C++, and beyond) links against. Every indicator is a
|
||||
state machine that updates in O(1) per new data point, so live trading bots and
|
||||
historical backtests share the exact same implementation.
|
||||
|
||||
```python
|
||||
@@ -71,9 +72,10 @@ times to get there.
|
||||
breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance
|
||||
metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and
|
||||
none of them stream.
|
||||
- **One Rust core, four first-class targets.** Native **Python · Node.js ·
|
||||
WebAssembly · Rust** — identical math, identical results, zero per-language
|
||||
reimplementation and zero GIL bottleneck.
|
||||
- **One Rust core, five first-class targets.** Native **Python · Node.js ·
|
||||
WebAssembly · Rust** plus a **C ABI** for C / C++ and any C-capable language —
|
||||
identical math, identical results, zero per-language reimplementation and zero
|
||||
GIL bottleneck.
|
||||
- **Correct by construction, not by hope.** Every `update` validates its input,
|
||||
runs a real warmup, and returns an `Option` so a single bad tick can't silently
|
||||
poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered
|
||||
@@ -95,7 +97,7 @@ Every other library forces one of those compromises. Wickra doesn't:
|
||||
|
||||
| Library | Install | Streaming | Languages | Indicators | Active |
|
||||
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
|
||||
| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **514** | **yes** |
|
||||
| **★ Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust · C** | **514** | **yes** |
|
||||
| kand | clean | yes | Python · WASM · Rust | ~60 | yes |
|
||||
| ta-rs | clean | yes | Rust only | ~30 | stale |
|
||||
| yata | clean | partial | Rust only | ~35 | yes |
|
||||
@@ -166,8 +168,8 @@ as one column each. `Doji` is direction-less by default (`+1.0` / `0.0`);
|
||||
construct it in signed mode (`Doji::new().signed()`, `Doji(signed=True)`,
|
||||
`new Doji(true)`) for a dragonfly / gravestone `±1` reading.
|
||||
|
||||
Adding a new indicator means implementing one trait in Rust; all four bindings
|
||||
inherit it automatically.
|
||||
Adding a new indicator means implementing one trait in Rust; all five bindings
|
||||
inherit it automatically (the C ABI is generated from the core).
|
||||
|
||||
## Languages
|
||||
|
||||
@@ -177,12 +179,14 @@ inherit it automatically.
|
||||
| Node.js (napi-rs) | `npm install wickra` | `examples/node/backtest.js` |
|
||||
| Browser / WASM | `npm install wickra-wasm` | `examples/wasm/index.html` |
|
||||
| Rust | `cargo add wickra` | `examples/rust/src/bin/backtest.rs` |
|
||||
| C / C++ (C ABI) | header + library, see [`bindings/c`](bindings/c) | `examples/c/streaming.c` |
|
||||
|
||||
Each binding ships several runnable examples (streaming, backtest, live feed);
|
||||
[`examples/README.md`](examples/README.md) is the full cross-language index.
|
||||
|
||||
The wickra-core crate is `unsafe`-forbidden, so every binding inherits a
|
||||
memory-safe implementation.
|
||||
The wickra-core crate is `unsafe`-forbidden, so the native bindings are
|
||||
memory-safe end to end. The C ABI runs the same safe core; only its thin FFI
|
||||
boundary uses `unsafe`, and the caller owns handle lifetimes (`_new` / `_free`).
|
||||
|
||||
## Rust API
|
||||
|
||||
@@ -244,13 +248,15 @@ wickra/
|
||||
├── bindings/
|
||||
│ ├── python/ PyO3 + maturin (publishes on PyPI)
|
||||
│ ├── node/ napi-rs (publishes on npm)
|
||||
│ └── wasm/ wasm-bindgen (browsers, bundlers, Node)
|
||||
│ ├── wasm/ wasm-bindgen (browsers, bundlers, Node)
|
||||
│ └── c/ C ABI (cdylib + staticlib) + generated include/wickra.h
|
||||
├── examples/ examples/README.md indexes every language
|
||||
│ ├── data/ real BTCUSDT OHLCV datasets, one per timeframe
|
||||
│ ├── rust/ Rust workspace member (`wickra-examples`)
|
||||
│ ├── python/ backtest, live trading, parallel assets, multi-tf
|
||||
│ ├── node/ streaming, backtest, live trading (load `wickra`)
|
||||
│ └── wasm/ browser demo for `wickra-wasm`
|
||||
│ ├── wasm/ browser demo for `wickra-wasm`
|
||||
│ └── c/ C smoke + streaming, C++ RAII wrapper
|
||||
└── .github/workflows/ CI and release pipelines
|
||||
```
|
||||
|
||||
@@ -278,6 +284,11 @@ wasm-pack build bindings/wasm --target web --release --features panic-hook
|
||||
|
||||
# Node binding (requires @napi-rs/cli)
|
||||
cd bindings/node && npm install && npm run build && npm test
|
||||
|
||||
# C ABI (cdylib + staticlib + generated header)
|
||||
cargo build -p wickra-c --release
|
||||
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
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
[package]
|
||||
name = "wickra-c"
|
||||
description = "C ABI (cdylib + staticlib) for the Wickra streaming-first technical indicators library — the hub every C-capable language (C, C++, Go, C#, Java, R) links against."
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
readme = "README.md"
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "wickra"
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
# The C ABI inherently needs `unsafe` (raw pointers across the FFI boundary,
|
||||
# `#[export_name]` symbol control). The workspace forbids `unsafe_code`, so this
|
||||
# crate cannot inherit `workspace = true`; it mirrors every workspace lint and
|
||||
# only relaxes `unsafe_code` to `allow` (parity with how the proc-macro bindings
|
||||
# emit their unsafe). The Rust core stays `unsafe`-forbidden — this is the one
|
||||
# crate where the boundary lives.
|
||||
[lints.rust]
|
||||
unsafe_code = "allow"
|
||||
missing_debug_implementations = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unused_must_use = "deny"
|
||||
|
||||
[lints.clippy]
|
||||
all = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_possible_truncation = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
similar_names = "allow"
|
||||
float_cmp = "allow"
|
||||
|
||||
[dependencies]
|
||||
wickra-core = { workspace = true }
|
||||
@@ -0,0 +1,59 @@
|
||||
# wickra-c
|
||||
|
||||
C ABI for [Wickra](https://github.com/wickra-lib/wickra) — streaming-first
|
||||
technical indicators with a Rust core. This crate is the **hub**: it compiles the
|
||||
core to a C-compatible shared/static library plus a generated header, so any
|
||||
C-capable language (C, C++, Go, C#, Java, R) links against one artifact instead
|
||||
of re-wiring every indicator natively.
|
||||
|
||||
The native Python, Node, and WebAssembly bindings are unaffected — this is
|
||||
additive, for the ecosystems without first-class Rust tooling.
|
||||
|
||||
## Artifacts
|
||||
|
||||
```sh
|
||||
cargo build -p wickra-c --release
|
||||
```
|
||||
|
||||
- `target/release/libwickra.{so,dylib}` / `wickra.dll` (+ `wickra.dll.lib` import lib on Windows)
|
||||
- `target/release/libwickra.a` / `wickra.lib` (static)
|
||||
- [`include/wickra.h`](include/wickra.h) — generated by cbindgen, committed.
|
||||
|
||||
## API shape
|
||||
|
||||
Each indicator is exposed as five `extern "C"` functions over an opaque handle:
|
||||
|
||||
```c
|
||||
struct Sma *wickra_sma_new(uintptr_t period); /* NULL on bad params */
|
||||
double wickra_sma_update(struct Sma *h, double value); /* NaN during warmup */
|
||||
void wickra_sma_batch(struct Sma *h, const double *in, double *out, uintptr_t n);
|
||||
void wickra_sma_reset(struct Sma *h);
|
||||
void wickra_sma_free(struct Sma *h); /* exactly once per _new */
|
||||
```
|
||||
|
||||
Conventions:
|
||||
|
||||
- **Opaque handles.** `wickra_<ind>_new` returns a `T *` you must release with
|
||||
exactly one `wickra_<ind>_free`. There is no RAII across the boundary.
|
||||
- **NaN sentinel.** Scalar outputs return `NaN` while warming up or on a `NULL`
|
||||
handle, mirroring the other bindings — no error codes for the common path.
|
||||
- **Caller-owned batch buffers.** `wickra_<ind>_batch` writes one output per
|
||||
input into a buffer you provide; nothing is allocated across the boundary.
|
||||
- **NULL-safe.** Every function tolerates a `NULL` handle without crashing.
|
||||
|
||||
## Header regeneration
|
||||
|
||||
The header is generated and committed; CI checks it is in sync:
|
||||
|
||||
```sh
|
||||
cbindgen --config bindings/c/cbindgen.toml --crate wickra-c --output bindings/c/include/wickra.h
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Runnable C examples (build via CMake or a direct compiler invocation) live in
|
||||
[`examples/c`](../../examples/c).
|
||||
|
||||
## License
|
||||
|
||||
`MIT OR Apache-2.0`, the same as the rest of Wickra.
|
||||
@@ -0,0 +1,20 @@
|
||||
language = "C"
|
||||
header = "/* Wickra C ABI — generated by cbindgen. Do not edit by hand. */"
|
||||
include_guard = "WICKRA_H"
|
||||
pragma_once = true
|
||||
# Wrap the declarations in `extern "C"` under __cplusplus so the header is usable
|
||||
# from C++ (the optional wickra.hpp RAII layer and any C++ consumer).
|
||||
cpp_compat = true
|
||||
tab_width = 4
|
||||
# Off: cbindgen copies the Rust struct/fn doc comments verbatim, and some core
|
||||
# indicator docs contain markdown (e.g. `**1/8**/**7/8**`) whose `*/` would close
|
||||
# the C block comment early and break the header. Usage docs live in the crate
|
||||
# README and examples; the header is a pure declaration contract.
|
||||
documentation = false
|
||||
|
||||
[parse]
|
||||
# Parse wickra-core too so the opaque indicator handle types (Sma, Ema, …) are
|
||||
# discovered and emitted as forward-declared opaque structs. Their fields are
|
||||
# never exposed — only `T *` handles cross the boundary.
|
||||
parse_deps = true
|
||||
include = ["wickra-core"]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
// Optional C++ convenience layer over the Wickra C ABI (`wickra.h`).
|
||||
//
|
||||
// The C ABI hands out raw handles that must be released exactly once with the
|
||||
// matching `wickra_<ind>_free`. `wickra::Handle` wraps that in a move-only RAII
|
||||
// owner so the free happens automatically at scope exit:
|
||||
//
|
||||
// #include "wickra.hpp"
|
||||
//
|
||||
// wickra::Handle<Sma, wickra_sma_free> sma(wickra_sma_new(14));
|
||||
// if (sma) {
|
||||
// double v = wickra_sma_update(sma.get(), 42.0); // NaN during warmup
|
||||
// }
|
||||
// // sma is freed here
|
||||
//
|
||||
// This is header-only and adds no runtime cost beyond the C calls themselves.
|
||||
|
||||
#ifndef WICKRA_HPP
|
||||
#define WICKRA_HPP
|
||||
|
||||
#include "wickra.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace wickra {
|
||||
|
||||
/// Move-only RAII owner of a Wickra handle. `T` is the opaque indicator type and
|
||||
/// `Free` its `wickra_<ind>_free` function.
|
||||
template <typename T, void (*Free)(T *)>
|
||||
class Handle {
|
||||
public:
|
||||
explicit Handle(T *ptr) noexcept : ptr_(ptr) {}
|
||||
|
||||
~Handle() {
|
||||
if (ptr_ != nullptr) {
|
||||
Free(ptr_);
|
||||
}
|
||||
}
|
||||
|
||||
Handle(const Handle &) = delete;
|
||||
Handle &operator=(const Handle &) = delete;
|
||||
|
||||
Handle(Handle &&other) noexcept : ptr_(std::exchange(other.ptr_, nullptr)) {}
|
||||
|
||||
Handle &operator=(Handle &&other) noexcept {
|
||||
if (this != &other) {
|
||||
if (ptr_ != nullptr) {
|
||||
Free(ptr_);
|
||||
}
|
||||
ptr_ = std::exchange(other.ptr_, nullptr);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// The raw handle, for passing to the `wickra_<ind>_*` functions.
|
||||
T *get() const noexcept { return ptr_; }
|
||||
|
||||
/// True if the handle is non-null (construction succeeded).
|
||||
explicit operator bool() const noexcept { return ptr_ != nullptr; }
|
||||
|
||||
private:
|
||||
T *ptr_;
|
||||
};
|
||||
|
||||
} // namespace wickra
|
||||
|
||||
#endif // WICKRA_HPP
|
||||
+44291
File diff suppressed because it is too large
Load Diff
@@ -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