#[cfg(test)]
mod enum_declaration_parity_tests;
mod error_helpers;
mod functions;
mod result_presence;
mod type_map;
mod types;
use crate::codegen::c_consumer;
use crate::core::backend::GeneratedFile;
use crate::core::config::{Language, ResolvedCrateConfig, resolve_output_dir};
use crate::core::ir::ApiSurface;
use std::path::PathBuf;
use error_helpers::emit_error_helpers;
use functions::emit_function;
use type_map::dart_module_name;
use types::{emit_enum, emit_type};
pub(crate) fn emit(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let ffi_features = crate::codegen::cfg::enabled_features_for_language(config, Language::Ffi);
let enum_features: std::collections::HashSet<&str> = ffi_features.iter().map(String::as_str).collect();
let module_name = dart_module_name(&config.name);
let prefix = config.ffi_prefix();
let lib_name = config.ffi_lib_name();
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);
let mut content = String::new();
content.push_str(&crate::backends::dart::template_env::render(
"dart_ffi_file_header.jinja",
minijinja::context! {
module_name => module_name.as_str(),
},
));
emit_lib_loader(&lib_name, &mut content);
emit_error_helpers(
&prefix,
&free_symbol,
&error_code_symbol,
&error_context_symbol,
&mut content,
);
let exclude_functions: std::collections::HashSet<&str> = config
.dart
.as_ref()
.map(|c| c.exclude_functions.iter().map(String::as_str).collect())
.unwrap_or_default();
let exclude_types: std::collections::HashSet<&str> = config
.dart
.as_ref()
.map(|c| c.exclude_types.iter().map(String::as_str).collect())
.unwrap_or_default();
let dart_capsule_types: std::collections::HashMap<String, crate::core::config::HostCapsuleTypeConfig> = config
.dart
.as_ref()
.map(|c| c.capsule_types.clone())
.unwrap_or_default();
for ty in api.types.iter().filter(|t| !exclude_types.contains(t.name.as_str())) {
emit_type(ty, &mut content);
content.push('\n');
}
for en in api.enums.iter().filter(|e| !exclude_types.contains(e.name.as_str())) {
emit_enum(en, &api.crate_name, Some(&enum_features), &mut content);
content.push('\n');
}
for f in api
.functions
.iter()
.filter(|f| !exclude_functions.contains(f.name.as_str()))
{
emit_function(
f,
&prefix,
&free_symbol,
&error_code_symbol,
&dart_capsule_types,
&mut content,
);
content.push('\n');
}
let dir = resolve_output_dir(None, &config.name, "packages/dart/lib/src");
let ffi_path = PathBuf::from(format!("{dir}/{module_name}_ffi.dart"));
let wrapper_path = PathBuf::from(format!("{dir}/{module_name}.dart"));
let wrapper_content = crate::backends::dart::template_env::render(
"dart_ffi_wrapper_file.jinja",
minijinja::context! {
module_name => module_name.as_str(),
},
);
let content = crate::core::hash::inject_stamp_line(
&content,
crate::core::hash::HANDLE_ABI_STAMP_KEY,
crate::core::template_versions::abi::HANDLE_ABI_VERSION,
);
Ok(vec![
GeneratedFile {
path: ffi_path,
content,
generated_header: false,
},
GeneratedFile {
path: wrapper_path,
content: wrapper_content,
generated_header: false,
},
])
}
fn emit_lib_loader(lib_name: &str, out: &mut String) {
out.push_str("/// Private singleton: the loaded native library.\n");
out.push_str("final DynamicLibrary _lib = DynamicLibrary.open(_libraryPath());\n\n");
out.push_str("/// Resolve the platform-specific path for the native shared library.\n");
out.push_str("String _libraryPath() {\n");
out.push_str(" if (Platform.isMacOS) {\n");
out.push_str(&crate::backends::dart::template_env::render(
"ffi_lib_path_macos_return.jinja",
minijinja::context! {
lib_name => lib_name,
},
));
out.push_str(" } else if (Platform.isWindows) {\n");
out.push_str(&crate::backends::dart::template_env::render(
"ffi_lib_path_windows_return.jinja",
minijinja::context! {
lib_name => lib_name,
},
));
out.push_str(" } else {\n");
out.push_str(&crate::backends::dart::template_env::render(
"ffi_lib_path_linux_return.jinja",
minijinja::context! {
lib_name => lib_name,
},
));
out.push_str(" }\n");
out.push_str("}\n\n");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lib_loader_contains_platform_branches() {
let mut out = String::new();
emit_lib_loader("my_lib", &mut out);
assert!(out.contains("Platform.isMacOS"), "missing macOS branch");
assert!(out.contains("Platform.isWindows"), "missing Windows branch");
assert!(out.contains("libmy_lib.dylib"), "missing dylib");
assert!(out.contains("my_lib.dll"), "missing dll");
assert!(out.contains("libmy_lib.so"), "missing so");
}
#[test]
fn error_helpers_reference_correct_symbols() {
let mut out = String::new();
emit_error_helpers(
"mylib",
"mylib_free_string",
"mylib_last_error_code",
"mylib_last_error_context",
&mut out,
);
assert!(out.contains("mylib_free_string"), "missing free_string symbol");
assert!(out.contains("mylib_last_error_code"), "missing error_code symbol");
assert!(out.contains("mylib_last_error_context"), "missing error_context symbol");
assert!(out.contains("_checkError"), "missing _checkError helper");
}
#[test]
fn emitted_ffi_dart_carries_the_handle_abi_stamp() {
let api = crate::core::ir::ApiSurface {
crate_name: "sample".to_string(),
types: vec![crate::core::ir::TypeDef {
name: "Thing".to_string(),
rust_path: "sample::Thing".to_string(),
is_opaque: true,
..crate::core::ir::TypeDef::default()
}],
..crate::core::ir::ApiSurface::default()
};
let config = crate::core::config::ResolvedCrateConfig {
name: "sample".to_string(),
..crate::core::config::ResolvedCrateConfig::default()
};
let files = emit(&api, &config).expect("dart gen_ffi emission");
let ffi_file = files
.iter()
.find(|file| file.path.to_string_lossy().ends_with("_ffi.dart"))
.expect("generated dart:ffi implementation");
assert!(
ffi_file.content.contains("Pointer<Void>"),
"fixture must actually emit the hardcoded handle type this stamp guards:\n{}",
ffi_file.content
);
crate::backends::ffi::handle_abi_stamp::assert_stamped_before_hashing(&ffi_file.content, "dart _ffi.dart");
let wrapper = files
.iter()
.find(|file| file.path != ffi_file.path)
.expect("generated re-export wrapper");
assert_eq!(
crate::core::hash::extract_stamp(&wrapper.content, crate::core::hash::HANDLE_ABI_STAMP_KEY),
None,
"the re-export wrapper names no handle type and must stay unstamped"
);
}
}