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