use crate::codegen::c_consumer;
use crate::core::config::Language;
use crate::core::ir::ErrorDef;
use crate::docs::clean_doc;
pub(crate) fn emit_helpers(prefix: &str, declared_errors: &[ErrorDef], out: &mut String) {
let free_symbol = c_consumer::free_string_symbol(prefix);
let error_code_symbol = c_consumer::last_error_code_symbol(prefix);
let error_context_symbol = c_consumer::last_error_context_symbol(prefix);
out.push_str("/// Free a string allocated by the FFI layer.\n");
out.push_str(&crate::backends::zig::template_env::render(
"helper_free_string_doc1.jinja",
minijinja::context! {
prefix => prefix,
},
));
out.push_str("/// Do NOT call this twice on the same pointer.\n");
out.push_str("pub fn _free_string(ptr: [*c]u8) void {\n");
out.push_str(&crate::backends::zig::template_env::render(
"helper_free_string_doc2.jinja",
minijinja::context! {
free_symbol => free_symbol,
},
));
out.push_str("}\n\n");
out.push_str("/// Binding-owned copy of the last error message captured by\n");
out.push_str("/// `_capture_error_message`. Freed and replaced each time a new error is\n");
out.push_str("/// captured; outlives the deferred FFI calls that clear the underlying\n");
out.push_str("/// thread-local state in the FFI layer.\n");
out.push_str("threadlocal var _captured_error: ?[]u8 = null;\n\n");
out.push_str("/// Retrieve the last error message captured by `_error_with_message`, if any.\n");
out.push_str("/// Returns a slice into binding-owned storage, valid until the next captured\n");
out.push_str("/// error overwrites it.\n");
out.push_str("pub fn _last_error() ?[]const u8 {\n");
out.push_str(" return _captured_error;\n");
out.push_str("}\n\n");
out.push_str("/// Copy the FFI layer's current error message into binding-owned storage.\n");
out.push_str(&format!(
"/// Must run before any deferred `_free` call: every `{prefix}_*` FFI entry point\n"
));
out.push_str("/// clears the thread-local error state on entry (`catch_ffi_panic` opens with\n");
out.push_str("/// `clear_last_error()`), so a `_free` invoked between the failing call and this\n");
out.push_str("/// capture would wipe the message before it could be read.\n");
out.push_str("fn _capture_error_message() void {\n");
out.push_str(&crate::backends::zig::template_env::render(
"helper_last_error_code.jinja",
minijinja::context! {
symbol => error_code_symbol,
},
));
out.push_str(" if (_code == 0) return;\n");
out.push_str(&crate::backends::zig::template_env::render(
"helper_last_error_ctx.jinja",
minijinja::context! {
symbol => error_context_symbol,
},
));
out.push_str(" if (_ctx == null) return;\n");
out.push_str(" const _msg = std.mem.sliceTo(_ctx, 0);\n");
out.push_str(" if (_captured_error) |_old| std.heap.c_allocator.free(_old);\n");
out.push_str(" _captured_error = std.heap.c_allocator.dupe(u8, _msg) catch null;\n");
out.push_str("}\n\n");
let dispatching: Vec<&ErrorDef> = declared_errors
.iter()
.filter(|error| error.variants.iter().any(|variant| variant.error_code.is_some()))
.collect();
out.push_str("/// Map the last FFI error to a typed error.\n");
if dispatching.is_empty() {
out.push_str("/// No declared variant carries a stable numeric FFI taxonomy code\n");
out.push_str("/// (`#[alef(error_code = N)]`), so no specific variant can be\n");
out.push_str("/// substantiated: every failure maps to `error.UnknownFfiError`.\n");
} else {
out.push_str("/// Dispatches exclusively on the stable numeric FFI taxonomy code.\n");
out.push_str("/// A code matching no declared variant maps to `error.UnknownFfiError`.\n");
}
out.push_str("inline fn _error_with_message(comptime E: type) E {\n");
out.push_str(" _capture_error_message();\n");
if !dispatching.is_empty() {
out.push_str(&format!(
" const code = @as(i32, @intCast(c.{error_code_symbol}()));\n"
));
for error in dispatching {
out.push_str(&format!(" if (E == {}) return switch (code) {{\n", error.name));
for variant in &error.variants {
let Some(error_code) = variant.error_code else {
continue;
};
let variant_name = crate::codegen::naming::public_host_identifier(
Language::Zig,
crate::codegen::naming::PublicIdentifierKind::Type,
&variant.name,
);
out.push_str(&format!(" {error_code} => error.{variant_name},\n"));
}
out.push_str(" else => error.UnknownFfiError,\n };\n");
}
}
out.push_str(" return error.UnknownFfiError;\n");
out.push_str("}\n");
}
pub(crate) fn emit_cleaned_zig_doc(out: &mut String, doc: &str, indent: &str) {
if doc.is_empty() {
return;
}
let cleaned = clean_doc(doc, Language::Zig);
crate::codegen::doc_emission::emit_zig_doc(out, &cleaned, indent);
}
#[cfg(test)]
mod tests {
use super::*;
fn request_error(error_code: Option<u32>) -> ErrorDef {
ErrorDef {
name: "RequestError".to_string(),
rust_path: "sample::RequestError".to_string(),
variants: vec![crate::core::ir::ErrorVariant {
error_code,
name: "InvalidInput".to_string(),
is_unit: true,
..Default::default()
}],
original_rust_path: String::new(),
doc: String::new(),
methods: Vec::new(),
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}
}
#[test]
fn error_with_message_dispatches_to_each_declared_error() {
let errors = vec![request_error(Some(100))];
let mut out = String::new();
emit_helpers("example_pack", &errors, &mut out);
assert!(
out.contains("100 => error.InvalidInput"),
"missing numeric taxonomy dispatch:\n{out}"
);
assert!(
!out.contains("std.debug.print"),
"FFI helpers must not write to stderr:\n{out}"
);
assert!(
out.contains(" else => error.UnknownFfiError,\n"),
"a code matching no declared variant must resolve to the opaque unknown error:\n{out}"
);
assert!(
out.contains(" return error.UnknownFfiError;\n"),
"an error set with no dispatch block must resolve to the opaque unknown error:\n{out}"
);
}
#[test]
fn unnumbered_error_variants_never_resolve_to_a_declared_variant() {
let errors = vec![request_error(None)];
let mut out = String::new();
emit_helpers("example_pack", &errors, &mut out);
assert!(
!out.contains("error.InvalidInput"),
"an uncoded variant must never be named by the dispatcher:\n{out}"
);
assert!(
!out.contains("_first_error"),
"the first-declared-variant fallback must not be emitted at all:\n{out}"
);
assert!(
!out.contains("fields[0].name"),
"nothing may resolve an FFI failure by declaration order:\n{out}"
);
assert!(
!out.contains("switch (code)"),
"with no coded variant there is nothing to dispatch on:\n{out}"
);
assert!(
out.contains(" return error.UnknownFfiError;\n"),
"every failure must surface as the opaque unknown error:\n{out}"
);
assert!(
!out.contains("Dispatches exclusively on the stable numeric FFI taxonomy code"),
"the emitted doc must not claim a dispatch that does not exist:\n{out}"
);
}
#[test]
fn error_with_message_captures_context_into_binding_owned_storage() {
let mut out = String::new();
emit_helpers("example_pack", &[], &mut out);
assert!(
!out.contains("_ = _last_error();"),
"the FFI error context must not be read and discarded:\n{out}"
);
assert!(
out.contains("threadlocal var _captured_error: ?[]u8 = null;"),
"missing binding-owned error message storage:\n{out}"
);
assert!(
out.contains("fn _capture_error_message() void {"),
"missing the capture routine:\n{out}"
);
assert!(
out.contains(" _capture_error_message();\n"),
"_error_with_message must capture before returning:\n{out}"
);
assert!(
out.contains("pub fn _last_error() ?[]const u8 {\n return _captured_error;\n}"),
"_last_error must read the binding-owned copy, not the FFI layer directly:\n{out}"
);
}
#[test]
fn error_with_message_returns_unknown_when_no_errors_declared() {
let mut out = String::new();
emit_helpers("crate", &[], &mut out);
assert!(
out.contains("inline fn _error_with_message(comptime E: type) E {"),
"missing _error_with_message decl:\n{out}"
);
assert!(
!out.contains("_from_ffi_msg_"),
"no per-error matcher should be referenced when none are declared:\n{out}"
);
assert!(
!out.contains("_first_error"),
"the first-declared-variant fallback must not be emitted at all:\n{out}"
);
assert!(
out.contains(" return error.UnknownFfiError;\n"),
"unknown-error fallback required:\n{out}"
);
assert!(
!out.contains("const code ="),
"no taxonomy code is read when nothing dispatches on it (unused local):\n{out}"
);
}
}