use compose_rust::codegen::{
WEB_HOST_WASM_NAME, WEB_MEMORY_MIN_PAGES, generate_wasm_rust, generate_web_bridge_kotlin,
generate_web_loader_js,
};
use compose_rust::schema::{
BOUNDARY_SCHEMA, BoundaryOp, BoundaryParam, WEB_BATCH_BYTES, WEB_BATCH_FIELDS,
WEB_EVENT_BUFFER_BYTES, WEB_EVENT_BUFFER_OFFSET, WEB_RUST_REGION_BASE, WEB_START_SYMBOL,
};
fn symbol(op: &BoundaryOp) -> String {
let mut snake = String::new();
for (index, character) in op.name.char_indices() {
if character.is_ascii_uppercase() && index != 0 {
snake.push('_');
}
snake.push(character.to_ascii_lowercase());
}
format!("dioxus_compose_host_web_{snake}")
}
fn arguments(op: &BoundaryOp) -> usize {
op.params
.iter()
.map(|param| match param {
BoundaryParam::Bytes { .. } => 2,
BoundaryParam::Nanos { .. } => 2,
})
.sum::<usize>()
+ usize::from(op.returns_batch || op.name == "ReleaseBatch")
}
#[test]
fn pr6_generated_web_bindings_match_the_boundary_schema() {
let rust = generate_wasm_rust();
let kotlin = generate_web_bridge_kotlin();
let loader = generate_web_loader_js();
for op in BOUNDARY_SCHEMA {
let symbol = symbol(op);
assert!(
rust.contains(&format!("pub extern \"C\" fn {symbol}(")),
"missing wasm shim for {}",
op.name
);
assert!(
rust.contains(&format!("crate::boundary::{}(", op.symbol)),
"the shim for {} does not call {}",
op.name,
op.symbol
);
assert!(
kotlin.contains(&format!("external fun host{}(", op.name)),
"missing forwarder declaration for {}",
op.name
);
assert!(
kotlin.contains(&symbol),
"the forwarder for {} does not name {symbol}",
op.name
);
}
assert!(
rust.contains("pub fn web_start(builder: LaunchBuilder, app: fn() -> Element) -> u32 {"),
"the extra entry point a page needs in place of a library loader is missing"
);
assert!(
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/lib.rs")).contains(&format!(
"pub extern \"C\" fn {WEB_START_SYMBOL}() -> u32 {{"
)),
"`web_main!` has to export it under the name the page calls"
);
assert!(
kotlin.contains(&format!("host.{WEB_START_SYMBOL}()")),
"the Renderer has to start the Host it just instantiated"
);
assert!(
loader.contains("WebAssembly.compileStreaming("),
"compiling before the Renderer's module is evaluated is the page's whole job"
);
assert_eq!(
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/boundary_wasm.gen.rs"
)),
rust,
"generated wasm shims are stale; run `cargo run -p compose-rust --bin codegen`",
);
assert_eq!(
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../dioxus-compose-renderer/web/src/bridge/HostBridge.gen.kt"
)),
kotlin,
"generated Kotlin bridge is stale; run `cargo run -p compose-rust --bin codegen`",
);
assert_eq!(
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../dioxus-compose-renderer/web/resources/dioxus-compose-host.gen.mjs"
)),
loader,
"the generated loader is stale; run `cargo run -p compose-rust --bin codegen`",
);
}
#[test]
fn pr6_both_halves_agree_on_the_argument_counts() {
let rust = generate_wasm_rust();
let kotlin = generate_web_bridge_kotlin();
for op in BOUNDARY_SCHEMA {
let expected = arguments(op);
let declaration = format!("external fun host{}(", op.name);
assert_eq!(
count(&signature_after(&kotlin, &declaration, ')')),
expected,
"{} takes {expected} arguments in Kotlin",
op.name
);
let shim = format!("pub extern \"C\" fn {}(", symbol(op));
assert_eq!(
count(&signature_after(&rust, &shim, ')')),
expected,
"{} takes {expected} arguments in the shim",
op.name
);
}
}
#[test]
fn pr6_only_primitives_cross_the_forwarder() {
let kotlin = generate_web_bridge_kotlin();
for op in BOUNDARY_SCHEMA {
let declaration = format!("external fun host{}(", op.name);
let signature = signature_after(&kotlin, &declaration, ')');
for argument in signature.split(',').filter(|part| !part.trim().is_empty()) {
assert!(
argument.trim().ends_with(": Int"),
"{} takes {argument}, which is not a 32-bit integer",
op.name
);
}
assert!(
kotlin.contains(&format!("{declaration}{signature}): Int")),
"{} has to answer with a status",
op.name
);
}
}
#[test]
fn pr6_the_forwarder_only_passes_its_arguments_on() {
let kotlin = generate_web_bridge_kotlin();
for op in BOUNDARY_SCHEMA {
let declaration = format!("external fun host{}(", op.name);
let parameters: Vec<String> = signature_after(&kotlin, &declaration, ')')
.split(',')
.filter(|part| !part.trim().is_empty())
.map(|part| {
part.trim()
.split(':')
.next()
.expect("a declared parameter has a name")
.to_owned()
})
.collect();
let names = parameters.join(", ");
let expected = format!(
"@JsFun(\"({names}) => globalThis.__dioxusComposeHost.{}({names})\")",
symbol(op)
);
assert!(
kotlin.contains(&expected),
"the forwarder for {} is not the fixed shape; expected\n{expected}",
op.name
);
}
for forbidden in ["await", "Promise", "new Uint8Array", "JSON", "BigInt"] {
assert!(
!kotlin.contains(forbidden),
"the Kotlin bridge mentions {forbidden}, which no forwarder may do"
);
}
}
#[test]
fn pr6_the_frame_request_binds_to_the_wasm_export() {
let kotlin = generate_web_bridge_kotlin();
let binding = signature_after(&kotlin, "dioxus_compose_renderer_request_frame:", ',');
assert!(
binding.contains("wasmExports.dioxus_compose_renderer_request_frame"),
"the frame request has to be bound to the Renderer's export, got: {binding}"
);
assert!(
!binding.contains("=>"),
"the frame request is wrapped in a closure, which puts a JavaScript frame on it: {binding}"
);
assert!(
kotlin.contains("@WasmExport(\"dioxus_compose_renderer_request_frame\")"),
"the Renderer has to export the function that import binds to"
);
}
#[test]
fn pr6_the_arena_is_never_copied() {
let rust = generate_wasm_rust();
for forbidden in ["copy_from_slice", "to_vec", "copy_nonoverlapping"] {
assert!(
!rust.contains(forbidden),
"the wasm shims call {forbidden}, and a batch is read in place"
);
}
let kotlin = generate_web_bridge_kotlin();
let loader = generate_web_loader_js();
for forbidden in ["Uint8Array", "DataView", "memory.buffer.slice"] {
assert!(
!kotlin.contains(forbidden) && !loader.contains(forbidden),
"the wiring reaches into the shared memory through {forbidden}; only the two \
modules read it"
);
}
assert!(
kotlin.contains("env: { memory }"),
"the Host has to import the memory the Renderer defined, not make one of its own"
);
}
#[test]
fn pr6_the_two_regions_are_stated_the_same_on_both_sides() {
let rust = generate_wasm_rust();
let kotlin = generate_web_bridge_kotlin();
let loader = generate_web_loader_js();
assert!(
kotlin.contains(&format!(
"const val RUST_REGION_BASE: Int = {WEB_RUST_REGION_BASE}"
)),
"the Renderer has to know where the Host's region starts"
);
assert!(
kotlin.contains(&format!("if (block < {WEB_RUST_REGION_BASE}) {{")),
"and refuse a Host whose block landed below it"
);
assert!(
rust.contains("fn lent(address: u32) -> bool {"),
"the shims have to refuse an address outside the Host's region"
);
assert!(
rust.contains("address >= WEB_RUST_REGION_BASE"),
"and refuse it against the same constant"
);
for field in WEB_BATCH_FIELDS {
let mut screaming = String::new();
for (index, character) in field.name.char_indices() {
if character.is_ascii_uppercase() && index != 0 {
screaming.push('_');
}
screaming.push(character.to_ascii_uppercase());
}
assert!(
kotlin.contains(&format!(
"const val BATCH_{screaming}_OFFSET: Int = {}",
field.offset
)),
"the Renderer reads {} at {}",
field.name,
field.offset
);
}
assert!(kotlin.contains(&format!("const val BATCH_BYTES: Int = {WEB_BATCH_BYTES}")));
assert!(kotlin.contains(&format!(
"const val EVENT_BUFFER_OFFSET: Int = {WEB_EVENT_BUFFER_OFFSET}"
)));
assert!(kotlin.contains(&format!(
"const val EVENT_BUFFER_BYTES: Int = {WEB_EVENT_BUFFER_BYTES}"
)));
assert!(rust.contains("assert!(size_of::<MutationBatch>() == WEB_BATCH_BYTES as usize);"));
assert!(rust.contains("offset_of!(BoundaryBlock, event) == WEB_EVENT_BUFFER_OFFSET as usize"));
}
#[test]
fn pr6_the_page_grows_the_memory_to_what_the_host_declares() {
let kotlin = generate_web_bridge_kotlin();
let loader = generate_web_loader_js();
assert!(
kotlin.contains(&format!("memory.grow({WEB_MEMORY_MIN_PAGES} - pages)")),
"the memory is grown to the minimum the Host's link declared, by the difference"
);
assert!(
loader.contains(&format!("const HOST_WASM = './{WEB_HOST_WASM_NAME}';")),
"the loader fetches the Host from beside itself"
);
assert!(
u64::from(WEB_MEMORY_MIN_PAGES) * 65536 > u64::from(WEB_RUST_REGION_BASE),
"{WEB_MEMORY_MIN_PAGES} pages do not reach the Host's region at {WEB_RUST_REGION_BASE}"
);
}
#[test]
fn pr6_the_page_answers_the_imports_the_host_carries() {
let kotlin = generate_web_bridge_kotlin();
for namespace in ["__wbindgen_placeholder__", "__wbindgen_externref_xform__"] {
assert!(
kotlin.contains(&format!("{namespace}: unbound('{namespace}')")),
"the instantiation has to answer {namespace} or the Host will not start"
);
}
assert!(
kotlin.contains("throw new Error("),
"and reaching one of them has to be reported rather than ignored"
);
}
#[test]
fn nfr5_a_page_without_a_host_says_so_and_carries_on() {
let kotlin = generate_web_bridge_kotlin();
let loader = generate_web_loader_js();
assert!(
kotlin.contains("if (!compiled) return 0;"),
"a missing Host has to answer zero rather than throw"
);
assert!(
loader.contains("console.info("),
"and say on the console why the screen is the development host"
);
}
fn signature_after(source: &str, opening: &str, closing: char) -> String {
let start = source
.find(opening)
.unwrap_or_else(|| panic!("no `{opening}` in the generated source"))
+ opening.len();
let rest = &source[start..];
let end = rest.find(closing).expect("unterminated argument list");
rest[..end].to_owned()
}
fn count(signature: &str) -> usize {
signature
.split(',')
.filter(|argument| !argument.trim().is_empty())
.count()
}