use crate::codegen::component::{
ComponentContractIr, ComponentMethodIr, ComponentParamIr, ResolvedComponent, ResolvedProvidedContract, WireType,
};
use crate::core::config::ResolvedCrateConfig;
use anyhow::{Context as _, Result, bail};
use heck::{ToSnakeCase, ToUpperCamelCase};
use std::fmt::Write as _;
pub fn generate_component_proxies(config: &ResolvedCrateConfig, resolved: &[ResolvedComponent]) -> Result<String> {
let mut out = String::new();
for component in resolved {
for provided in &component.provides {
write!(out, "{}", generate_proxy(config, provided)?)?;
}
}
Ok(out)
}
fn generate_proxy(config: &ResolvedCrateConfig, provided: &ResolvedProvidedContract) -> Result<String> {
validate_proxy_methods(&provided.contract)?;
let contract_cfg = config
.component_contracts
.iter()
.find(|contract| contract.name == provided.contract.name)
.with_context(|| {
format!(
"resolved contract `{}` has no matching config entry",
provided.contract.name
)
})?;
let trait_path = &contract_cfg.trait_path;
let contract_name = provided.contract.name.to_upper_camel_case();
let table_name = format!("{contract_name}ApiV{}", provided.contract.interface_version);
let proxy_name = format!("{contract_name}Proxy");
let contract_hash = byte_array(&provided.contract_hash);
let mut out = String::new();
writeln!(out, "// This file is auto-generated by alef. DO NOT EDIT.")?;
writeln!(out, "#[repr(C)]")?;
writeln!(out, "struct {table_name} {{")?;
out.push_str(" struct_size: usize,\n");
for method in &provided.contract.methods {
writeln!(
out,
" {}: Option<{}>,",
method.name.to_snake_case(),
function_pointer_type(method)
)?;
}
out.push_str("}\n\n");
writeln!(out, "unsafe impl alef_component_abi::AlefContract for {table_name} {{")?;
writeln!(out, " const CONTRACT_HASH: [u8; 32] = {contract_hash};")?;
out.push_str("}\n\n");
writeln!(out, "pub struct {proxy_name} {{")?;
writeln!(
out,
" instance: alef_component_runtime::ComponentInstance<{table_name}>,"
)?;
out.push_str("}\n\n");
writeln!(out, "impl {proxy_name} {{")?;
writeln!(
out,
" pub fn new(component: &std::sync::Arc<alef_component_runtime::LoadedComponent>) -> Result<Self, String> {{"
)?;
writeln!(
out,
" let instance = component.instantiate::<{table_name}>().map_err(|error| error.to_string())?;"
)?;
out.push_str(" Ok(Self { instance })\n }\n}\n\n");
writeln!(out, "impl {trait_path} for {proxy_name} {{")?;
for method in &provided.contract.methods {
write!(out, "{}", emit_method(trait_path, method)?)?;
}
out.push_str("}\n\n");
Ok(out)
}
fn supported_wire_type(ty: &WireType) -> bool {
matches!(
ty,
WireType::Unit
| WireType::Bool
| WireType::U8
| WireType::U16
| WireType::U32
| WireType::U64
| WireType::I8
| WireType::I16
| WireType::I32
| WireType::I64
| WireType::F32
| WireType::F64
| WireType::Utf8
| WireType::Bytes
)
}
fn validate_proxy_methods(contract: &ComponentContractIr) -> Result<()> {
for method in &contract.methods {
if method.is_async {
bail!(
"component proxy generation does not yet support the async method `{}`; a follow-up widens the producer and the proxy together",
method.name
);
}
if method.fallible && method.error_type.is_none() {
bail!(
"fallible component method `{}` has no recorded error type; the proxy cannot construct a typed error",
method.name
);
}
for param in &method.params {
if !supported_wire_type(¶m.ty) {
bail!(
"component proxy does not yet support {:?} in parameter {}::{}",
param.ty,
method.name,
param.name
);
}
}
if !supported_wire_type(&method.result) {
bail!(
"component proxy does not yet support {:?} as the result of {}",
method.result,
method.name
);
}
}
Ok(())
}
fn wire_ffi_type(ty: &WireType, output: bool) -> String {
match ty {
WireType::Unit => "()".to_string(),
WireType::Bool | WireType::U8 => "u8".to_string(),
WireType::U16 => "u16".to_string(),
WireType::U32 => "u32".to_string(),
WireType::U64 => "u64".to_string(),
WireType::I8 => "i8".to_string(),
WireType::I16 => "i16".to_string(),
WireType::I32 => "i32".to_string(),
WireType::I64 => "i64".to_string(),
WireType::F32 => "f32".to_string(),
WireType::F64 => "f64".to_string(),
WireType::Utf8 if output => "alef_component_abi::AlefOwnedBuffer".to_string(),
WireType::Utf8 => "alef_component_abi::AlefStr".to_string(),
WireType::Bytes if output => "alef_component_abi::AlefOwnedBuffer".to_string(),
WireType::Bytes => "alef_component_abi::AlefSlice".to_string(),
other => unreachable!("validated as unsupported by the proxy: {other:?}"),
}
}
fn function_pointer_type(method: &ComponentMethodIr) -> String {
let mut params = vec!["*mut std::ffi::c_void".to_string()];
params.extend(method.params.iter().map(|param| wire_ffi_type(¶m.ty, false)));
if method.result != WireType::Unit {
params.push(format!("*mut {}", wire_ffi_type(&method.result, true)));
}
params.push("*mut alef_component_abi::AlefOwnedBuffer".to_string());
format!(
"unsafe extern \"C\" fn({}) -> alef_component_abi::AlefStatus",
params.join(", ")
)
}
fn rust_param_type(param: &ComponentParamIr) -> String {
match ¶m.ty {
WireType::Utf8 if param.borrowed => "&str".to_string(),
WireType::Utf8 => "String".to_string(),
WireType::Bytes if param.borrowed => "&[u8]".to_string(),
WireType::Bytes => "Vec<u8>".to_string(),
WireType::Bool => "bool".to_string(),
other => wire_ffi_type(other, false),
}
}
fn rust_result_type(ty: &WireType) -> String {
match ty {
WireType::Utf8 => "String".to_string(),
WireType::Bytes => "Vec<u8>".to_string(),
WireType::Bool => "bool".to_string(),
other => wire_ffi_type(other, false),
}
}
fn error_type_path(trait_path: &str, error_type: &str) -> String {
let crate_root = trait_path.split("::").next().unwrap_or(trait_path);
format!("{crate_root}::{error_type}")
}
fn wire_argument_expression(name: &str, ty: &WireType) -> String {
match ty {
WireType::Utf8 => format!("alef_component_abi::AlefStr {{ ptr: {name}.as_ptr().cast(), len: {name}.len() }}"),
WireType::Bytes => format!("alef_component_abi::AlefSlice {{ ptr: {name}.as_ptr(), len: {name}.len() }}"),
WireType::Bool => format!("u8::from({name})"),
_ => name.to_string(),
}
}
fn zero_wire_value(ty: &WireType) -> String {
match ty {
WireType::Utf8 | WireType::Bytes => "alef_component_abi::AlefOwnedBuffer::EMPTY".to_string(),
WireType::F32 | WireType::F64 => "0.0".to_string(),
_ => "0".to_string(),
}
}
fn output_conversion(ty: &WireType, value: &str) -> String {
match ty {
WireType::Utf8 => {
format!(
"String::from_utf8_lossy(&unsafe {{ alef_component_runtime::take_owned_bytes({value}) }}).into_owned()"
)
}
WireType::Bytes => format!("unsafe {{ alef_component_runtime::take_owned_bytes({value}) }}"),
WireType::Bool => format!("{value} != 0"),
_ => value.to_string(),
}
}
fn method_signature(trait_path: &str, method: &ComponentMethodIr) -> String {
let params = method
.params
.iter()
.map(|param| format!("{}: {}", param.name.to_snake_case(), rust_param_type(param)))
.collect::<Vec<_>>()
.join(", ");
let plain_return = rust_result_type(&method.result);
let return_type = if method.fallible {
let error_type = method
.error_type
.as_deref()
.expect("validated as present for fallible methods");
format!("Result<{plain_return}, {}>", error_type_path(trait_path, error_type))
} else {
plain_return
};
format!(
"fn {}(&self{}) -> {return_type}",
method.name.to_snake_case(),
prefixed(¶ms)
)
}
fn call_arguments(method: &ComponentMethodIr) -> Vec<String> {
let mut call_args = vec!["self.instance.handle()".to_string()];
call_args.extend(
method
.params
.iter()
.map(|param| wire_argument_expression(¶m.name.to_snake_case(), ¶m.ty)),
);
if method.result != WireType::Unit {
call_args.push("&mut out_result".to_string());
}
call_args.push("&mut out_error".to_string());
call_args
}
fn emit_success_arm(out: &mut String, method: &ComponentMethodIr) -> Result<()> {
let success_value = if method.result == WireType::Unit {
"()".to_string()
} else {
output_conversion(&method.result, "out_result")
};
if method.fallible {
writeln!(out, " Ok({success_value})")?;
} else {
writeln!(out, " {success_value}")?;
}
Ok(())
}
fn emit_failure_arm(out: &mut String, trait_path: &str, method: &ComponentMethodIr) -> Result<()> {
let method_name = method.name.to_snake_case();
out.push_str(" let message = unsafe { alef_component_runtime::take_owned_text(out_error) };\n");
if method.fallible {
let error_type = method
.error_type
.as_deref()
.expect("validated as present for fallible methods");
writeln!(
out,
" Err({}::from(message))",
error_type_path(trait_path, error_type)
)?;
} else {
writeln!(
out,
" panic!(\"component method `{method_name}` failed with status {{}}: {{message}}\", status.0)"
)?;
}
Ok(())
}
fn emit_method(trait_path: &str, method: &ComponentMethodIr) -> Result<String> {
let method_name = method.name.to_snake_case();
let mut out = String::new();
writeln!(out, " {} {{", method_signature(trait_path, method))?;
out.push_str(" let table = self.instance.table();\n");
writeln!(
out,
" let function = table.{method_name}.expect(\"activated component table is missing `{method_name}`\");"
)?;
if method.result != WireType::Unit {
writeln!(
out,
" let mut out_result: {} = {};",
wire_ffi_type(&method.result, true),
zero_wire_value(&method.result)
)?;
}
out.push_str(" let mut out_error = alef_component_abi::AlefOwnedBuffer::EMPTY;\n");
writeln!(
out,
" let status = unsafe {{ function({}) }};",
call_arguments(method).join(", ")
)?;
out.push_str(" if status.is_ok() {\n");
emit_success_arm(&mut out, method)?;
out.push_str(" } else {\n");
emit_failure_arm(&mut out, trait_path, method)?;
out.push_str(" }\n }\n");
Ok(out)
}
fn prefixed(params: &str) -> String {
if params.is_empty() {
String::new()
} else {
format!(", {params}")
}
}
fn byte_array(bytes: &[u8; 32]) -> String {
format!("[{}]", bytes.iter().map(u8::to_string).collect::<Vec<_>>().join(", "))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codegen::component::resolve_components;
use crate::core::config::{ComponentConfig, ComponentContractConfig, ComponentProvidesConfig};
use crate::core::ir::{ApiSurface, MethodDef, ParamDef, ReceiverKind, TypeDef, TypeRef};
fn fixture() -> (ApiSurface, ResolvedCrateConfig) {
two_contract_fixture(false)
}
fn add_coder_contract(
types: &mut Vec<TypeDef>,
component_contracts: &mut Vec<ComponentContractConfig>,
provides: &mut Vec<ComponentProvidesConfig>,
) {
types.push(TypeDef {
name: "Coder".into(),
rust_path: "demo::Coder".into(),
is_trait: true,
is_opaque: true,
methods: vec![MethodDef {
name: "encode".into(),
return_type: TypeRef::Bytes,
receiver: Some(ReceiverKind::Ref),
..MethodDef::default()
}],
..TypeDef::default()
});
component_contracts.push(ComponentContractConfig {
name: "coder".into(),
trait_path: "demo::Coder".into(),
interface_version: 1,
});
provides.push(ComponentProvidesConfig {
contract: "coder".into(),
implementation: "demo::ReverseCoder".into(),
});
}
fn two_contract_fixture(second_contract: bool) -> (ApiSurface, ResolvedCrateConfig) {
let mut types = vec![TypeDef {
name: "Codec".into(),
rust_path: "demo::Codec".into(),
is_trait: true,
is_opaque: true,
methods: vec![
MethodDef {
name: "transform".into(),
params: vec![ParamDef {
name: "input".into(),
ty: TypeRef::Bytes,
is_ref: true,
..ParamDef::default()
}],
return_type: TypeRef::Bytes,
receiver: Some(ReceiverKind::Ref),
error_type: Some("Error".into()),
..MethodDef::default()
},
MethodDef {
name: "name".into(),
return_type: TypeRef::String,
receiver: Some(ReceiverKind::Ref),
..MethodDef::default()
},
],
..TypeDef::default()
}];
let mut component_contracts = vec![ComponentContractConfig {
name: "codec".into(),
trait_path: "demo::Codec".into(),
interface_version: 1,
}];
let mut provides = vec![ComponentProvidesConfig {
contract: "codec".into(),
implementation: "demo::ReverseCodec".into(),
}];
if second_contract {
add_coder_contract(&mut types, &mut component_contracts, &mut provides);
}
let api = ApiSurface {
crate_name: "demo".into(),
version: "1.2.3".into(),
types,
..ApiSurface::default()
};
let config = ResolvedCrateConfig {
name: "demo".into(),
component_contracts,
components: vec![ComponentConfig {
name: "reverse".into(),
provides,
features: vec!["reverse".into()],
default_features: false,
targets: Some(vec!["x86_64-unknown-linux-gnu".into()]),
bundled_on: Vec::new(),
}],
..ResolvedCrateConfig::default()
};
(api, config)
}
#[test]
fn generated_proxy_is_valid_rust_and_wires_both_methods() {
let (api, config) = fixture();
let resolved = resolve_components(&api, &config).unwrap();
let generated = generate_component_proxies(&config, &resolved).unwrap();
assert!(generated.contains("struct CodecApiV1"), "{generated}");
assert!(
generated.contains("unsafe impl alef_component_abi::AlefContract for CodecApiV1"),
"{generated}"
);
assert!(generated.contains("pub struct CodecProxy"), "{generated}");
assert!(generated.contains("impl demo::Codec for CodecProxy"), "{generated}");
assert!(
generated.contains("fn transform(&self, input: &[u8]) -> Result<Vec<u8>, demo::Error>"),
"{generated}"
);
assert!(generated.contains("fn name(&self) -> String {"), "{generated}");
assert!(
generated.contains("alef_component_runtime::take_owned_bytes(out_result)"),
"{generated}"
);
assert!(generated.contains("Err(demo::Error::from(message))"), "{generated}");
let wrapped = format!("mod generated {{ {generated} }}");
syn::parse_file(&wrapped).expect("generated component proxy must be valid Rust syntax");
}
#[test]
fn a_component_providing_two_contracts_gets_two_proxies() {
let (api, config) = two_contract_fixture(true);
let resolved = resolve_components(&api, &config).unwrap();
assert_eq!(resolved.len(), 1);
assert_eq!(resolved[0].provides.len(), 2);
let generated = generate_component_proxies(&config, &resolved).unwrap();
assert!(generated.contains("pub struct CodecProxy"), "{generated}");
assert!(generated.contains("pub struct CoderProxy"), "{generated}");
assert!(generated.contains("impl demo::Coder for CoderProxy"), "{generated}");
let wrapped = format!("mod generated {{ {generated} }}");
syn::parse_file(&wrapped).expect("generated component proxies must be valid Rust syntax");
}
#[test]
fn rejects_async_methods() {
let (mut api, config) = fixture();
api.types[0].methods[0].is_async = true;
let resolved = resolve_components(&api, &config).unwrap();
let error = generate_component_proxies(&config, &resolved).unwrap_err();
assert!(error.to_string().contains("does not yet support the async method"));
}
}