1use crate::abi::mx_tag;
20use crate::registry::{
21 AltrepRegistration, MX_ALTREP_REGISTRATIONS, MX_CALL_DEFS, MX_TRAIT_DISPATCH,
22 TraitDispatchEntry,
23};
24use std::ffi::CStr;
25use std::fmt::Write as _;
26
27const GENERATOR_VERSION: u32 = 1;
34
35pub struct CallDefRow {
41 pub name: String,
42 pub num_args: i32,
43}
44
45pub struct AltrepRegRow {
47 pub symbol: String,
48}
49
50pub struct TraitDispatchRow {
52 pub concrete_tag: mx_tag,
53 pub trait_tag: mx_tag,
54 pub vtable_symbol: String,
55}
56
57pub fn format_wasm_registry(
74 call_defs: &[CallDefRow],
75 altrep_regs: &[AltrepRegRow],
76 trait_dispatches: &[TraitDispatchRow],
77) -> String {
78 let body = format_body(call_defs, altrep_regs, trait_dispatches);
79 let content_hash = fnv1a_64(body.as_bytes());
80
81 let mut out = String::new();
82 writeln!(&mut out, "// AUTO-GENERATED — DO NOT EDIT.").unwrap();
83 writeln!(&mut out, "//").unwrap();
84 writeln!(
85 &mut out,
86 "// Produced on host by `miniextendr_write_wasm_registry`. Compiled on"
87 )
88 .unwrap();
89 writeln!(
90 &mut out,
91 "// wasm32-* targets in place of the linkme distributed_slices."
92 )
93 .unwrap();
94 writeln!(&mut out, "//").unwrap();
95 writeln!(&mut out, "// generator-version: {GENERATOR_VERSION}").unwrap();
96 writeln!(&mut out, "// content-hash: {content_hash:016x}").unwrap();
97 writeln!(&mut out).unwrap();
98 out.push_str(&body);
99 out
100}
101
102fn format_body(
103 call_defs: &[CallDefRow],
104 altrep_regs: &[AltrepRegRow],
105 trait_dispatches: &[TraitDispatchRow],
106) -> String {
107 let mut out = String::new();
108
109 writeln!(&mut out, "use ::miniextendr_api::SEXP;").unwrap();
114 writeln!(&mut out, "use ::miniextendr_api::sys::R_CallMethodDef;").unwrap();
115 writeln!(
116 &mut out,
117 "use ::miniextendr_api::registry::{{AltrepRegistration, TraitDispatchEntry}};"
118 )
119 .unwrap();
120 writeln!(&mut out).unwrap();
121
122 format_extern_unwind_block(&mut out, call_defs);
123 format_extern_c_block(&mut out, altrep_regs, trait_dispatches);
124 format_call_defs_slice(&mut out, call_defs);
125 format_altrep_regs_slice(&mut out, altrep_regs);
126 format_trait_dispatch_slice(&mut out, trait_dispatches);
127
128 out
129}
130
131fn format_extern_unwind_block(out: &mut String, call_defs: &[CallDefRow]) {
132 writeln!(out, "unsafe extern \"C-unwind\" {{").unwrap();
133 for row in call_defs {
134 let params = sexp_param_list(row.num_args);
135 writeln!(out, " pub fn {}({params}) -> SEXP;", row.name).unwrap();
136 }
137 writeln!(out, "}}").unwrap();
138 writeln!(out).unwrap();
139}
140
141fn format_extern_c_block(
142 out: &mut String,
143 altrep_regs: &[AltrepRegRow],
144 trait_dispatches: &[TraitDispatchRow],
145) {
146 writeln!(out, "unsafe extern \"C\" {{").unwrap();
147 for row in altrep_regs {
153 writeln!(out, " pub safe fn {}();", row.symbol).unwrap();
154 }
155 for row in trait_dispatches {
159 writeln!(out, " pub static {}: u8;", row.vtable_symbol).unwrap();
160 }
161 writeln!(out, "}}").unwrap();
162 writeln!(out).unwrap();
163}
164
165fn format_call_defs_slice(out: &mut String, call_defs: &[CallDefRow]) {
166 writeln!(out, "pub static MX_CALL_DEFS_WASM: &[R_CallMethodDef] = &[").unwrap();
167 for row in call_defs {
168 let arg_types = sexp_type_list(row.num_args);
173 writeln!(out, " R_CallMethodDef {{").unwrap();
174 writeln!(out, " name: c\"{}\".as_ptr(),", row.name).unwrap();
175 writeln!(
176 out,
177 " fun: Some(unsafe {{ ::core::mem::transmute::<unsafe extern \"C-unwind\" fn({arg_types}) -> SEXP, _>({}) }}),",
178 row.name
179 )
180 .unwrap();
181 writeln!(out, " numArgs: {},", row.num_args).unwrap();
182 writeln!(out, " }},").unwrap();
183 }
184 writeln!(out, "];").unwrap();
185 writeln!(out).unwrap();
186}
187
188fn format_altrep_regs_slice(out: &mut String, altrep_regs: &[AltrepRegRow]) {
189 writeln!(
190 out,
191 "pub static MX_ALTREP_REGISTRATIONS_WASM: &[AltrepRegistration] = &["
192 )
193 .unwrap();
194 for row in altrep_regs {
195 writeln!(out, " AltrepRegistration {{").unwrap();
196 writeln!(out, " register: {},", row.symbol).unwrap();
197 writeln!(out, " symbol: {:?},", row.symbol).unwrap();
198 writeln!(out, " }},").unwrap();
199 }
200 writeln!(out, "];").unwrap();
201 writeln!(out).unwrap();
202}
203
204fn format_trait_dispatch_slice(out: &mut String, trait_dispatches: &[TraitDispatchRow]) {
205 writeln!(
206 out,
207 "pub static MX_TRAIT_DISPATCH_WASM: &[TraitDispatchEntry] = &["
208 )
209 .unwrap();
210 for row in trait_dispatches {
211 writeln!(out, " TraitDispatchEntry {{").unwrap();
212 writeln!(
215 out,
216 " concrete_tag: ::miniextendr_api::abi::mx_tag::new(0x{:016x}, 0x{:016x}),",
217 row.concrete_tag.lo, row.concrete_tag.hi
218 )
219 .unwrap();
220 writeln!(
221 out,
222 " trait_tag: ::miniextendr_api::abi::mx_tag::new(0x{:016x}, 0x{:016x}),",
223 row.trait_tag.lo, row.trait_tag.hi
224 )
225 .unwrap();
226 writeln!(
227 out,
228 " vtable: unsafe {{ ::core::ptr::from_ref(&{}).cast::<::core::ffi::c_void>() }},",
229 row.vtable_symbol
230 )
231 .unwrap();
232 writeln!(out, " vtable_symbol: {:?},", row.vtable_symbol).unwrap();
233 writeln!(out, " }},").unwrap();
234 }
235 writeln!(out, "];").unwrap();
236}
237
238fn sexp_param_list(num_args: i32) -> String {
243 if num_args <= 0 {
244 return String::new();
245 }
246 std::iter::repeat_n("_: SEXP", num_args as usize)
247 .collect::<Vec<_>>()
248 .join(", ")
249}
250
251fn sexp_type_list(num_args: i32) -> String {
255 if num_args <= 0 {
256 return String::new();
257 }
258 std::iter::repeat_n("SEXP", num_args as usize)
259 .collect::<Vec<_>>()
260 .join(", ")
261}
262
263fn fnv1a_64(data: &[u8]) -> u64 {
267 const OFFSET_BASIS: u64 = 0xcbf29ce484222325;
268 const PRIME: u64 = 0x00000100000001b3;
269 let mut h = OFFSET_BASIS;
270 for &b in data {
271 h ^= b as u64;
272 h = h.wrapping_mul(PRIME);
273 }
274 h
275}
276
277fn read_runtime_slices() -> (Vec<CallDefRow>, Vec<AltrepRegRow>, Vec<TraitDispatchRow>) {
280 let call_defs: Vec<CallDefRow> = MX_CALL_DEFS
281 .iter()
282 .map(|d| {
283 let name = unsafe { CStr::from_ptr(d.name) }
288 .to_str()
289 .expect("MX_CALL_DEFS.name is not valid UTF-8")
290 .to_string();
291 CallDefRow {
292 name,
293 num_args: d.numArgs,
294 }
295 })
296 .collect();
297
298 let altrep_regs: Vec<AltrepRegRow> = MX_ALTREP_REGISTRATIONS
299 .iter()
300 .map(|r: &AltrepRegistration| AltrepRegRow {
301 symbol: r.symbol.to_string(),
302 })
303 .collect();
304
305 let trait_dispatches: Vec<TraitDispatchRow> = MX_TRAIT_DISPATCH
306 .iter()
307 .map(|t: &TraitDispatchEntry| TraitDispatchRow {
308 concrete_tag: t.concrete_tag,
309 trait_tag: t.trait_tag,
310 vtable_symbol: t.vtable_symbol.to_string(),
311 })
312 .collect();
313
314 (call_defs, altrep_regs, trait_dispatches)
315}
316
317pub fn write_wasm_registry_to_file(path: &str) {
320 let (call_defs, altrep_regs, trait_dispatches) = read_runtime_slices();
321 let content = format_wasm_registry(&call_defs, &altrep_regs, &trait_dispatches);
322
323 let existing = std::fs::read_to_string(path).unwrap_or_default();
324 if existing == content {
325 return;
326 }
327
328 let dest = std::path::Path::new(path);
334 let tmp = dest.with_extension("tmp");
335 std::fs::write(&tmp, content.as_bytes())
336 .unwrap_or_else(|e| panic!("failed to write {}: {e}", tmp.display()));
337 #[cfg(windows)]
338 let _ = std::fs::remove_file(dest);
339 std::fs::rename(&tmp, dest).unwrap_or_else(|e| {
340 panic!(
341 "failed to rename {} → {}: {e}",
342 tmp.display(),
343 dest.display()
344 )
345 });
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 fn sample_inputs() -> (Vec<CallDefRow>, Vec<AltrepRegRow>, Vec<TraitDispatchRow>) {
353 let call_defs = vec![
354 CallDefRow {
355 name: "miniextendr_my_fn".into(),
356 num_args: 0,
357 },
358 CallDefRow {
359 name: "miniextendr_other".into(),
360 num_args: 3,
361 },
362 ];
363 let altrep_regs = vec![AltrepRegRow {
364 symbol: "__mx_altrep_reg_MyType".into(),
365 }];
366 let trait_dispatches = vec![TraitDispatchRow {
367 concrete_tag: mx_tag::new(0xdead_beef_dead_beef, 0x1234_5678_1234_5678),
368 trait_tag: mx_tag::new(0xcafe_babe_cafe_babe, 0xfeed_face_feed_face),
369 vtable_symbol: "__VTABLE_COUNTER_FOR_MYTYPE".into(),
370 }];
371 (call_defs, altrep_regs, trait_dispatches)
372 }
373
374 #[test]
375 fn header_carries_generator_version_and_content_hash() {
376 let (a, b, c) = sample_inputs();
377 let out = format_wasm_registry(&a, &b, &c);
378 assert!(
379 out.contains(&format!("generator-version: {GENERATOR_VERSION}")),
380 "expected generator-version line; got:\n{out}"
381 );
382 assert!(
383 out.contains("content-hash: "),
384 "expected content-hash line; got:\n{out}"
385 );
386 }
387
388 #[test]
389 fn content_hash_is_deterministic() {
390 let (a, b, c) = sample_inputs();
391 let first = format_wasm_registry(&a, &b, &c);
392 let second = format_wasm_registry(&a, &b, &c);
393 assert_eq!(first, second);
394 }
395
396 #[test]
397 fn content_hash_changes_when_body_changes() {
398 let (a, b, c) = sample_inputs();
399 let mut a2 = a.clone_into_unique();
400 a2.push(CallDefRow {
401 name: "different_fn".into(),
402 num_args: 1,
403 });
404 let first = format_wasm_registry(&a, &b, &c);
405 let second = format_wasm_registry(&a2, &b, &c);
406 assert_ne!(first, second);
407 }
408
409 #[test]
410 fn emits_extern_decls_for_every_referenced_symbol() {
411 let (a, b, c) = sample_inputs();
412 let out = format_wasm_registry(&a, &b, &c);
413 assert!(out.contains("pub fn miniextendr_my_fn() -> SEXP;"));
416 assert!(out.contains("pub fn miniextendr_other(_: SEXP, _: SEXP, _: SEXP) -> SEXP;"));
417 assert!(out.contains("pub safe fn __mx_altrep_reg_MyType();"));
419 assert!(out.contains("pub static __VTABLE_COUNTER_FOR_MYTYPE: u8;"));
420 }
421
422 #[test]
423 fn emits_named_slice_constants() {
424 let (a, b, c) = sample_inputs();
425 let out = format_wasm_registry(&a, &b, &c);
426 assert!(out.contains("pub static MX_CALL_DEFS_WASM: &[R_CallMethodDef]"));
427 assert!(out.contains("pub static MX_ALTREP_REGISTRATIONS_WASM: &[AltrepRegistration]"));
428 assert!(out.contains("pub static MX_TRAIT_DISPATCH_WASM: &[TraitDispatchEntry]"));
429 }
430
431 #[test]
432 fn renders_mx_tag_with_const_constructor() {
433 let (a, b, c) = sample_inputs();
434 let out = format_wasm_registry(&a, &b, &c);
435 assert!(
436 out.contains(
437 "::miniextendr_api::abi::mx_tag::new(0xdeadbeefdeadbeef, 0x1234567812345678)"
438 ),
439 "expected fully-qualified concrete_tag literal; got:\n{out}"
440 );
441 assert!(
442 out.contains(
443 "::miniextendr_api::abi::mx_tag::new(0xcafebabecafebabe, 0xfeedfacefeedface)"
444 ),
445 "expected fully-qualified trait_tag literal; got:\n{out}"
446 );
447 }
448
449 #[test]
454 fn dispatch_free_output_has_no_dispatch_only_imports() {
455 let (call_defs, _, _) = sample_inputs();
456 let out = format_wasm_registry(&call_defs, &[], &[]);
457 assert!(
458 !out.contains("mx_tag"),
459 "dispatch-free output must not mention mx_tag; got:\n{out}"
460 );
461 assert!(
462 !out.contains("c_void"),
463 "dispatch-free output must not mention c_void; got:\n{out}"
464 );
465 }
466
467 #[test]
472 fn every_import_is_used_in_dispatch_free_output() {
473 let (call_defs, _, _) = sample_inputs();
474 let out = format_wasm_registry(&call_defs, &[], &[]);
475 let (imports, rest): (Vec<&str>, Vec<&str>) =
476 out.lines().partition(|l| l.starts_with("use "));
477 assert!(!imports.is_empty(), "output has imports; got:\n{out}");
478 let body = rest.join("\n");
479 for line in imports {
480 let idents = line
481 .trim_start_matches("use ")
482 .trim_end_matches(';')
483 .rsplit("::")
484 .next()
485 .unwrap()
486 .trim_matches(['{', '}'])
487 .split(',')
488 .map(str::trim);
489 for ident in idents {
490 assert!(
491 body.contains(ident),
492 "import `{ident}` from `{line}` is unused; got:\n{out}"
493 );
494 }
495 }
496 }
497
498 #[test]
502 fn dispatch_entries_reference_items_fully_qualified() {
503 let (a, b, c) = sample_inputs();
504 let out = format_wasm_registry(&a, &b, &c);
505 assert!(
506 !out.contains("use ::miniextendr_api::abi::mx_tag;"),
507 "mx_tag header import must not be emitted; got:\n{out}"
508 );
509 assert!(
510 !out.contains("use ::core::ffi::c_void;"),
511 "c_void header import must not be emitted; got:\n{out}"
512 );
513 assert!(
514 out.contains("::miniextendr_api::abi::mx_tag::new("),
515 "dispatch tags must be fully qualified; got:\n{out}"
516 );
517 assert!(
518 out.contains(".cast::<::core::ffi::c_void>()"),
519 "vtable cast must be fully qualified; got:\n{out}"
520 );
521 }
522
523 #[test]
524 fn empty_inputs_produce_empty_slices() {
525 let out = format_wasm_registry(&[], &[], &[]);
526 assert!(out.contains("pub static MX_CALL_DEFS_WASM: &[R_CallMethodDef] = &[\n];"));
527 assert!(
528 out.contains("pub static MX_ALTREP_REGISTRATIONS_WASM: &[AltrepRegistration] = &[\n];")
529 );
530 assert!(out.contains("pub static MX_TRAIT_DISPATCH_WASM: &[TraitDispatchEntry] = &[\n];"));
531 }
532
533 #[test]
534 fn param_list_uses_underscore_bindings() {
535 assert_eq!(sexp_param_list(0), "");
536 assert_eq!(sexp_param_list(1), "_: SEXP");
537 assert_eq!(sexp_param_list(3), "_: SEXP, _: SEXP, _: SEXP");
538 }
539
540 #[test]
541 fn type_list_is_bare_types() {
542 assert_eq!(sexp_type_list(0), "");
543 assert_eq!(sexp_type_list(1), "SEXP");
544 assert_eq!(sexp_type_list(3), "SEXP, SEXP, SEXP");
545 }
546
547 #[test]
548 fn altrep_register_decls_use_safe_keyword() {
549 let (a, b, c) = sample_inputs();
550 let out = format_wasm_registry(&a, &b, &c);
551 assert!(
552 out.contains("pub safe fn __mx_altrep_reg_MyType()"),
553 "expected `safe fn` so the fn type matches AltrepRegistration.register; got:\n{out}"
554 );
555 }
556
557 trait CloneIntoUnique {
561 fn clone_into_unique(&self) -> Vec<CallDefRow>;
562 }
563 impl CloneIntoUnique for Vec<CallDefRow> {
564 fn clone_into_unique(&self) -> Vec<CallDefRow> {
565 self.iter()
566 .map(|r| CallDefRow {
567 name: r.name.clone(),
568 num_args: r.num_args,
569 })
570 .collect()
571 }
572 }
573}