apiplant_abi/c.rs
1//! A plain C ABI, for functions written in something other than Rust.
2//!
3//! The [main contract](crate) is expressed in [`abi_stable`] types — `RString`,
4//! `RResult`, `#[sabi_trait]` vtables, a root module whose header carries type
5//! layout metadata the host verifies at load time. That machinery is what makes
6//! the Rust-to-Rust boundary safe across compiler versions, but it is not
7//! something you can hand-write in C, Zig or Go.
8//!
9//! So a library may instead export the six plain C symbols below. The host tries
10//! the `abi_stable` root module first and falls back to these, wrapping whatever
11//! it finds in the same [`Function`](crate::Function) trait object — so a C
12//! function is mounted, authenticated, documented in the OpenAPI spec and usable
13//! as a lifecycle hook exactly like a Rust one.
14//!
15//! ## What the library exports
16//!
17//! ```c
18//! uint32_t apiplant_abi_version(void);
19//! const char *apiplant_manifest(void);
20//! int32_t apiplant_invoke(const char *name, const char *input_json,
21//! const ApiplantHost *host, char **out);
22//! void apiplant_free(char *string);
23//! ```
24//!
25//! `apiplant_manifest` returns a JSON **array** — one object per function the
26//! library provides, mirroring what `function!` generates on the Rust side:
27//!
28//! ```json
29//! [{ "name": "hello", "version": "1.0.0", "description": "Greets someone.",
30//! "visibility": "public", "method": "POST",
31//! "input_schema": { "type": "object" }, "output_schema": { "type": "object" } }]
32//! ```
33//!
34//! Only `name` is required. `visibility` takes the same strings as a resource's
35//! permissions (`"public"`, `"authenticated"`, `"role:admin"`, `"private"`, and
36//! it defaults to `"private"` — the safe direction, so a typo hides an endpoint
37//! rather than exposing it). `method` defaults to `"POST"`. The two schema fields
38//! are optional and may be given as an object or as a JSON string; they only feed
39//! the generated docs.
40//!
41//! ## Memory
42//!
43//! Each side frees what it allocated, because the two do not share an allocator:
44//!
45//! * The string `apiplant_invoke` writes to `*out` is released by the host
46//! calling the library's own [`apiplant_free`](FreeFn).
47//! * Strings the host returns from [`Host::config`], [`Host::query`],
48//! [`Host::principal_id`], [`Host::hook`], [`Host::send_email`] and
49//! [`Host::cache`], [`Host::payments`] and [`Host::ai`] are released by the library calling
50//! [`Host::free_string`].
51//!
52//! The pointer from `apiplant_manifest` is never freed, so it must be static.
53//!
54//! ## Growing the host
55//!
56//! [`Host`] gains callbacks at its **end**, never in the middle: the host is
57//! what allocates the struct, so a library compiled against an older, shorter
58//! definition still finds every field it knows at the offset it expects, and
59//! simply never reads the ones added since. That is why [`ABI_VERSION`] does
60//! not change when a callback is appended — and why it must change if one is
61//! ever removed or reordered.
62//!
63//! ## Faults
64//!
65//! [`Host`]'s callbacks never unwind — the host catches its own panics before
66//! they reach C. In the other direction the return code separates a bad request
67//! from a broken function, which is what the string-prefix convention
68//! ([`INTERNAL_ERROR_PREFIX`](crate::INTERNAL_ERROR_PREFIX)) expresses in Rust:
69//!
70//! | code | meaning | response |
71//! |------|---------|----------|
72//! | [`OK`] | `*out` is the JSON response body | `200` |
73//! | [`ERR_REQUEST`] | `*out` is a message for the caller | `400` |
74//! | [`ERR_INTERNAL`] | `*out` is a message for the log | `500`, message withheld |
75
76use core::ffi::{c_char, c_void};
77
78/// Version of this C contract. The host refuses a library reporting anything
79/// else, so a breaking change here is a clean load error rather than a crash.
80pub const ABI_VERSION: u32 = 1;
81
82/// `*out` holds the JSON response body.
83pub const OK: i32 = 0;
84/// `*out` holds a message describing what was wrong with the request (`400`).
85pub const ERR_REQUEST: i32 = 1;
86/// `*out` holds a message describing how the function broke (`500`). The host
87/// logs it and does not echo it to the caller.
88pub const ERR_INTERNAL: i32 = 2;
89
90/// Severities accepted by [`Host::log`]; matches [`LogLevel`](crate::LogLevel).
91pub mod log_level {
92 pub const TRACE: i32 = 0;
93 pub const DEBUG: i32 = 1;
94 pub const INFO: i32 = 2;
95 pub const WARN: i32 = 3;
96 pub const ERROR: i32 = 4;
97}
98
99/// Services the host lends to a C function for the duration of one call.
100///
101/// The host fills this in and passes a pointer that is valid **only** until
102/// `apiplant_invoke` returns; `ctx` must be handed back to every callback
103/// untouched. Each `char *` the host returns is owned by the callee and must be
104/// released with [`free_string`](Host::free_string).
105#[repr(C)]
106pub struct Host {
107 /// Opaque host state. Pass it back to every callback; never dereference it.
108 pub ctx: *mut c_void,
109
110 /// Run a query. `request_json` is `{"sql": "…", "params": [ … ]}`. Returns a
111 /// JSON array of rows, `{"rows_affected": n}`, or `{"error": "…"}` when the
112 /// query failed — the shape distinguishes them, so there is no out-param.
113 pub query: Option<extern "C" fn(ctx: *mut c_void, request_json: *const c_char) -> *mut c_char>,
114
115 /// Emit a log line through the host's `tracing` subscriber. `level` is one of
116 /// [`log_level`]; anything else is treated as `INFO`.
117 pub log: Option<extern "C" fn(ctx: *mut c_void, level: i32, message: *const c_char)>,
118
119 /// The function's resolved configuration, as a JSON object.
120 pub config: Option<extern "C" fn(ctx: *mut c_void) -> *mut c_char>,
121
122 /// Id of the authenticated caller, or an empty string when anonymous.
123 pub principal_id: Option<extern "C" fn(ctx: *mut c_void) -> *mut c_char>,
124
125 /// Lifecycle-hook context as JSON, or an empty string for a plain HTTP call.
126 /// See [`HostApi::hook`](crate::HostApi::hook) for the shape.
127 pub hook: Option<extern "C" fn(ctx: *mut c_void) -> *mut c_char>,
128
129 /// Release a string one of the callbacks above returned.
130 pub free_string: Option<extern "C" fn(ctx: *mut c_void, string: *mut c_char)>,
131
132 /// Send an email. `request_json` is the message
133 /// (`{"to":…,"subject":…,"text":…}`); returns the receipt
134 /// `{"provider":…,"id":…,"recipients":n}` or `{"error":"…"}`. As with
135 /// [`query`](Self::query) the shape distinguishes them.
136 /// See [`HostApi::send_email`](crate::HostApi::send_email).
137 pub send_email:
138 Option<extern "C" fn(ctx: *mut c_void, request_json: *const c_char) -> *mut c_char>,
139
140 /// Run one cache operation. `request_json` is `{"op":"get","key":"…"}` and
141 /// friends; returns the operation's reply or `{"error":"…"}`.
142 /// See [`HostApi::cache`](crate::HostApi::cache).
143 pub cache: Option<extern "C" fn(ctx: *mut c_void, request_json: *const c_char) -> *mut c_char>,
144
145 /// Run one payment operation. `request_json` is
146 /// `{"op":"checkout","stripe_price_id":"…"}` and friends; returns the
147 /// operation's reply or `{"error":"…"}`.
148 /// See [`HostApi::payments`](crate::HostApi::payments).
149 pub payments:
150 Option<extern "C" fn(ctx: *mut c_void, request_json: *const c_char) -> *mut c_char>,
151
152 /// Ask the AI assistant. `request_json` is a conversation
153 /// (`{"messages":[{"role":"user","content":"…"}]}`); returns the answer
154 /// `{"text":…,"provider":…,"model":…}` or `{"error":"…"}`.
155 /// See [`HostApi::ai`](crate::HostApi::ai).
156 pub ai: Option<extern "C" fn(ctx: *mut c_void, request_json: *const c_char) -> *mut c_char>,
157
158 /// Push a chunk of the response to the caller before the call returns.
159 /// Returns non-zero while it is still worth producing more, and zero once
160 /// the caller has hung up. See [`HostApi::emit`](crate::HostApi::emit).
161 pub emit: Option<extern "C" fn(ctx: *mut c_void, chunk: *const c_char) -> i32>,
162}
163
164/// Reports the contract the library was built against; must return
165/// [`ABI_VERSION`].
166pub type AbiVersionFn = unsafe extern "C" fn() -> u32;
167
168/// Returns a static, NUL-terminated JSON array of manifests. Never freed.
169pub type ManifestFn = unsafe extern "C" fn() -> *const c_char;
170
171/// Handles one request. Writes a NUL-terminated string to `*out` and returns
172/// [`OK`], [`ERR_REQUEST`] or [`ERR_INTERNAL`]. Must not unwind or longjmp.
173pub type InvokeFn = unsafe extern "C" fn(
174 name: *const c_char,
175 input_json: *const c_char,
176 host: *const Host,
177 out: *mut *mut c_char,
178) -> i32;
179
180/// Releases a string produced by [`InvokeFn`].
181pub type FreeFn = unsafe extern "C" fn(string: *mut c_char);
182
183/// Symbol the host looks up to decide a library speaks this ABI.
184pub const SYM_ABI_VERSION: &[u8] = b"apiplant_abi_version\0";
185/// Symbol for [`ManifestFn`].
186pub const SYM_MANIFEST: &[u8] = b"apiplant_manifest\0";
187/// Symbol for [`InvokeFn`].
188pub const SYM_INVOKE: &[u8] = b"apiplant_invoke\0";
189/// Symbol for [`FreeFn`].
190pub const SYM_FREE: &[u8] = b"apiplant_free\0";