use heck::ToPascalCase as _;
use proc_macro2::{Ident, Literal, TokenStream};
use quote::{format_ident, quote};
use crate::models::header_json::{
HeaderInterfaceFunction, HeaderJson, HeaderReturnValue, HeaderType,
};
use crate::util::{ident, make_load_safety_doc, option_as_slice, safe_ident};
pub fn generate_header_types(types: &[HeaderType]) -> TokenStream {
let type_definitions = types.iter().map(generate_type_definition);
quote! {
#( #type_definitions )*
}
}
pub struct InterfaceParts {
pub function_types: TokenStream,
pub struct_def: TokenStream,
pub struct_impl: TokenStream,
}
pub fn generate_interface_parts(interface: &[HeaderInterfaceFunction]) -> InterfaceParts {
let safety_doc = make_load_safety_doc();
let mut typedefs = Vec::new();
let mut fields = Vec::new();
let mut field_inits = Vec::new();
for func in interface {
let cfg_attr = make_since_attribute(&func.since);
let type_name = interface_type_name(func);
let field_name = ident(&func.name);
let params = generate_params(&func.arguments);
let return_clause = func
.return_value
.as_ref()
.map(map_return_clause)
.unwrap_or_default();
typedefs.push(quote! {
#cfg_attr
pub type #type_name = unsafe extern "C" fn(#( #params ),*) #return_clause;
});
let doc_attr = make_interface_func_doc(func);
fields.push(quote! {
#cfg_attr
#doc_attr
pub #field_name: #type_name,
});
let name_cstr = Literal::c_string(&std::ffi::CString::new(func.name.as_str()).unwrap());
let name_str = &func.name;
field_inits.push(quote! {
#cfg_attr
#field_name: {
let fptr = get_proc_address(#name_cstr.as_ptr())
.unwrap_or_else(|| panic!("failed to load `{}`", #name_str));
unsafe { std::mem::transmute::<unsafe extern "C" fn(), #type_name>(fptr) }
},
});
}
InterfaceParts {
function_types: quote! { #( #typedefs )* },
struct_def: quote! {
pub struct GDExtensionInterface {
#( #fields )*
}
},
struct_impl: quote! {
impl GDExtensionInterface {
#safety_doc
pub(crate) unsafe fn load(
get_proc_address: crate::GDExtensionInterfaceGetProcAddress,
) -> Self {
let get_proc_address = get_proc_address.expect("invalid get_proc_address function pointer");
Self { #( #field_inits )* }
}
}
},
}
}
pub fn generate_sys_gdextension_interface_from_json(header: &HeaderJson) -> TokenStream {
let header_types = generate_header_types(&header.types);
let InterfaceParts {
function_types: iface_function_types,
struct_def: iface_struct_def,
struct_impl: iface_struct_impl,
} = generate_interface_parts(&header.interface);
quote! {
pub type char16_t = u16;
pub type char32_t = u32;
pub type wchar_t = std::ffi::c_int;
#header_types
#iface_function_types
#iface_struct_def
#iface_struct_impl
}
}
fn interface_type_name(func: &HeaderInterfaceFunction) -> Ident {
format_ident!("GDExtensionInterface{}", func.name.to_pascal_case())
}
fn make_since_attribute(since: &str) -> TokenStream {
let since_minor: u8 = since
.strip_prefix("4.")
.expect("major version must be 4")
.parse()
.expect("invalid minor version in `since` field");
let (_, min_minor) = godot_bindings::MIN_SUPPORTED_VERSION;
if since_minor <= min_minor {
TokenStream::new()
} else {
quote! { #[cfg(since_api = #since)] }
}
}
fn make_interface_func_doc(func: &HeaderInterfaceFunction) -> TokenStream {
let mut doc_parts = Vec::new();
if !func.description.is_empty() {
doc_parts.push(func.description.join("\n"));
}
if !func.arguments.is_empty() {
let mut param_docs = Vec::new();
for arg in &func.arguments {
if let Some(name) = &arg.name
&& let Some(desc_lines) = &arg.description
&& !desc_lines.is_empty()
{
param_docs.push(format!("- `{}` - {}", name, desc_lines.join(" ")));
}
}
if !param_docs.is_empty() {
doc_parts.push(String::new());
doc_parts.push("## Parameters".to_string());
doc_parts.extend(param_docs);
}
}
let is_void = func
.return_value
.as_ref()
.is_none_or(|rv| rv.type_ == "void");
if !is_void
&& let Some(rv) = &func.return_value
&& let Some(ret_desc_lines) = &rv.description
&& !ret_desc_lines.is_empty()
{
doc_parts.push(String::new());
doc_parts.push("## Return value".to_string());
doc_parts.push(ret_desc_lines.join(" "));
}
if !doc_parts.is_empty() {
let doc_str = doc_parts.join("\n");
quote! { #[doc = #doc_str] }
} else {
TokenStream::new()
}
}
fn generate_type_definition(type_def: &HeaderType) -> TokenStream {
match type_def.kind.as_str() {
"enum" => generate_enum_type(type_def),
"handle" => generate_handle_type(type_def),
"alias" => generate_alias_type(type_def),
"struct" => generate_struct_type(type_def),
"function" => generate_function_type(type_def),
_ => TokenStream::new(),
}
}
fn generate_enum_type(type_def: &HeaderType) -> TokenStream {
let name = ident(&type_def.name);
let values = option_as_slice(&type_def.values).iter().map(|val| {
let variant_name = ident(&val.name);
let variant_value = Literal::i64_unsuffixed(val.value);
quote! {
pub const #variant_name: #name = #variant_value;
}
});
quote! {
pub type #name = std::ffi::c_int;
#( #values )*
}
}
fn generate_handle_type(type_def: &HeaderType) -> TokenStream {
let name = ident(&type_def.name);
let is_const = type_def.is_const == Some(true);
let opaque_name = if let Some(parent) = &type_def.parent {
if is_const {
handle_opaque_name(parent)
} else {
handle_opaque_name(&type_def.name)
}
} else {
handle_opaque_name(&type_def.name)
};
let opaque_ident = ident(&opaque_name);
let struct_def = if is_const && type_def.parent.is_some() {
TokenStream::new()
} else {
quote! {
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct #opaque_ident {
_unused: [u8; 0],
}
}
};
let type_alias = if is_const {
quote! {
pub type #name = *const #opaque_ident;
}
} else {
quote! {
pub type #name = *mut #opaque_ident;
}
};
quote! {
#struct_def
#type_alias
}
}
fn handle_opaque_name(handle_name: &str) -> String {
let stripped = handle_name
.strip_prefix("GDExtension")
.unwrap_or(handle_name);
let stripped = stripped.strip_prefix("Const").unwrap_or(stripped);
let stripped = stripped.strip_suffix("Ptr").unwrap_or(stripped);
format!("__Gdext{stripped}")
}
fn generate_alias_type(type_def: &HeaderType) -> TokenStream {
let name = ident(&type_def.name);
let target_type = type_def.type_.as_ref().map(|t| map_c_type(t));
quote! {
pub type #name = #target_type;
}
}
fn generate_struct_type(type_def: &HeaderType) -> TokenStream {
let name = ident(&type_def.name);
let fields = if let Some(members) = &type_def.members {
members
.iter()
.map(|member| {
let field_name = safe_ident(&member.name);
let field_type = map_c_type(&member.type_);
quote! {
pub #field_name: #field_type,
}
})
.collect::<Vec<_>>()
} else {
vec![]
};
quote! {
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct #name {
#( #fields )*
}
}
}
fn generate_function_type(type_def: &HeaderType) -> TokenStream {
let name = ident(&type_def.name);
let return_clause = type_def
.return_value
.as_ref()
.map(map_return_clause)
.unwrap_or_default();
let params = type_def
.arguments
.as_ref()
.map(|args| generate_params(args))
.unwrap_or_default();
quote! {
pub type #name = Option<unsafe extern "C" fn(#( #params ),*) #return_clause>;
}
}
fn generate_params(args: &[crate::models::header_json::HeaderArgument]) -> Vec<TokenStream> {
args.iter()
.map(|arg| {
let param_type = map_c_type(&arg.type_);
if let Some(param_name_str) = &arg.name {
if param_name_str.is_empty() {
quote! { #param_type }
} else {
let param_name = safe_ident(param_name_str);
quote! { #param_name: #param_type }
}
} else {
quote! { #param_type }
}
})
.collect()
}
fn map_c_type(c_type: &str) -> TokenStream {
let (is_const, c_type) = if let Some(rest) = c_type.strip_prefix("const ") {
(true, rest.trim())
} else {
(false, c_type)
};
if c_type.ends_with('*') {
let base_type = c_type.trim_end_matches('*').trim();
let inner = map_c_type_as_pointee(base_type);
return if is_const {
quote! { *const #inner }
} else {
quote! { *mut #inner }
};
}
map_c_base_type(c_type)
}
fn map_c_type_as_pointee(c_type: &str) -> TokenStream {
if c_type == "void" {
quote! { std::ffi::c_void }
} else {
map_c_type(c_type)
}
}
fn map_c_base_type(c_type: &str) -> TokenStream {
match c_type {
"void" => quote! { () },
"char" => quote! { std::ffi::c_char },
"int" => quote! { std::ffi::c_int }, "int8_t" => quote! { i8 },
"int16_t" => quote! { i16 },
"int32_t" => quote! { i32 },
"int64_t" => quote! { i64 },
"uint8_t" => quote! { u8 },
"uint16_t" => quote! { u16 },
"uint32_t" => quote! { u32 },
"uint64_t" => quote! { u64 },
"size_t" => quote! { usize },
"float" => quote! { f32 },
"double" => quote! { f64 },
_ => {
let type_ident = ident(c_type);
quote! { #type_ident }
}
}
}
fn map_return_clause(return_value: &HeaderReturnValue) -> TokenStream {
if return_value.type_ == "void" {
TokenStream::new()
} else {
let ty = map_c_type(&return_value.type_);
quote! { -> #ty }
}
}
#[cfg(test)] #[cfg_attr(published_docs, doc(cfg(test)))]
mod tests {
use nanoserde::DeJson;
use super::*;
fn load_header() -> HeaderJson {
let mut watch = godot_bindings::StopWatch::start();
let json_str = godot_bindings::load_gdextension_interface_json(&mut watch);
DeJson::deserialize_json(json_str.as_ref()).expect("failed to deserialize JSON")
}
#[test]
fn test_generate_header_code() {
let header = load_header();
let header_types = generate_header_types(&header.types).to_string();
assert!(header_types.contains("pub type GDExtensionVariantType"));
assert!(header_types.contains("GDEXTENSION_VARIANT_TYPE_NIL"));
assert!(header_types.contains("pub struct GDExtensionCallError"));
assert!(header_types.contains("pub struct __GdextMethodBind"));
assert!(header_types.contains("* const __GdextMethodBind"));
assert!(header_types.contains("* mut __GdextVariant"));
assert!(header_types.contains("* const __GdextVariant"));
assert!(header_types.contains("Option < unsafe extern \"C\" fn"));
let parts = generate_interface_parts(&header.interface);
let function_types = parts.function_types.to_string();
assert!(function_types.contains("pub type GDExtensionInterfaceMemAlloc"));
assert!(function_types.contains("unsafe extern \"C\" fn"));
assert!(
!function_types.contains("Option"),
"interface typedefs should be bare fn ptrs, not Option"
);
let struct_def = parts.struct_def.to_string();
assert!(struct_def.contains("pub struct GDExtensionInterface"));
assert!(struct_def.contains("pub mem_alloc : GDExtensionInterfaceMemAlloc"));
assert!(!struct_def.contains("Option"));
let struct_impl = parts.struct_impl.to_string();
assert!(struct_impl.contains("unsafe fn load"));
assert!(struct_impl.contains("get_proc_address"));
assert!(struct_impl.contains("transmute"));
assert!(struct_impl.contains("unwrap_or_else"));
}
}