Skip to main content

warp_runtime/
c_signatures.rs

1//! C functions as warp calls them, without their headers: how each parameter and the result cross (notes/ffi_handles.md).
2//! warp reads a library's headers into these (src/ffi); an executable carries the ones its program imports in front of
3//! its machine code, so the stub links them without reading a header (c_calls.rs, card exe-c-ffi)
4
5/// What a carried program starts with when it calls C, which machine code never does
6pub const CARRIED_C_FUNCTIONS: &[u8; 8] = b"WRPCcfun";
7/// The libc functions link_libc_functions defines by hand; any other libc import is linked from the process
8pub const HAND_LINKED_LIBC: [&str; 8] = ["abs", "strlen", "atoi", "atol", "atof", "strcmp", "strncmp", "rand"];
9const FIELD_SEPARATOR: char = '\t';
10const LIST_SEPARATOR: char = ',';
11
12/// What a C pointer type crosses as
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub enum CPointer {
15	/// `char *`, `const unsigned char *`: a warp text (a parameter its NUL-terminated letters in linear memory)
16	Text,
17	/// `void *`, `int *`: a parameter points into linear memory; a result is a handle
18	Memory,
19	/// `sqlite3 *`, `FILE *`: an id into the run's handle table (CHandles), 0 is NULL
20	Handle,
21	/// `sqlite3 **ppDb`: left out of the warp call; the first one becomes the result unless a struct output is, the others
22	/// receive NULL
23	Out,
24	/// `wasmtime_instance_t *instance` as the call's output: the struct read back as a map (c_structs.rs)
25	Struct,
26}
27
28/// A C parameter: a number by its wasm type, or a pointer by what it crosses as
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30pub enum ParamType {
31	I32,
32	I64,
33	F32,
34	F64,
35	/// linear memory: a text, a buffer
36	Ptr,
37	/// a handle id (CPointer::Handle)
38	Handle,
39	/// an out-pointer, filled by the call (CPointer::Out)
40	Out,
41	/// `const uint8_t *module, size_t length`: a warp text or list of byte values, copied for the call; its length
42	/// fills the Length after it
43	Bytes,
44	/// the `size_t` after Bytes or StructsIn: left out of the warp call
45	Length,
46	/// `const wasmtime_instance_t *`: a map written into the struct's bytes (or a handle id, its pointer)
47	StructIn,
48	/// `const wasmtime_val_t *args, size_t nargs`: a list of maps; its length fills the Length after it
49	StructsIn,
50	/// a trailing `wasmtime_instance_t *`: left out of the warp call, the call's result (CPointer::Struct)
51	StructOut,
52	/// `wasm_trap_t **trap`, an error-shaped out-pointer (is_error_type): left out, non-NULL after the call fails it
53	ErrorOut,
54	/// a trailing `wasmtime_val_t *results, size_t nresults`: left out, the size_t after it says how many, the call's
55	/// result is their list
56	StructsOut,
57}
58
59impl ParamType {
60	/// Whether the warp call passes this parameter: out-pointers and lengths are filled by the call
61	pub fn is_passed(self) -> bool {
62		!matches!(
63			self,
64			ParamType::Out | ParamType::ErrorOut | ParamType::Length | ParamType::StructOut | ParamType::StructsOut
65		)
66	}
67
68	pub fn is_struct_output(self) -> bool {
69		matches!(self, ParamType::StructOut | ParamType::StructsOut)
70	}
71}
72
73/// A C result's register: none, an integer, a float, or a bool (its low byte)
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
75pub enum RetType {
76	Void,
77	I32,
78	I64,
79	F32,
80	F64,
81	Bool,
82}
83
84const PARAM_NAMES: [(ParamType, &str); 14] = [
85	(ParamType::I32, "i32"),
86	(ParamType::I64, "i64"),
87	(ParamType::F32, "f32"),
88	(ParamType::F64, "f64"),
89	(ParamType::Ptr, "ptr"),
90	(ParamType::Handle, "handle"),
91	(ParamType::Out, "out"),
92	(ParamType::Bytes, "bytes"),
93	(ParamType::Length, "length"),
94	(ParamType::StructIn, "struct_in"),
95	(ParamType::StructsIn, "structs_in"),
96	(ParamType::StructOut, "struct_out"),
97	(ParamType::StructsOut, "structs_out"),
98	(ParamType::ErrorOut, "error_out"),
99];
100const RET_NAMES: [(RetType, &str); 6] = [
101	(RetType::Void, "void"),
102	(RetType::I32, "i32"),
103	(RetType::I64, "i64"),
104	(RetType::F32, "f32"),
105	(RetType::F64, "f64"),
106	(RetType::Bool, "bool"),
107];
108/// The pointers a result crosses as; a number result is "number"
109const RESULT_NAMES: [(Option<CPointer>, &str); 4] = [
110	(None, "number"),
111	(Some(CPointer::Text), "text"),
112	(Some(CPointer::Handle), "handle"),
113	(Some(CPointer::Struct), "struct"),
114];
115const LAYOUT_SEPARATOR: char = ';';
116
117fn name_of<T: PartialEq + Copy>(names: &[(T, &'static str)], value: T) -> &'static str {
118	names.iter().find(|(known, _)| *known == value).map_or("?", |(_, name)| name)
119}
120
121fn value_of<T: Copy>(names: &[(T, &str)], name: &str) -> Option<T> {
122	names.iter().find(|(_, known)| *known == name).map(|(value, _)| *value)
123}
124
125/// A C function as an import of a program: its import module (the library as the program names it, `zlib`), the
126/// library it loads (`z`), and how its parameters and result cross
127#[derive(Clone, Debug, PartialEq, Eq)]
128pub struct CFunction {
129	pub module: String,
130	pub library: String,
131	pub name: String,
132	pub param_types: Vec<ParamType>,
133	/// Text, Handle, or None for the C function's own number (ret_type)
134	pub result: Option<CPointer>,
135	pub ret_type: RetType,
136	/// what the first out-pointer points to (`sqlite3_stmt` of `sqlite3_stmt **ppStmt`), for the error of a NULL there
137	pub out_pointee: Option<String>,
138	/// the layout text of each struct parameter (c_structs.rs), "" for the others; empty without struct parameters
139	pub layouts: Vec<String>,
140	/// whether the C result reports a failure instead of being the result
141	pub failure: Failure,
142}
143
144/// How a C result reports a failure: an error-shaped pointer (`wasmtime_error_t *`) when not NULL, a bool of a function
145/// with an output when false
146#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
147pub enum Failure {
148	None,
149	NotNull,
150	False,
151}
152const FAILURE_NAMES: [(Failure, &str); 3] =
153	[(Failure::None, ""), (Failure::NotNull, "not_null"), (Failure::False, "false")];
154
155/// An error-shaped C type: a non-NULL one reports a failure (`wasmtime_error_t *`, `wasm_trap_t **`)
156pub fn is_error_type(c_type: &str) -> bool {
157	let lower = c_type.to_lowercase();
158	["error", "trap", "status", "exception"].iter().any(|word| lower.contains(word))
159}
160
161impl CFunction {
162	/// One line of the carried table, its fields tab-separated: `zlib z compressBound i64 i64 number` and an empty
163	/// out-pointee
164	pub fn line(&self) -> String {
165		let params: Vec<&str> = self.param_types.iter().map(|param| name_of(&PARAM_NAMES, *param)).collect();
166		[
167			self.module.as_str(),
168			&self.library,
169			&self.name,
170			&params.join(&LIST_SEPARATOR.to_string()),
171			name_of(&RET_NAMES, self.ret_type),
172			name_of(&RESULT_NAMES, self.result),
173			self.out_pointee.as_deref().unwrap_or_default(),
174			&self.layouts.join(&LAYOUT_SEPARATOR.to_string()),
175			name_of(&FAILURE_NAMES, self.failure),
176		]
177		.join(&FIELD_SEPARATOR.to_string())
178	}
179
180	pub fn of_line(line: &str) -> Option<CFunction> {
181		let [module, library, name, params, ret_type, result, out_pointee, layouts, failure] =
182			line.split(FIELD_SEPARATOR).collect::<Vec<_>>().try_into().ok()?;
183		let param_types = params
184			.split(LIST_SEPARATOR)
185			.filter(|param| !param.is_empty())
186			.map(|param| value_of(&PARAM_NAMES, param))
187			.collect::<Option<_>>()?;
188		Some(CFunction {
189			module: module.to_string(),
190			library: library.to_string(),
191			name: name.to_string(),
192			param_types,
193			result: value_of(&RESULT_NAMES, result)?,
194			ret_type: value_of(&RET_NAMES, ret_type)?,
195			out_pointee: (!out_pointee.is_empty()).then(|| out_pointee.to_string()),
196			layouts: layouts.split(LAYOUT_SEPARATOR).filter(|_| !layouts.is_empty()).map(str::to_string).collect(),
197			failure: value_of(&FAILURE_NAMES, failure)?,
198		})
199	}
200}
201
202/// `machine_code` with the C functions it imports in front: CARRIED_C_FUNCTIONS, the table's length (u64 LE), one
203/// line per function; machine code alone when it imports none
204pub fn with_c_functions(functions: &[CFunction], machine_code: &[u8]) -> Vec<u8> {
205	if functions.is_empty() {
206		return machine_code.to_vec();
207	}
208	let table = functions.iter().map(CFunction::line).collect::<Vec<_>>().join("\n");
209	[CARRIED_C_FUNCTIONS.as_slice(), &(table.len() as u64).to_le_bytes(), table.as_bytes(), machine_code].concat()
210}
211
212/// The C functions and the machine code `with_c_functions` joined; none for plain machine code
213pub fn split_c_functions(carried: &[u8]) -> Result<(Vec<CFunction>, &[u8]), String> {
214	let Some(rest) = carried.strip_prefix(CARRIED_C_FUNCTIONS.as_slice()) else {
215		return Ok((vec![], carried));
216	};
217	let damaged = || "the executable's table of C functions is damaged".to_string();
218	let (length, rest) = rest.split_first_chunk::<8>().ok_or_else(damaged)?;
219	let (table, machine_code) = rest.split_at_checked(u64::from_le_bytes(*length) as usize).ok_or_else(damaged)?;
220	let table = std::str::from_utf8(table).map_err(|_| damaged())?;
221	let functions = table.lines().map(CFunction::of_line).collect::<Option<_>>().ok_or_else(damaged)?;
222	Ok((functions, machine_code))
223}