use crate::backends::ffi::type_map::is_void_return;
use crate::codegen::doc_emission::emit_c_doxygen;
use crate::core::ir::TypeRef;
use ahash::AHashSet;
fn render_doxygen_typedef_block(doc: &str) -> String {
if doc.trim().is_empty() {
return String::new();
}
let mut raw = String::new();
emit_c_doxygen(&mut raw, doc, "");
let mut out = String::with_capacity(raw.len() + 16);
out.push_str("/**\n");
for line in raw.lines() {
let body = line.strip_prefix("/// ").unwrap_or(line.trim_start_matches("///"));
if body.is_empty() {
out.push_str(" *\n");
} else {
out.push_str(" * ");
out.push_str(body);
out.push('\n');
}
}
out.push_str(" */\n");
out
}
fn copy_expr(expr: &str) -> String {
if expr.starts_with("obj.") || expr.starts_with("(*") {
expr.to_string()
} else {
format!("*{expr}")
}
}
pub(super) fn gen_value_to_c(
expr: &str,
ty: &TypeRef,
indent: &str,
enum_names: &AHashSet<String>,
clone_names: &AHashSet<String>,
) -> String {
match ty {
TypeRef::Primitive(p) => {
let type_class = if matches!(p, crate::core::ir::PrimitiveType::Bool) {
"primitive_bool"
} else {
"primitive_other"
};
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => type_class,
expr => expr,
indent => indent,
},
)
}
TypeRef::String | TypeRef::Char => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "string",
expr => expr,
indent => indent,
},
),
TypeRef::Path => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "path",
expr => expr,
indent => indent,
},
),
TypeRef::Json => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "json_or_vec_or_map",
expr => expr,
indent => indent,
},
),
TypeRef::Named(name) => {
if enum_names.contains(name.as_str()) {
let copy = copy_expr(expr);
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "named_enum",
expr => expr,
copy_expr => ©,
indent => indent,
},
)
} else if clone_names.contains(name.as_str()) {
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "named_clone",
expr => expr,
indent => indent,
},
)
} else {
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "named_non_clone",
expr => expr,
indent => indent,
},
)
}
}
TypeRef::Vec(_) | TypeRef::Map(_, _) => {
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "json_or_vec_or_map",
expr => expr,
indent => indent,
},
)
}
TypeRef::Bytes => {
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "bytes",
expr => expr,
indent => indent,
},
)
}
TypeRef::Duration => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "duration",
expr => expr,
indent => indent,
},
),
TypeRef::Unit => String::new(),
TypeRef::Optional(inner) => {
let inner_conversion = gen_value_to_c("val", inner, &format!("{indent} "), enum_names, clone_names);
let null_value = null_return_value(&TypeRef::Optional(inner.clone()));
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "optional",
expr => expr,
indent => indent,
inner_conversion => &inner_conversion,
null_value => null_value,
},
)
}
}
}
pub(super) fn gen_ffi_unimplemented_body(return_type: &TypeRef, fn_name: &str, has_error: bool) -> String {
let err_msg = format!("Not implemented: {fn_name}");
if has_error && is_void_return(return_type) {
format!(" set_last_error(99, \"{err_msg}\");\n -1")
} else if is_void_return(return_type) {
format!(" set_last_error(99, \"{err_msg}\");")
} else {
let ret = null_return_value(return_type);
format!(" set_last_error(99, \"{err_msg}\");\n {ret}")
}
}
pub(super) fn null_return_value(ty: &TypeRef) -> &'static str {
use crate::core::ir::PrimitiveType;
match ty {
TypeRef::Primitive(p) => match p {
PrimitiveType::F32 | PrimitiveType::F64 => "0.0",
_ => "0",
},
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "std::ptr::null_mut()",
TypeRef::Bytes => "std::ptr::null_mut()",
TypeRef::Named(_) => "std::ptr::null_mut()",
TypeRef::Vec(_) | TypeRef::Map(_, _) => "std::ptr::null_mut()",
TypeRef::Duration => "0",
TypeRef::Optional(inner) => match inner.as_ref() {
TypeRef::Primitive(p) => match p {
PrimitiveType::F32 | PrimitiveType::F64 => "0.0",
_ => "0",
},
TypeRef::Optional(inner2) => match inner2.as_ref() {
TypeRef::Primitive(p) => match p {
PrimitiveType::F32 | PrimitiveType::F64 => "0.0",
_ => "0",
},
_ => "std::ptr::null_mut()",
},
TypeRef::Duration => "0",
_ => "std::ptr::null_mut()",
},
TypeRef::Unit => "()",
}
}
pub(super) fn gen_owned_value_to_c(expr: &str, ty: &TypeRef, indent: &str, _enum_names: &AHashSet<String>) -> String {
match ty {
TypeRef::Primitive(prim) => match prim {
crate::core::ir::PrimitiveType::Bool => crate::backends::ffi::template_env::render(
"owned_value_to_c_bool.jinja",
minijinja::context! {
expr => expr,
indent => indent,
},
),
_ => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "primitive_other",
expr => expr,
indent => indent,
},
),
},
TypeRef::String | TypeRef::Char => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "string",
expr => expr,
indent => indent,
},
),
TypeRef::Json => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "json_or_vec_or_map",
expr => expr,
indent => indent,
},
),
TypeRef::Path => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "path",
expr => expr,
indent => indent,
},
),
TypeRef::Named(_) => {
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "named_owned",
expr => expr,
indent => indent,
},
)
}
TypeRef::Vec(_) | TypeRef::Map(_, _) => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "json_or_vec_or_map",
expr => expr,
indent => indent,
},
),
TypeRef::Bytes => {
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "bytes",
expr => expr,
indent => indent,
},
)
}
TypeRef::Optional(inner) => {
if let TypeRef::Primitive(prim) = inner.as_ref() {
if !matches!(prim, crate::core::ir::PrimitiveType::Bool) {
let null_value = null_return_value(&TypeRef::Optional(inner.clone()));
return format!("{indent}{expr}.unwrap_or({null_value})");
}
}
let inner_conversion = gen_owned_value_to_c("val", inner, &format!("{indent} "), _enum_names);
let null_value = null_return_value(&TypeRef::Optional(inner.clone()));
crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "optional_owned",
expr => expr,
indent => indent,
inner_conversion => &inner_conversion,
null_value => null_value,
},
)
}
TypeRef::Duration => crate::backends::ffi::template_env::render(
"value_to_c_conversion.jinja",
minijinja::context! {
type_class => "duration",
expr => expr,
indent => indent,
},
),
TypeRef::Unit => String::new(),
}
}
pub(super) fn gen_cbindgen_toml(prefix: &str, api: &crate::core::ir::ApiSurface) -> String {
let prefix_upper = prefix.to_uppercase();
let mut entries: Vec<(String, String)> = api
.types
.iter()
.map(|t| (format!("{prefix_upper}{}", t.name), t.doc.clone()))
.collect();
for e in &api.enums {
let c_name = format!("{prefix_upper}{}", e.name);
if !entries.iter().any(|(n, _)| n == &c_name) {
entries.push((c_name, e.doc.clone()));
}
}
for err in &api.errors {
if !err.methods.is_empty() {
let c_name = format!("{prefix_upper}{}", err.name);
if !entries.iter().any(|(n, _)| n == &c_name) {
entries.push((c_name, err.doc.clone()));
}
}
}
entries.sort_by(|a, b| a.0.cmp(&b.0));
let forward_decls: String = entries
.iter()
.map(|(name, doc)| {
let doc_block = render_doxygen_typedef_block(doc);
if doc_block.is_empty() {
format!("typedef struct {name} {name};")
} else {
format!("{doc_block}typedef struct {name} {name};")
}
})
.collect::<Vec<_>>()
.join("\n");
let after_includes = if forward_decls.is_empty() {
String::new()
} else {
toml_multiline_basic_string(&format!("/* Opaque type forward declarations */\n{forward_decls}\n"))
};
crate::backends::ffi::template_env::render(
"cbindgen_toml.jinja",
minijinja::context! {
prefix_upper => &prefix_upper,
after_includes => &after_includes,
},
)
}
fn toml_multiline_basic_string(value: &str) -> String {
let escaped = value
.replace('\\', "\\\\")
.replace("\"\"\"", "\\\"\\\"\\\"")
.replace('\u{8}', "\\b")
.replace('\u{c}', "\\f");
format!("\"\"\"\n{escaped}\"\"\"")
}
pub(super) fn gen_build_rs(header_name: &str, lib_name: &str, go_output_dir: Option<&str>) -> String {
let go_copy_step = match go_output_dir {
Some(go_dir) => {
let go_dir = go_dir.trim_end_matches('/');
let depth = std::path::Path::new(go_dir)
.components()
.filter(|c| matches!(c, std::path::Component::Normal(_)))
.count()
.max(1);
let to_root = "../".repeat(depth);
let dest_dir = format!("{to_root}{go_dir}/include");
format!(
"\n let go_include_dir = std::path::Path::new(\"{dest_dir}\");\n \
std::fs::create_dir_all(go_include_dir).expect(\"Unable to create Go include directory\");\n \
std::fs::copy(\"include/{header_name}\", go_include_dir.join(\"{header_name}\"))\n \
.expect(\"Unable to copy header to Go include directory\");\n"
)
}
None => String::new(),
};
crate::backends::ffi::template_env::render(
"build_rs.jinja",
minijinja::context! {
header_name => header_name,
lib_name => lib_name,
go_copy_step => go_copy_step,
},
)
}
pub(super) fn gen_last_error(prefix: &str) -> String {
crate::backends::ffi::template_env::render(
"last_error.jinja",
minijinja::context! {
prefix => prefix,
},
)
}
pub(super) fn gen_free_string(prefix: &str) -> String {
crate::backends::ffi::template_env::render(
"free_string.jinja",
minijinja::context! {
prefix => prefix,
},
)
}
pub(super) fn gen_version(prefix: &str) -> String {
crate::backends::ffi::template_env::render(
"version_fn.jinja",
minijinja::context! {
prefix => prefix,
},
)
}
pub(super) fn gen_free_bytes(prefix: &str) -> String {
format!(
r#"/// Free a byte buffer previously returned by this library via out-params.
/// `ptr`, `len`, and `cap` must match the values written by the library function,
/// or the call must pass `ptr = null` (in which case it is a no-op).
/// # Safety
/// Pointer must have been returned by this library (via out_ptr / out_len / out_cap
/// out-params), or be null. The len and cap values must be unchanged since the call.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn {prefix}_free_bytes(ptr: *mut u8, len: usize, cap: usize) {{
if !ptr.is_null() {{
// SAFETY: ptr/len/cap were produced by Vec::into_raw_parts (or equivalent)
// by this library; caller must not have mutated them.
unsafe {{ drop(Vec::from_raw_parts(ptr, len, cap)); }}
}}
}}"#,
prefix = prefix
)
}
pub(super) fn gen_ffi_tokio_runtime() -> String {
crate::backends::ffi::template_env::render("ffi_tokio_runtime.jinja", minijinja::context! {})
}
pub(super) fn gen_stream_handle_functions(
prefix: &str,
owner_type: &str,
adapter_name: &str,
core_path: &str,
item_type: &str,
request_type: &str,
core_import: &str,
) -> String {
use heck::{ToPascalCase, ToSnakeCase};
let pascal_prefix = prefix.to_pascal_case();
let pascal_owner = owner_type.to_pascal_case();
let pascal_name = adapter_name.to_pascal_case();
let owner_snake = owner_type.to_snake_case();
let handle_name = format!("{pascal_prefix}{pascal_owner}{pascal_name}StreamHandle");
let fn_start = format!("{prefix}_{owner_snake}_{adapter_name}_start");
let fn_next = format!("{prefix}_{owner_snake}_{adapter_name}_next");
let fn_free = format!("{prefix}_{owner_snake}_{adapter_name}_free");
let core_item = format!("{core_import}::{item_type}");
let boxed_err = "Box<dyn std::error::Error + Send + Sync + 'static>";
let stream_ty = format!("futures_util::stream::BoxStream<'static, Result<{core_item}, {boxed_err}>>");
let owner_ty = format!("{core_import}::{owner_type}");
format!(
r#"/// Opaque handle owning a tokio runtime and a boxed chat-stream for iterator-style consumption.
///
/// Created by `{fn_start}`, advanced by `{fn_next}`, destroyed by `{fn_free}`.
/// The handle is NOT thread-safe — callers must ensure only one thread calls `_next` at a time.
pub struct {handle_name} {{
rt: tokio::runtime::Runtime,
stream: std::sync::Mutex<Option<{stream_ty}>>,
}}
/// Start a streaming chat completion and return an opaque iterator handle.
///
/// Returns null and sets `{prefix}_last_error_code` on failure (null pointers or stream-open error).
/// On success the caller owns the returned pointer and MUST call `{fn_free}` when done.
///
/// # Safety
/// `client` must be a non-null valid pointer to a live `{owner_ty}` produced by this library.
/// `req` must be a non-null valid pointer to a live `{request_type}` produced by this library.
/// Both pointers must remain valid until this function returns.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn {fn_start}(
client: *const {owner_ty},
req: *const {request_type},
) -> *mut {handle_name} {{
clear_last_error();
if client.is_null() {{
set_last_error(99, "{fn_start}: client must not be NULL");
return std::ptr::null_mut();
}}
if req.is_null() {{
set_last_error(99, "{fn_start}: req must not be NULL");
return std::ptr::null_mut();
}}
// SAFETY: caller guarantees `client` is a non-null, valid, aligned pointer to a live
// `{owner_ty}` value. The reference does not outlive this function.
let client_ref = unsafe {{ &*client }};
// SAFETY: caller guarantees `req` is a non-null, valid, aligned pointer to a live
// `{request_type}` value. We clone it to obtain an owned request independent of the
// caller's lifetime.
let req_owned = unsafe {{ (*req).clone() }};
let rt = match tokio::runtime::Runtime::new() {{
Ok(r) => r,
Err(e) => {{
set_last_error(99, &format!("{fn_start}: failed to create tokio runtime: {{e}}"));
return std::ptr::null_mut();
}}
}};
let stream_result = rt.block_on(async {{ client_ref.{core_path}(req_owned).await }});
let raw_stream = match stream_result {{
Ok(s) => s,
Err(e) => {{
set_last_error(99, &format!("{fn_start}: failed to open stream: {{e}}"));
return std::ptr::null_mut();
}}
}};
// Map the stream's concrete error type to Box<dyn Error> to erase it from the handle type.
let mapped: {stream_ty} = {{
use futures_util::StreamExt;
Box::pin(raw_stream.map(|r| r.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync + 'static>)))
}};
let handle = Box::new({handle_name} {{
rt,
stream: std::sync::Mutex::new(Some(mapped)),
}});
Box::into_raw(handle)
}}
/// Advance the stream and return a heap-allocated chunk, or null.
///
/// Returns null in two cases:
/// - Clean end-of-stream: `{prefix}_last_error_code()` returns 0.
/// - Stream error: `{prefix}_last_error_code()` returns non-zero.
///
/// The returned pointer is heap-allocated and the caller MUST free it by calling
/// `{prefix}_{owner_snake}_{item_type}_free` (or the appropriate type-free function).
///
/// # Safety
/// `handle` must be a non-null valid pointer previously returned by `{fn_start}` and not yet
/// freed. Calling `_next` after `_free` is undefined behaviour. The handle must not be shared
/// across threads without external synchronisation.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn {fn_next}(
handle: *mut {handle_name},
) -> *mut {core_item} {{
clear_last_error();
if handle.is_null() {{
set_last_error(99, "{fn_next}: handle must not be NULL");
return std::ptr::null_mut();
}}
// SAFETY: caller guarantees `handle` is a non-null valid pointer produced by `{fn_start}`
// and not yet freed. We take a shared reference for the duration of this call.
let h = unsafe {{ &*handle }};
let mut guard = match h.stream.lock() {{
Ok(g) => g,
Err(_) => {{
set_last_error(99, "{fn_next}: stream mutex is poisoned");
return std::ptr::null_mut();
}}
}};
let stream = match guard.as_mut() {{
Some(s) => s,
None => {{
// Stream already exhausted or taken.
return std::ptr::null_mut();
}}
}};
use futures_util::StreamExt;
match h.rt.block_on(stream.next()) {{
Some(Ok(chunk)) => {{
// SAFETY: We box the chunk and transfer ownership to the caller via raw pointer.
// The caller must free it via the appropriate type-free function.
Box::into_raw(Box::new(chunk))
}}
Some(Err(e)) => {{
set_last_error(99, &format!("{fn_next}: stream error: {{e}}"));
std::ptr::null_mut()
}}
None => {{
// Clean end-of-stream — error code remains 0 (cleared at top of function).
*guard = None;
std::ptr::null_mut()
}}
}}
}}
/// Free a stream handle created by `{fn_start}`.
///
/// Safe to call with a null pointer (no-op). After this call the handle pointer is invalid.
///
/// # Safety
/// `handle` must either be null or a valid pointer previously returned by `{fn_start}` and
/// not yet freed. Double-free is undefined behaviour.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn {fn_free}(handle: *mut {handle_name}) {{
if !handle.is_null() {{
// SAFETY: `handle` was produced by Box::into_raw in `{fn_start}` and has not been freed.
// Reconstructing the Box transfers ownership back to Rust, which drops it at end of scope.
unsafe {{ drop(Box::from_raw(handle)); }}
}}
}}"#,
handle_name = handle_name,
fn_start = fn_start,
fn_next = fn_next,
fn_free = fn_free,
prefix = prefix,
owner_ty = owner_ty,
request_type = request_type,
core_path = core_path,
stream_ty = stream_ty,
core_item = core_item,
owner_snake = owner_snake,
item_type = item_type,
)
}