use alef_core::backend::GeneratedFile;
use alef_core::config::ResolvedCrateConfig;
use alef_core::ir::{ApiSurface, FunctionDef};
use alef_core::template_versions;
use std::collections::BTreeSet;
use std::path::PathBuf;
use crate::gen_bindings::{
emit_enum_pub, emit_error_type_pub, emit_function_jvm, emit_type_pub, format_param_pub, kotlin_type_str_pub,
to_lower_camel, to_pascal_case,
};
use crate::gen_native::emit_native_function_pub;
const BRIDGE_ALIAS: &str = "Bridge";
pub fn emit(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let package = config.kotlin_package();
let package_path = package.replace('.', "/");
let module_name = to_pascal_case(&config.name);
let crate_name = &config.name;
let mpp_root = "packages/kotlin-mpp".to_string();
let common_kt_path = PathBuf::from(&mpp_root)
.join("src/commonMain/kotlin")
.join(&package_path)
.join(format!("{module_name}.kt"));
let jvm_kt_path = PathBuf::from(&mpp_root)
.join("src/jvmMain/kotlin")
.join(&package_path)
.join(format!("{module_name}.kt"));
let native_kt_path = PathBuf::from(&mpp_root)
.join("src/nativeMain/kotlin")
.join(&package_path)
.join(format!("{module_name}.kt"));
let def_path = PathBuf::from(&mpp_root).join(format!("{crate_name}.def"));
let gradle_path = PathBuf::from(&mpp_root).join("build.gradle.kts");
Ok(vec![
GeneratedFile {
path: common_kt_path,
content: emit_common(api, config),
generated_header: false,
},
GeneratedFile {
path: jvm_kt_path,
content: emit_jvm_actual(api, config),
generated_header: false,
},
GeneratedFile {
path: native_kt_path,
content: emit_native_actual(api, config),
generated_header: false,
},
GeneratedFile {
path: def_path,
content: emit_def_file(config),
generated_header: false,
},
GeneratedFile {
path: gradle_path,
content: emit_gradle_build(config),
generated_header: false,
},
])
}
fn emit_common(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
let package = config.kotlin_package();
let module_name = to_pascal_case(&config.name);
let exclude_functions: std::collections::HashSet<&str> = config
.kotlin
.as_ref()
.map(|c| c.exclude_functions.iter().map(String::as_str).collect())
.unwrap_or_default();
let exclude_types: std::collections::HashSet<&str> = config
.kotlin
.as_ref()
.map(|c| c.exclude_types.iter().map(String::as_str).collect())
.unwrap_or_default();
let mut imports: BTreeSet<String> = BTreeSet::new();
let mut body = String::new();
for ty in api.types.iter().filter(|t| !exclude_types.contains(t.name.as_str())) {
emit_type_pub(ty, &mut body, &mut imports);
body.push('\n');
}
for en in api.enums.iter().filter(|e| !exclude_types.contains(e.name.as_str())) {
emit_enum_pub(en, &mut body, &package);
body.push('\n');
}
for error in &api.errors {
emit_error_type_pub(error, &mut body, &mut imports);
body.push('\n');
}
let visible_functions: Vec<&FunctionDef> = api
.functions
.iter()
.filter(|f| !exclude_functions.contains(f.name.as_str()))
.collect();
if !visible_functions.is_empty() {
body.push_str(&crate::template_env::render(
"expect_object_declaration.jinja",
minijinja::context! {
name => module_name,
},
));
for f in &visible_functions {
emit_expect_function(f, &mut body, &mut imports);
body.push('\n');
}
body.push_str("}\n");
}
render_kt_file(&package, &imports, &body)
}
fn emit_expect_function(f: &FunctionDef, out: &mut String, imports: &mut BTreeSet<String>) {
if !f.doc.is_empty() {
let doc_lines: Vec<String> = f.doc.lines().map(ToString::to_string).collect();
out.push_str(&crate::template_env::render(
"doc_comment.jinja",
minijinja::context! {
indent => " ",
lines => doc_lines,
},
));
}
let params: Vec<String> = f.params.iter().map(|p| format_param_pub(p, imports)).collect();
let return_ty = kotlin_type_str_pub(&f.return_type, false, imports);
let async_kw = if f.is_async { "suspend " } else { "" };
let func_name_camel = to_lower_camel(&f.name);
out.push_str(&crate::template_env::render(
"expect_function_signature.jinja",
minijinja::context! {
async_kw => async_kw,
name => func_name_camel,
params => params.join(", "),
return_type => return_ty,
},
));
}
fn emit_jvm_actual(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
let package = config.kotlin_package();
let module_name = to_pascal_case(&config.name);
let java_package = config.java_package();
let exclude_functions: std::collections::HashSet<&str> = config
.kotlin
.as_ref()
.map(|c| c.exclude_functions.iter().map(String::as_str).collect())
.unwrap_or_default();
let visible_functions: Vec<&FunctionDef> = api
.functions
.iter()
.filter(|f| !exclude_functions.contains(f.name.as_str()))
.collect();
let mut imports: BTreeSet<String> = BTreeSet::new();
let mut body = String::new();
if !visible_functions.is_empty() {
imports.insert(format!("import {java_package}.{module_name} as {BRIDGE_ALIAS}"));
if visible_functions.iter().any(|f| f.is_async) {
imports.insert("import kotlinx.coroutines.Dispatchers".to_string());
imports.insert("import kotlinx.coroutines.future.await".to_string());
imports.insert("import kotlinx.coroutines.withContext".to_string());
}
body.push_str(&crate::template_env::render(
"actual_object_declaration.jinja",
minijinja::context! {
name => module_name,
},
));
for f in &visible_functions {
emit_function_jvm(f, &mut body, &mut imports, &java_package);
body.push('\n');
}
body.push_str("}\n");
}
render_kt_file(&package, &imports, &body)
}
fn emit_native_actual(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
let package = config.kotlin_package();
let module_name = to_pascal_case(&config.name);
let prefix = config.ffi_prefix();
let crate_name = &config.name;
let exclude_functions: std::collections::HashSet<&str> = config
.kotlin
.as_ref()
.map(|c| c.exclude_functions.iter().map(String::as_str).collect())
.unwrap_or_default();
let visible_functions: Vec<&FunctionDef> = api
.functions
.iter()
.filter(|f| !exclude_functions.contains(f.name.as_str()))
.collect();
let mut imports: BTreeSet<String> = BTreeSet::new();
imports.insert("import kotlinx.cinterop.*".to_string());
imports.insert(format!("import {crate_name}.*"));
let mut body = String::new();
if !visible_functions.is_empty() {
body.push_str(&crate::template_env::render(
"actual_object_declaration.jinja",
minijinja::context! {
name => module_name,
},
));
for f in &visible_functions {
emit_native_function_pub(f, &prefix, &mut body);
body.push('\n');
}
body.push_str("}\n");
}
render_kt_file(&package, &imports, &body)
}
fn emit_def_file(config: &ResolvedCrateConfig) -> String {
let header = config.ffi_header_name();
let lib_name = config.ffi_lib_name();
let prefix = config.ffi_prefix();
format!("headers = {header}\nheaderFilter = {prefix}_*\nlinkerOpts = -L../../../target/release -l{lib_name}\n")
}
fn emit_gradle_build(config: &ResolvedCrateConfig) -> String {
let crate_name = &config.name;
let kotlin_version = template_versions::maven::KOTLIN_JVM_PLUGIN;
format!(
r#"// Generated by alef. Do not edit by hand.
plugins {{
kotlin("multiplatform") version "{kotlin_version}"
}}
kotlin {{
jvm()
linuxX64 {{
compilations["main"].cinterops {{
val {crate_name} by creating {{
defFile = project.file("{crate_name}.def")
}}
}}
binaries {{
sharedLib()
}}
}}
macosArm64 {{
compilations["main"].cinterops {{
val {crate_name} by creating {{
defFile = project.file("{crate_name}.def")
}}
}}
binaries {{
sharedLib()
}}
}}
iosX64 {{
compilations["main"].cinterops {{
val {crate_name} by creating {{
defFile = project.file("{crate_name}.def")
}}
}}
binaries {{
framework()
}}
}}
iosArm64 {{
compilations["main"].cinterops {{
val {crate_name} by creating {{
defFile = project.file("{crate_name}.def")
}}
}}
binaries {{
framework()
}}
}}
iosSimulatorArm64 {{
compilations["main"].cinterops {{
val {crate_name} by creating {{
defFile = project.file("{crate_name}.def")
}}
}}
binaries {{
framework()
}}
}}
androidNativeArm64 {{
compilations["main"].cinterops {{
val {crate_name} by creating {{
defFile = project.file("{crate_name}.def")
}}
}}
binaries {{
sharedLib()
}}
}}
sourceSets {{
val commonMain by getting
val jvmMain by getting
val nativeMain by creating {{
dependsOn(commonMain)
}}
val linuxX64Main by getting {{
dependsOn(nativeMain)
}}
val macosArm64Main by getting {{
dependsOn(nativeMain)
}}
val iosMain by creating {{
dependsOn(nativeMain)
}}
val iosX64Main by getting {{
dependsOn(iosMain)
}}
val iosArm64Main by getting {{
dependsOn(iosMain)
}}
val iosSimulatorArm64Main by getting {{
dependsOn(iosMain)
}}
val androidNativeArm64Main by getting {{
dependsOn(nativeMain)
}}
}}
}}
"#
)
}
fn render_kt_file(package: &str, imports: &BTreeSet<String>, body: &str) -> String {
let mut content = String::new();
content.push_str("// Generated by alef. Do not edit by hand.\n\n");
content.push_str(&crate::template_env::render(
"package_declaration.jinja",
minijinja::context! {
package => package,
},
));
content.push_str("\n\n");
for import in imports {
content.push_str(import);
content.push('\n');
}
if !imports.is_empty() {
content.push('\n');
}
content.push_str(body);
content
}