use alef::backends::zig::ZigBackend;
use alef::core::backend::Backend;
use alef::core::config::{NewAlefConfig, ResolvedCrateConfig};
use alef::core::ir::{
ApiSurface, HandlerContractDef, MethodDef, ParamDef, ReceiverKind, RegistrationDef, ServiceDef, TypeRef,
};
use std::fs;
use std::path::Path;
use std::process::{Command, Output};
fn config() -> ResolvedCrateConfig {
let parsed: NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["ffi", "zig"]
[[crates]]
name = "test"
sources = ["src/lib.rs"]
[crates.ffi]
prefix = "test"
"#,
)
.expect("parse fixture config");
parsed.resolve().expect("resolve fixture config").remove(0)
}
fn surface() -> ApiSurface {
ApiSurface {
crate_name: "test".to_owned(),
version: "0.1.0".to_owned(),
services: vec![ServiceDef {
name: "TestService".to_owned(),
rust_path: "test::TestService".to_owned(),
constructor: MethodDef {
name: "new".to_owned(),
is_static: true,
return_type: TypeRef::Unit,
..MethodDef::default()
},
configurators: vec![],
registrations: vec![route_registration()],
entrypoints: vec![],
doc: "Neutral Zig service ABI fixture.".to_owned(),
cfg: None,
}],
handler_contracts: vec![handler_contract()],
..ApiSurface::default()
}
}
fn route_registration() -> RegistrationDef {
RegistrationDef {
method: "route".to_owned(),
callback_param: "handler".to_owned(),
callback_contract: "RequestHandler".to_owned(),
metadata_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
..ParamDef::default()
}],
receiver: Some(ReceiverKind::RefMut),
return_type: TypeRef::Unit,
..RegistrationDef::default()
}
}
fn handler_contract() -> HandlerContractDef {
HandlerContractDef {
trait_name: "RequestHandler".to_owned(),
rust_path: "test::RequestHandler".to_owned(),
dispatch: MethodDef {
name: "handle".to_owned(),
params: vec![ParamDef {
name: "request".to_owned(),
ty: TypeRef::Named("Request".to_owned()),
..ParamDef::default()
}],
return_type: TypeRef::Named("Response".to_owned()),
receiver: Some(ReceiverKind::Ref),
cfg: None,
..MethodDef::default()
},
optional_methods: vec![],
wire_request_type: Some("Request".to_owned()),
wire_response_type: Some("Response".to_owned()),
dispatch_extra_params: vec![],
wire_param_name: None,
dispatch_return_type: None,
response_adapter: None,
doc: "Neutral handler contract.".to_owned(),
}
}
fn write_fixture(directory: &Path, service_source: &str, header_name: &str) {
fs::write(directory.join("test_service.zig"), service_source).expect("write generated Zig service");
fs::write(directory.join(header_name), C_HEADER).expect("write C header");
fs::write(directory.join("service_fixture.c"), C_SOURCE).expect("write C source");
fs::write(directory.join("allocator_test.zig"), ZIG_HARNESS).expect("write Zig harness");
}
fn output_or_panic(action: &str, output: Output) {
assert!(
output.status.success(),
"{action} failed\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
const C_HEADER: &str = r#"#include <stdint.h>
typedef char *(*handler_callback_t)(void *, const char *);
typedef void (*handler_response_free_t)(char *);
uint64_t test_test_service_new(void);
void test_test_service_free(uint64_t owner);
int test_test_service_register_route(
uint64_t owner,
handler_callback_t callback,
handler_response_free_t response_free,
void *context,
const char *path
);
"#;
const C_SOURCE: &str = r#"#include "test.h"
#include <string.h>
uint64_t test_test_service_new(void) { return 41; }
void test_test_service_free(uint64_t owner) { (void)owner; }
int test_test_service_register_route(
uint64_t owner,
handler_callback_t callback,
handler_response_free_t response_free,
void *context,
const char *path
) {
char *response = callback(context, "{}");
int valid = response != 0 && strcmp(response, "{}") == 0;
response_free(response);
return owner == 41 && valid && strcmp(path, "/ok") == 0 ? 17 : -1;
}
"#;
const ZIG_HARNESS: &str = r#"const std = @import("std");
const service = @import("test_service.zig");
fn handler(context: *anyopaque, request: [*:0]const u8) callconv(.c) [*:0]u8 {
_ = context;
_ = request;
return std.heap.c_allocator.dupeZ(u8, "{}") catch unreachable;
}
fn freeResponse(response: [*:0]u8) callconv(.c) void {
std.heap.c_allocator.free(std.mem.span(response));
}
test "service registration preserves allocator pairing" {
var instance = service.TestService.init();
defer instance.deinit();
const context: *anyopaque = @ptrFromInt(1);
const status = instance.route(handler, freeResponse, context, "/ok");
try std.testing.expectEqual(@as(c_int, 17), status);
}
"#;
#[test]
fn generated_zig_service_matches_allocator_callback_abi_at_runtime() {
if which::which("zig").is_err() || which::which("cc").is_err() {
return;
}
let config = config();
let files = ZigBackend
.generate_service_api(&surface(), &config)
.expect("generate Zig service API");
let service_source = &files.first().expect("generated Zig service file").content;
assert!(service_source.contains("response_free"));
assert!(service_source.contains("owner: u64"));
let directory = tempfile::tempdir().expect("create fixture directory");
write_fixture(directory.path(), service_source, &config.ffi_header_name());
let output = Command::new("zig")
.args(["test", "allocator_test.zig", "service_fixture.c", "-I", ".", "-lc"])
.current_dir(directory.path())
.output()
.expect("run Zig service allocator harness");
output_or_panic("compile and run generated Zig service", output);
}