The frozen C-ABI: the six exports a study implements, the bar/series/input/sink types, the drawing API, and the safety contract.
A study compiles against exactly one Sharpnel header — sharpnel_study_sdk.h — and nothing else from Sharpnel. It is pure C: plain-old-data structs and raw function pointers. No Qt, no Skia, no STL, no Sharpnel internals cross the boundary. The current ABI is version 1 and it is frozen; new capabilities are added additively (see ABI stability).
The header carries static_assert layout guards (for example sizeof(SnBar) == 80), so if a compiler ever packed a struct differently you would find out at your compile, not at runtime.
Every study exports these C symbols. The names carry the ABI major (v1). The host resolves and validates all non-optional symbols before registering — any missing required symbol rejects the whole plugin.
abi_version() | Called FIRST, before any struct is touched. Return SHARPNEL_STUDY_ABI_VERSION. Required. |
describe(SnStudyDesc*) | Static identity for the Add-Study menu: target (overlay / sub-pane), type_id, display_name, category. Required. |
inputs(SnInputSpec*, max) | Declare inputs; the host builds the settings dialog from them. Writes up to max rows; returns the count it would write (host clamps to max). Required. |
create() / destroy(void*) | Per-instance state so the same study runs on two charts at once. create() required; destroy() optional (host null-guards). |
compute(...) | The work: read the bar series, write subgraphs and drawings through the sink. Required. |
shutdown() | Optional process-global teardown at unload. |
describe() fills an SnStudyDesc that tells the terminal what your study is called and where it draws:
target | SN_TARGET_OVERLAY (0) draws on the price plane; SN_TARGET_SUBPANE (1) draws in its own pane below. |
type_id | Stable internal id (e.g. "acme.sma"). Borrowed — the host copies it immediately. |
display_name | The label shown under Add Study. |
category | The Add-Study submenu it appears under (e.g. "Trend", "Momentum"). |
compute() receives an SnSeries — a read-only, host-owned array of SnBar valid only for that call. Each bar carries the order-flow split, not just OHLC:
time | Unix seconds. |
open / high / low / close | Session-resolved prices; raw_close is the untransformed close. |
volume / buy_volume / sell_volume | Per-bar volume and its aggressor split — the basis for delta studies. |
flags | Climax tier (0–6) in the low byte; the rest is reserved. |
count / tick_size | Bar count on the series, and the instrument's tick size. |
The recompute_from argument is the index of the first bar that changed since the previous call — bars before it are byte-identical, so an incremental study can skip them. You may also ignore it and recompute everything (the examples do).
Declare inputs in inputs() and the terminal renders the settings UI for you — labels, numeric bounds, and enum choices all come from your SnInputSpec rows. In compute() the host hands back the resolved values as a flat SnInput[]; read the one you want by matching key, then take the value from the field for its type. Supported types:
SN_IN_INT (0) | Integer, value in ival. Honors min / max / step. |
SN_IN_DOUBLE (1) | Double, value in dval. Honors min / max / step. |
SN_IN_BOOL (2) | Checkbox, value in ival as 0 | 1. |
SN_IN_ENUM (3) | Dropdown; value in ival as the choice index. Labels come from enum_labels / enum_count. |
SN_IN_COLOR (4) | ARGB color picker, value in color. |
SN_IN_SOURCE (5) | Price-source picker, value in ival as an SN_SRC_* index (close, open, high, low, hl2, hlc3, ohlc4, volume, buy/sell vol, delta). |
Fill the matching default on each SnInputSpec — def_i for Int/Bool/Enum/Source, def_d for Double, def_color for Color. All const char* fields (key, label, enum labels) are borrowed for the call; the host copies them.
All output goes through the host-owned SnSinkApi, valid only inside the compute() call that handed it to you. The host owns every buffer; you only fill it.
subgraph(name, style, color, width) | Returns a writable {ptr, len} buffer where len == series.count. Idempotent by name — a second call with the same name returns the SAME buffer (style/color/width updated), never a second allocation. Write only indices [0, len). |
level(value, color, dashed, label) | Horizontal reference line in the study's value space. |
segment(b0, v0, b1, v1, color) | Line segment between two (bar, value) points. Bar indices must be < count. |
marker(bar, value, color, text) | Text marker at a bar. Bar index must be < count. |
alert(op, sub_idx, thresh, other_idx, msg) | Declarative alert condition (op / threshold) the terminal can fire on. |
Draw styles for subgraph(): SN_STYLE_LINE (0), SN_STYLE_HISTOGRAM (1), SN_STYLE_AREA (2), SN_STYLE_DOTS (3), SN_STYLE_BAND (4), SN_STYLE_STEP (5). Colors are uint32_t ARGB (0xAARRGGBB — e.g. 0xFFFFAA00u is opaque amber). Leave warmup bars as SN_NOVALUE (a NaN) rather than writing zero, so the line starts where your study is actually valid.
Per-compute caps: up to 16 subgraphs, 64 reference levels, and 100,000 drawings (segments + markers).
The boundary is designed so a buggy study can't take your terminal down. Read these once:
SN_NOVALUE. Write only indices [0, len) — bound your loop by buf.len. The host plants a canary and fault-disables a study that writes past the end. No plugin allocation ever crosses the boundary.const char* is valid only for the single call; the host copies it immediately (bounded by SN_MAX_STR = 256 bytes). Don't retain host pointers past the call they arrived in.compute() is always called on the chart thread; the same instance is never called concurrently. The sink and api are valid only inside that call, on that thread — do not retain them or touch them from other threads.compute body in try / catch(...). No C++ or hardware exception may propagate out of any boundary function. A fault that crosses it fault-disables your plugin for the rest of the session — the terminal keeps running.The v1 ABI is frozen. New capabilities are added additively — appended fields behind reserved struct tails, and appended sink callbacks at the end of SnSinkApi — so a v1 study keeps loading unchanged across terminal updates. A genuinely breaking change bumps the major version and the host refuses the mismatch cleanly; it never tries to run a study built against an incompatible ABI. Build once, keep loading.