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;
31
32pub struct CallDefRow {
38 pub name: String,
39 pub num_args: i32,
40}
41
42pub struct AltrepRegRow {
44 pub symbol: String,
45}
46
47pub struct TraitDispatchRow {
49 pub concrete_tag: mx_tag,
50 pub trait_tag: mx_tag,
51 pub vtable_symbol: String,
52}
53
54pub fn format_wasm_registry(
71 call_defs: &[CallDefRow],
72 altrep_regs: &[AltrepRegRow],
73 trait_dispatches: &[TraitDispatchRow],
74) -> String {
75 let body = format_body(call_defs, altrep_regs, trait_dispatches);
76 let content_hash = fnv1a_64(body.as_bytes());
77
78 let mut out = String::new();
79 writeln!(&mut out, "// AUTO-GENERATED — DO NOT EDIT.").unwrap();
80 writeln!(&mut out, "//").unwrap();
81 writeln!(
82 &mut out,
83 "// Produced on host by `miniextendr_write_wasm_registry`. Compiled on"
84 )
85 .unwrap();
86 writeln!(
87 &mut out,
88 "// wasm32-* targets in place of the linkme distributed_slices."
89 )
90 .unwrap();
91 writeln!(&mut out, "//").unwrap();
92 writeln!(&mut out, "// generator-version: {GENERATOR_VERSION}").unwrap();
93 writeln!(&mut out, "// content-hash: {content_hash:016x}").unwrap();
94 writeln!(&mut out).unwrap();
95 out.push_str(&body);
96 out
97}
98
99fn format_body(
100 call_defs: &[CallDefRow],
101 altrep_regs: &[AltrepRegRow],
102 trait_dispatches: &[TraitDispatchRow],
103) -> String {
104 let mut out = String::new();
105
106 writeln!(&mut out, "use ::miniextendr_api::abi::mx_tag;").unwrap();
107 writeln!(&mut out, "use ::miniextendr_api::SEXP;").unwrap();
108 writeln!(&mut out, "use ::miniextendr_api::sys::R_CallMethodDef;").unwrap();
109 writeln!(
110 &mut out,
111 "use ::miniextendr_api::registry::{{AltrepRegistration, TraitDispatchEntry}};"
112 )
113 .unwrap();
114 writeln!(&mut out, "use ::core::ffi::c_void;").unwrap();
115 writeln!(&mut out).unwrap();
116
117 format_extern_unwind_block(&mut out, call_defs);
118 format_extern_c_block(&mut out, altrep_regs, trait_dispatches);
119 format_call_defs_slice(&mut out, call_defs);
120 format_altrep_regs_slice(&mut out, altrep_regs);
121 format_trait_dispatch_slice(&mut out, trait_dispatches);
122
123 out
124}
125
126fn format_extern_unwind_block(out: &mut String, call_defs: &[CallDefRow]) {
127 writeln!(out, "unsafe extern \"C-unwind\" {{").unwrap();
128 for row in call_defs {
129 let params = sexp_param_list(row.num_args);
130 writeln!(out, " pub fn {}({params}) -> SEXP;", row.name).unwrap();
131 }
132 writeln!(out, "}}").unwrap();
133 writeln!(out).unwrap();
134}
135
136fn format_extern_c_block(
137 out: &mut String,
138 altrep_regs: &[AltrepRegRow],
139 trait_dispatches: &[TraitDispatchRow],
140) {
141 writeln!(out, "unsafe extern \"C\" {{").unwrap();
142 for row in altrep_regs {
148 writeln!(out, " pub safe fn {}();", row.symbol).unwrap();
149 }
150 for row in trait_dispatches {
154 writeln!(out, " pub static {}: u8;", row.vtable_symbol).unwrap();
155 }
156 writeln!(out, "}}").unwrap();
157 writeln!(out).unwrap();
158}
159
160fn format_call_defs_slice(out: &mut String, call_defs: &[CallDefRow]) {
161 writeln!(out, "pub static MX_CALL_DEFS_WASM: &[R_CallMethodDef] = &[").unwrap();
162 for row in call_defs {
163 let arg_types = sexp_type_list(row.num_args);
168 writeln!(out, " R_CallMethodDef {{").unwrap();
169 writeln!(out, " name: c\"{}\".as_ptr(),", row.name).unwrap();
170 writeln!(
171 out,
172 " fun: Some(unsafe {{ ::core::mem::transmute::<unsafe extern \"C-unwind\" fn({arg_types}) -> SEXP, _>({}) }}),",
173 row.name
174 )
175 .unwrap();
176 writeln!(out, " numArgs: {},", row.num_args).unwrap();
177 writeln!(out, " }},").unwrap();
178 }
179 writeln!(out, "];").unwrap();
180 writeln!(out).unwrap();
181}
182
183fn format_altrep_regs_slice(out: &mut String, altrep_regs: &[AltrepRegRow]) {
184 writeln!(
185 out,
186 "pub static MX_ALTREP_REGISTRATIONS_WASM: &[AltrepRegistration] = &["
187 )
188 .unwrap();
189 for row in altrep_regs {
190 writeln!(out, " AltrepRegistration {{").unwrap();
191 writeln!(out, " register: {},", row.symbol).unwrap();
192 writeln!(out, " symbol: {:?},", row.symbol).unwrap();
193 writeln!(out, " }},").unwrap();
194 }
195 writeln!(out, "];").unwrap();
196 writeln!(out).unwrap();
197}
198
199fn format_trait_dispatch_slice(out: &mut String, trait_dispatches: &[TraitDispatchRow]) {
200 writeln!(
201 out,
202 "pub static MX_TRAIT_DISPATCH_WASM: &[TraitDispatchEntry] = &["
203 )
204 .unwrap();
205 for row in trait_dispatches {
206 writeln!(out, " TraitDispatchEntry {{").unwrap();
207 writeln!(
208 out,
209 " concrete_tag: mx_tag::new(0x{:016x}, 0x{:016x}),",
210 row.concrete_tag.lo, row.concrete_tag.hi
211 )
212 .unwrap();
213 writeln!(
214 out,
215 " trait_tag: mx_tag::new(0x{:016x}, 0x{:016x}),",
216 row.trait_tag.lo, row.trait_tag.hi
217 )
218 .unwrap();
219 writeln!(
220 out,
221 " vtable: unsafe {{ ::core::ptr::from_ref(&{}).cast::<c_void>() }},",
222 row.vtable_symbol
223 )
224 .unwrap();
225 writeln!(out, " vtable_symbol: {:?},", row.vtable_symbol).unwrap();
226 writeln!(out, " }},").unwrap();
227 }
228 writeln!(out, "];").unwrap();
229}
230
231fn sexp_param_list(num_args: i32) -> String {
236 if num_args <= 0 {
237 return String::new();
238 }
239 std::iter::repeat_n("_: SEXP", num_args as usize)
240 .collect::<Vec<_>>()
241 .join(", ")
242}
243
244fn sexp_type_list(num_args: i32) -> String {
248 if num_args <= 0 {
249 return String::new();
250 }
251 std::iter::repeat_n("SEXP", num_args as usize)
252 .collect::<Vec<_>>()
253 .join(", ")
254}
255
256fn fnv1a_64(data: &[u8]) -> u64 {
260 const OFFSET_BASIS: u64 = 0xcbf29ce484222325;
261 const PRIME: u64 = 0x00000100000001b3;
262 let mut h = OFFSET_BASIS;
263 for &b in data {
264 h ^= b as u64;
265 h = h.wrapping_mul(PRIME);
266 }
267 h
268}
269
270fn read_runtime_slices() -> (Vec<CallDefRow>, Vec<AltrepRegRow>, Vec<TraitDispatchRow>) {
273 let call_defs: Vec<CallDefRow> = MX_CALL_DEFS
274 .iter()
275 .map(|d| {
276 let name = unsafe { CStr::from_ptr(d.name) }
281 .to_str()
282 .expect("MX_CALL_DEFS.name is not valid UTF-8")
283 .to_string();
284 CallDefRow {
285 name,
286 num_args: d.numArgs,
287 }
288 })
289 .collect();
290
291 let altrep_regs: Vec<AltrepRegRow> = MX_ALTREP_REGISTRATIONS
292 .iter()
293 .map(|r: &AltrepRegistration| AltrepRegRow {
294 symbol: r.symbol.to_string(),
295 })
296 .collect();
297
298 let trait_dispatches: Vec<TraitDispatchRow> = MX_TRAIT_DISPATCH
299 .iter()
300 .map(|t: &TraitDispatchEntry| TraitDispatchRow {
301 concrete_tag: t.concrete_tag,
302 trait_tag: t.trait_tag,
303 vtable_symbol: t.vtable_symbol.to_string(),
304 })
305 .collect();
306
307 (call_defs, altrep_regs, trait_dispatches)
308}
309
310pub fn write_wasm_registry_to_file(path: &str) {
313 let (call_defs, altrep_regs, trait_dispatches) = read_runtime_slices();
314 let content = format_wasm_registry(&call_defs, &altrep_regs, &trait_dispatches);
315
316 let existing = std::fs::read_to_string(path).unwrap_or_default();
317 if existing == content {
318 return;
319 }
320
321 let dest = std::path::Path::new(path);
327 let tmp = dest.with_extension("tmp");
328 std::fs::write(&tmp, content.as_bytes())
329 .unwrap_or_else(|e| panic!("failed to write {}: {e}", tmp.display()));
330 #[cfg(windows)]
331 let _ = std::fs::remove_file(dest);
332 std::fs::rename(&tmp, dest).unwrap_or_else(|e| {
333 panic!(
334 "failed to rename {} → {}: {e}",
335 tmp.display(),
336 dest.display()
337 )
338 });
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344
345 fn sample_inputs() -> (Vec<CallDefRow>, Vec<AltrepRegRow>, Vec<TraitDispatchRow>) {
346 let call_defs = vec![
347 CallDefRow {
348 name: "miniextendr_my_fn".into(),
349 num_args: 0,
350 },
351 CallDefRow {
352 name: "miniextendr_other".into(),
353 num_args: 3,
354 },
355 ];
356 let altrep_regs = vec![AltrepRegRow {
357 symbol: "__mx_altrep_reg_MyType".into(),
358 }];
359 let trait_dispatches = vec![TraitDispatchRow {
360 concrete_tag: mx_tag::new(0xdead_beef_dead_beef, 0x1234_5678_1234_5678),
361 trait_tag: mx_tag::new(0xcafe_babe_cafe_babe, 0xfeed_face_feed_face),
362 vtable_symbol: "__VTABLE_COUNTER_FOR_MYTYPE".into(),
363 }];
364 (call_defs, altrep_regs, trait_dispatches)
365 }
366
367 #[test]
368 fn header_carries_generator_version_and_content_hash() {
369 let (a, b, c) = sample_inputs();
370 let out = format_wasm_registry(&a, &b, &c);
371 assert!(
372 out.contains(&format!("generator-version: {GENERATOR_VERSION}")),
373 "expected generator-version line; got:\n{out}"
374 );
375 assert!(
376 out.contains("content-hash: "),
377 "expected content-hash line; got:\n{out}"
378 );
379 }
380
381 #[test]
382 fn content_hash_is_deterministic() {
383 let (a, b, c) = sample_inputs();
384 let first = format_wasm_registry(&a, &b, &c);
385 let second = format_wasm_registry(&a, &b, &c);
386 assert_eq!(first, second);
387 }
388
389 #[test]
390 fn content_hash_changes_when_body_changes() {
391 let (a, b, c) = sample_inputs();
392 let mut a2 = a.clone_into_unique();
393 a2.push(CallDefRow {
394 name: "different_fn".into(),
395 num_args: 1,
396 });
397 let first = format_wasm_registry(&a, &b, &c);
398 let second = format_wasm_registry(&a2, &b, &c);
399 assert_ne!(first, second);
400 }
401
402 #[test]
403 fn emits_extern_decls_for_every_referenced_symbol() {
404 let (a, b, c) = sample_inputs();
405 let out = format_wasm_registry(&a, &b, &c);
406 assert!(out.contains("pub fn miniextendr_my_fn() -> SEXP;"));
409 assert!(out.contains("pub fn miniextendr_other(_: SEXP, _: SEXP, _: SEXP) -> SEXP;"));
410 assert!(out.contains("pub safe fn __mx_altrep_reg_MyType();"));
412 assert!(out.contains("pub static __VTABLE_COUNTER_FOR_MYTYPE: u8;"));
413 }
414
415 #[test]
416 fn emits_named_slice_constants() {
417 let (a, b, c) = sample_inputs();
418 let out = format_wasm_registry(&a, &b, &c);
419 assert!(out.contains("pub static MX_CALL_DEFS_WASM: &[R_CallMethodDef]"));
420 assert!(out.contains("pub static MX_ALTREP_REGISTRATIONS_WASM: &[AltrepRegistration]"));
421 assert!(out.contains("pub static MX_TRAIT_DISPATCH_WASM: &[TraitDispatchEntry]"));
422 }
423
424 #[test]
425 fn renders_mx_tag_with_const_constructor() {
426 let (a, b, c) = sample_inputs();
427 let out = format_wasm_registry(&a, &b, &c);
428 assert!(
429 out.contains("mx_tag::new(0xdeadbeefdeadbeef, 0x1234567812345678)"),
430 "expected concrete_tag literal; got:\n{out}"
431 );
432 assert!(
433 out.contains("mx_tag::new(0xcafebabecafebabe, 0xfeedfacefeedface)"),
434 "expected trait_tag literal; got:\n{out}"
435 );
436 }
437
438 #[test]
439 fn empty_inputs_produce_empty_slices() {
440 let out = format_wasm_registry(&[], &[], &[]);
441 assert!(out.contains("pub static MX_CALL_DEFS_WASM: &[R_CallMethodDef] = &[\n];"));
442 assert!(
443 out.contains("pub static MX_ALTREP_REGISTRATIONS_WASM: &[AltrepRegistration] = &[\n];")
444 );
445 assert!(out.contains("pub static MX_TRAIT_DISPATCH_WASM: &[TraitDispatchEntry] = &[\n];"));
446 }
447
448 #[test]
449 fn param_list_uses_underscore_bindings() {
450 assert_eq!(sexp_param_list(0), "");
451 assert_eq!(sexp_param_list(1), "_: SEXP");
452 assert_eq!(sexp_param_list(3), "_: SEXP, _: SEXP, _: SEXP");
453 }
454
455 #[test]
456 fn type_list_is_bare_types() {
457 assert_eq!(sexp_type_list(0), "");
458 assert_eq!(sexp_type_list(1), "SEXP");
459 assert_eq!(sexp_type_list(3), "SEXP, SEXP, SEXP");
460 }
461
462 #[test]
463 fn altrep_register_decls_use_safe_keyword() {
464 let (a, b, c) = sample_inputs();
465 let out = format_wasm_registry(&a, &b, &c);
466 assert!(
467 out.contains("pub safe fn __mx_altrep_reg_MyType()"),
468 "expected `safe fn` so the fn type matches AltrepRegistration.register; got:\n{out}"
469 );
470 }
471
472 trait CloneIntoUnique {
476 fn clone_into_unique(&self) -> Vec<CallDefRow>;
477 }
478 impl CloneIntoUnique for Vec<CallDefRow> {
479 fn clone_into_unique(&self) -> Vec<CallDefRow> {
480 self.iter()
481 .map(|r| CallDefRow {
482 name: r.name.clone(),
483 num_args: r.num_args,
484 })
485 .collect()
486 }
487 }
488}