Skip to main content

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";