1use anyhow::{Result, anyhow};
39use std::collections::HashSet;
40use std::path::Path;
41use tempfile::TempDir;
42use wasmtime::{
43 Config, Engine, Store,
44 component::{Component, Func, Instance, Linker, ResourceTable, Val},
45};
46use wasmtime_wasi::{FsPerms, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
47use wasmtime_wizer::{WasmtimeWizerComponent, Wizer};
48
49use crate::linker::{NativeExtension, link_with_extensions};
50
51struct PreInitCtx {
53 wasi: WasiCtx,
54 table: ResourceTable,
55 #[allow(dead_code)]
57 temp_dir: Option<TempDir>,
58}
59
60impl std::fmt::Debug for PreInitCtx {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.debug_struct("PreInitCtx").finish_non_exhaustive()
63 }
64}
65
66impl WasiView for PreInitCtx {
67 fn ctx(&mut self) -> WasiCtxView<'_> {
68 WasiCtxView {
69 ctx: &mut self.wasi,
70 table: &mut self.table,
71 }
72 }
73}
74
75pub async fn pre_initialize(
100 python_stdlib: &Path,
101 site_packages: Option<&Path>,
102 imports: &[&str],
103 extensions: &[NativeExtension],
104 setup_code: Option<&str>,
105) -> Result<Vec<u8>> {
106 let imports: Vec<String> = imports.iter().map(|s| (*s).to_string()).collect();
107
108 let original_component = link_with_extensions(extensions)
110 .map_err(|e| anyhow!("Failed to link component with extensions: {}", e))?;
111
112 let wizer = Wizer::new();
116 let (cx, instrumented_wasm) = wizer
117 .instrument_component(&original_component)
118 .map_err(|e| e.context("Failed to instrument component"))?;
119
120 let mut config = Config::new();
122 config.wasm_component_model(true);
123 config.wasm_component_model_async(true);
124
125 let engine = Engine::new(&config)?;
126 let component = Component::new(&engine, &instrumented_wasm)?;
127
128 let table = ResourceTable::new();
130
131 let mut python_path_parts = vec!["/python-stdlib".to_string()];
133 if site_packages.is_some() {
134 python_path_parts.push("/site-packages".to_string());
135 }
136 let python_path = python_path_parts.join(":");
137
138 let mut wasi_builder = WasiCtxBuilder::new();
139 wasi_builder
140 .env("PYTHONHOME", "/python-stdlib")
141 .env("PYTHONPATH", &python_path)
142 .env("PYTHONUNBUFFERED", "1");
143
144 if python_stdlib.exists() {
146 wasi_builder.preopened_dir(python_stdlib, "python-stdlib", FsPerms::ReadOnly)?;
147 } else {
148 return Err(anyhow!(
149 "Python stdlib not found at {}",
150 python_stdlib.display()
151 ));
152 }
153
154 let temp_dir = if let Some(site_pkg) = site_packages {
156 if site_pkg.exists() {
157 wasi_builder.preopened_dir(site_pkg, "site-packages", FsPerms::ReadOnly)?;
158 }
159 None
160 } else {
161 let temp = TempDir::new()?;
163 wasi_builder.preopened_dir(temp.path(), "site-packages", FsPerms::ReadOnly)?;
164 Some(temp)
165 };
166
167 let wasi = wasi_builder.build();
168
169 let mut store = Store::new(
170 &engine,
171 PreInitCtx {
172 wasi,
173 table,
174 temp_dir,
175 },
176 );
177
178 let mut linker = Linker::new(&engine);
180 wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
181
182 add_sandbox_stubs(&mut linker)?;
185
186 let instance = linker.instantiate_async(&mut store, &component).await?;
189
190 if !imports.is_empty() {
192 call_execute_for_imports(&mut store, &instance, &imports).await?;
193 }
194
195 if let Some(code) = setup_code {
198 call_execute_code(&mut store, &instance, code, "setup code").await?;
199 }
200
201 call_finalize_preinit(&mut store, &instance).await?;
206
207 let snapshot_bytes = wizer
209 .snapshot_component(
210 &cx,
211 &mut WasmtimeWizerComponent {
212 store: &mut store,
213 instance,
214 },
215 )
216 .await
217 .map_err(|e| e.context("Failed to pre-initialize component"))?;
218
219 restore_initialize_exports(&snapshot_bytes)
226}
227
228fn slice_range<'a>(bytes: &'a [u8], range: &std::ops::Range<u64>) -> Result<&'a [u8]> {
241 let start = usize::try_from(range.start)?;
242 let end = usize::try_from(range.end)?;
243 Ok(&bytes[start..end])
244}
245
246fn restore_initialize_exports(component_bytes: &[u8]) -> Result<Vec<u8>> {
247 let mut modules_with_init: HashSet<u32> = HashSet::new();
249 let mut any_module_imports_init = false;
250 let mut module_index = 0u32;
251
252 for payload in wasmparser::Parser::new(0).parse_all(component_bytes) {
253 if let wasmparser::Payload::ModuleSection {
254 unchecked_range: range,
255 ..
256 } = payload?
257 {
258 let module_bytes = slice_range(component_bytes, &range)?;
259 for inner in wasmparser::Parser::new(0).parse_all(module_bytes) {
261 match inner? {
262 wasmparser::Payload::ExportSection(reader) => {
263 for export in reader {
264 if export?.name == "_initialize" {
265 modules_with_init.insert(module_index);
266 }
267 }
268 }
269 wasmparser::Payload::ImportSection(reader) => {
270 for import in reader.into_imports() {
273 if import?.name == "_initialize" {
274 any_module_imports_init = true;
275 }
276 }
277 }
278 _ => {}
279 }
280 }
281 module_index += 1;
282 }
283 }
284
285 if !any_module_imports_init {
286 return Ok(component_bytes.to_vec());
287 }
288
289 let mut component = wasm_encoder::Component::new();
291 module_index = 0;
292 let mut depth = 0u32;
293
294 for payload in wasmparser::Parser::new(0).parse_all(component_bytes) {
295 let payload = payload?;
296
297 match &payload {
299 wasmparser::Payload::Version { .. } => {
300 if depth > 0 {
301 depth += 1;
303 continue;
304 }
305 depth += 1;
306 continue; }
308 wasmparser::Payload::End { .. } => {
309 depth -= 1;
310 continue; }
312 _ => {
313 if depth > 1 {
314 continue;
316 }
317 }
318 }
319
320 match payload {
321 wasmparser::Payload::ModuleSection {
322 unchecked_range: range,
323 ..
324 } => {
325 let module_bytes = slice_range(component_bytes, &range)?;
326
327 if !modules_with_init.contains(&module_index) {
328 let patched = add_noop_initialize(module_bytes)?;
329 component.section(&wasm_encoder::RawSection {
330 id: wasm_encoder::ComponentSectionId::CoreModule as u8,
331 data: &patched,
332 });
333 } else {
334 component.section(&wasm_encoder::RawSection {
335 id: wasm_encoder::ComponentSectionId::CoreModule as u8,
336 data: module_bytes,
337 });
338 }
339 module_index += 1;
340 }
341 other => {
342 if let Some((id, range)) = other.as_section() {
343 component.section(&wasm_encoder::RawSection {
344 id,
345 data: slice_range(component_bytes, &range)?,
346 });
347 }
348 }
349 }
350 }
351
352 Ok(component.finish())
353}
354
355fn add_noop_initialize(module_bytes: &[u8]) -> Result<Vec<u8>> {
361 use wasm_encoder::reencode::{Reencode, RoundtripReencoder};
362
363 let mut num_types = 0u32;
364 let mut num_imported_funcs = 0u32;
365 let mut num_defined_funcs = 0u32;
366 let mut noop_type_idx = None;
367
368 for payload in wasmparser::Parser::new(0).parse_all(module_bytes) {
370 match payload? {
371 wasmparser::Payload::TypeSection(reader) => {
372 for ty in reader.into_iter() {
373 let ty = ty?;
374 for sub in ty.types() {
375 if let wasmparser::CompositeInnerType::Func(func_ty) =
376 &sub.composite_type.inner
377 && func_ty.params().is_empty()
378 && func_ty.results().is_empty()
379 {
380 noop_type_idx = Some(num_types);
381 }
382 num_types += 1;
383 }
384 }
385 }
386 wasmparser::Payload::ImportSection(reader) => {
387 for import in reader.into_imports() {
391 if matches!(import?.ty, wasmparser::TypeRef::Func(_)) {
392 num_imported_funcs += 1;
393 }
394 }
395 }
396 wasmparser::Payload::FunctionSection(reader) => {
397 num_defined_funcs = reader.count();
398 }
399 wasmparser::Payload::CodeSectionStart { .. } => {}
400 _ => {}
401 }
402 }
403
404 let num_funcs = num_imported_funcs + num_defined_funcs;
405 let noop_type = noop_type_idx.unwrap_or(num_types);
406 let noop_func_index = num_funcs;
407 let needs_new_type = noop_type_idx.is_none();
408
409 let mut encoder = wasm_encoder::Module::new();
412 let mut reencode = RoundtripReencoder;
413
414 for payload in wasmparser::Parser::new(0).parse_all(module_bytes) {
415 match payload? {
416 wasmparser::Payload::Version { .. } => {}
417 wasmparser::Payload::TypeSection(reader) => {
418 let mut types = wasm_encoder::TypeSection::new();
419 reencode.parse_type_section(&mut types, reader)?;
420 if needs_new_type {
421 types.ty().function([], []);
422 }
423 encoder.section(&types);
424 }
425 wasmparser::Payload::FunctionSection(reader) => {
426 let mut funcs = wasm_encoder::FunctionSection::new();
427 reencode.parse_function_section(&mut funcs, reader)?;
428 funcs.function(noop_type);
429 encoder.section(&funcs);
430 }
431 wasmparser::Payload::ExportSection(reader) => {
432 let mut exports = wasm_encoder::ExportSection::new();
433 reencode.parse_export_section(&mut exports, reader)?;
434 exports.export(
435 "_initialize",
436 wasm_encoder::ExportKind::Func,
437 noop_func_index,
438 );
439 encoder.section(&exports);
440 }
441 wasmparser::Payload::CodeSectionStart { range, .. } => {
442 let section_data = slice_range(module_bytes, &range)?;
445 let code_reader = wasmparser::CodeSectionReader::new(
446 wasmparser::BinaryReader::new(section_data, 0),
447 )?;
448
449 let mut code = wasm_encoder::CodeSection::new();
450 reencode.parse_code_section(&mut code, code_reader)?;
451
452 let mut noop_func = wasm_encoder::Function::new([]);
454 noop_func.instructions().end();
455 code.function(&noop_func);
456 encoder.section(&code);
457 }
458 wasmparser::Payload::CodeSectionEntry(_) => {
459 }
461 wasmparser::Payload::End { .. } => {}
462 other => {
463 if let Some((id, range)) = other.as_section() {
464 encoder.section(&wasm_encoder::RawSection {
465 id,
466 data: slice_range(module_bytes, &range)?,
467 });
468 }
469 }
470 }
471 }
472
473 Ok(encoder.finish())
474}
475
476fn add_sandbox_stubs(linker: &mut Linker<PreInitCtx>) -> Result<()> {
478 use wasmtime::component::Accessor;
479
480 linker.root().func_wrap_concurrent(
482 "invoke",
483 |_accessor: &Accessor<PreInitCtx>, (_name, _args): (String, String)| {
484 Box::pin(async move {
485 Ok((Result::<String, String>::Err(
486 "callbacks not available during pre-init".into(),
487 ),))
488 })
489 },
490 )?;
491
492 linker.root().func_new(
494 "list-callbacks",
495 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
496 _func_ty: wasmtime::component::types::ComponentFunc,
497 _params: &[Val],
498 results: &mut [Val]| {
499 results[0] = Val::List(vec![]);
501 Ok(())
502 },
503 )?;
504
505 linker.root().func_new(
507 "report-trace",
508 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
509 _func_ty: wasmtime::component::types::ComponentFunc,
510 _params: &[Val],
511 _results: &mut [Val]| {
512 Ok(())
514 },
515 )?;
516
517 linker.root().func_new(
520 "get-execution-options",
521 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
522 _func_ty: wasmtime::component::types::ComponentFunc,
523 _params: &[Val],
524 results: &mut [Val]| {
525 results[0] = Val::Record(vec![
526 ("python-tracing".to_string(), Val::Bool(false)),
527 ("reuse-empty-callbacks".to_string(), Val::Bool(false)),
528 ]);
529 Ok(())
530 },
531 )?;
532
533 linker.root().func_new(
535 "report-output",
536 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
537 _func_ty: wasmtime::component::types::ComponentFunc,
538 _params: &[Val],
539 _results: &mut [Val]| {
540 Ok(())
542 },
543 )?;
544
545 add_network_stubs(linker)?;
547
548 Ok(())
549}
550
551#[derive(
554 wasmtime::component::ComponentType, wasmtime::component::Lift, wasmtime::component::Lower,
555)]
556#[component(variant)]
557enum PreInitTcpError {
558 #[component(name = "connection-refused")]
559 ConnectionRefused,
560 #[component(name = "connection-reset")]
561 ConnectionReset,
562 #[component(name = "timed-out")]
563 TimedOut,
564 #[component(name = "host-not-found")]
565 HostNotFound,
566 #[component(name = "io-error")]
567 IoError(String),
568 #[component(name = "not-permitted")]
569 NotPermitted(String),
570 #[component(name = "invalid-handle")]
571 InvalidHandle,
572}
573
574#[derive(
577 wasmtime::component::ComponentType, wasmtime::component::Lift, wasmtime::component::Lower,
578)]
579#[component(variant)]
580enum PreInitTlsError {
581 #[component(name = "tcp")]
582 Tcp(PreInitTcpError),
583 #[component(name = "handshake-failed")]
584 HandshakeFailed(String),
585 #[component(name = "certificate-error")]
586 CertificateError(String),
587 #[component(name = "invalid-handle")]
588 InvalidHandle,
589}
590
591fn add_network_stubs(linker: &mut Linker<PreInitCtx>) -> Result<()> {
599 let mut tcp_instance = linker
601 .instance("eryx:net/tcp@0.1.0")
602 .map_err(|e| e.context("Failed to get eryx:net/tcp instance"))?;
603
604 tcp_instance.func_wrap_async(
606 "connect",
607 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
608 (_host, _port, _timeout_ms): (String, u16, u32)| {
609 Box::new(async move {
610 Ok((Result::<u32, PreInitTcpError>::Err(
611 PreInitTcpError::NotPermitted(
612 "networking not available during pre-init".into(),
613 ),
614 ),))
615 })
616 },
617 )?;
618
619 tcp_instance.func_wrap_async(
621 "read",
622 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
623 (_handle, _len, _timeout_ms): (u32, u32, u32)| {
624 Box::new(async move {
625 Ok((Result::<Vec<u8>, PreInitTcpError>::Err(
626 PreInitTcpError::NotPermitted(
627 "networking not available during pre-init".into(),
628 ),
629 ),))
630 })
631 },
632 )?;
633
634 tcp_instance.func_wrap_async(
636 "write",
637 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
638 (_handle, _timeout_ms, _data): (u32, u32, Vec<u8>)| {
639 Box::new(async move {
640 Ok((Result::<u32, PreInitTcpError>::Err(
641 PreInitTcpError::NotPermitted(
642 "networking not available during pre-init".into(),
643 ),
644 ),))
645 })
646 },
647 )?;
648
649 tcp_instance.func_wrap(
651 "close",
652 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>, (_handle,): (u32,)| {
653 Ok(())
655 },
656 )?;
657
658 let mut tls_instance = linker
660 .instance("eryx:net/tls@0.1.0")
661 .map_err(|e| e.context("Failed to get eryx:net/tls instance"))?;
662
663 tls_instance.func_wrap_async(
665 "upgrade",
666 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
667 (_tcp_handle, _hostname, _timeout_ms): (u32, String, u32)| {
668 Box::new(async move {
669 Ok((Result::<u32, PreInitTlsError>::Err(
670 PreInitTlsError::HandshakeFailed(
671 "networking not available during pre-init".into(),
672 ),
673 ),))
674 })
675 },
676 )?;
677
678 tls_instance.func_wrap_async(
680 "read",
681 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
682 (_handle, _len, _timeout_ms): (u32, u32, u32)| {
683 Box::new(async move {
684 Ok((Result::<Vec<u8>, PreInitTlsError>::Err(
685 PreInitTlsError::HandshakeFailed(
686 "networking not available during pre-init".into(),
687 ),
688 ),))
689 })
690 },
691 )?;
692
693 tls_instance.func_wrap_async(
695 "write",
696 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
697 (_handle, _timeout_ms, _data): (u32, u32, Vec<u8>)| {
698 Box::new(async move {
699 Ok((Result::<u32, PreInitTlsError>::Err(
700 PreInitTlsError::HandshakeFailed(
701 "networking not available during pre-init".into(),
702 ),
703 ),))
704 })
705 },
706 )?;
707
708 tls_instance.func_wrap(
710 "close",
711 |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>, (_handle,): (u32,)| {
712 Ok(())
714 },
715 )?;
716
717 Ok(())
718}
719
720async fn call_execute_for_imports(
722 store: &mut Store<PreInitCtx>,
723 instance: &Instance,
724 imports: &[String],
725) -> Result<()> {
726 let import_code = imports
727 .iter()
728 .map(|module| format!("import {module}"))
729 .collect::<Vec<_>>()
730 .join("\n");
731
732 call_execute_code(store, instance, &import_code, "imports").await
733}
734
735async fn call_execute_code(
740 store: &mut Store<PreInitCtx>,
741 instance: &Instance,
742 code: &str,
743 label: &str,
744) -> Result<()> {
745 let execute_func = find_execute_func(store, instance)?;
746
747 let args = [Val::String(code.to_string())];
748 let mut results = vec![Val::Bool(false)];
749
750 execute_func
751 .call_async(&mut *store, &args, &mut results)
752 .await
753 .map_err(|e| e.context(format!("Failed to execute {label} during pre-init")))?;
754
755 match &results[0] {
756 Val::Result(Ok(_)) => Ok(()),
757 Val::Result(Err(Some(error_val))) => {
758 let error_msg = match error_val.as_ref() {
759 Val::String(s) => s.clone(),
760 other => format!("unexpected error value: {other:?}"),
761 };
762 Err(anyhow!(
763 "Pre-init {label} execution failed: {error_msg}\nCode:\n{code}"
764 ))
765 }
766 Val::Result(Err(None)) => Err(anyhow!(
767 "Pre-init {label} execution failed with unknown error\nCode:\n{code}"
768 )),
769 other => {
770 tracing::warn!("Unexpected result type from execute during pre-init: {other:?}");
771 Ok(())
772 }
773 }
774}
775
776fn find_execute_func(store: &mut Store<PreInitCtx>, instance: &Instance) -> Result<Func> {
778 if let Some(func) = instance.get_func(&mut *store, "execute") {
779 Ok(func)
780 } else if let Some(func) = instance.get_func(&mut *store, "[async]execute") {
781 Ok(func)
782 } else {
783 let (_item, exports_idx) = instance
784 .get_export(&mut *store, None, "exports")
785 .ok_or_else(|| anyhow!("No 'exports' or 'execute' export found"))?;
786
787 let execute_idx = instance
788 .get_export_index(&mut *store, Some(&exports_idx), "execute")
789 .ok_or_else(|| anyhow!("No 'execute' in exports interface"))?;
790
791 instance
792 .get_func(&mut *store, execute_idx)
793 .ok_or_else(|| anyhow!("Could not get execute func from index"))
794 }
795}
796
797async fn call_finalize_preinit(store: &mut Store<PreInitCtx>, instance: &Instance) -> Result<()> {
799 let finalize_func = instance
801 .get_func(&mut *store, "finalize-preinit")
802 .ok_or_else(|| anyhow!("finalize-preinit export not found"))?;
803
804 let args: [Val; 0] = [];
806 let mut results: [Val; 0] = [];
807
808 finalize_func
809 .call_async(&mut *store, &args, &mut results)
810 .await
811 .map_err(|e| e.context("Failed to call finalize-preinit"))?;
812
813 Ok(())
814}
815
816#[derive(Debug, Clone)]
818#[non_exhaustive]
819pub enum PreInitError {
820 Engine(String),
822 Compile(String),
824 Instantiate(String),
826 PythonInit(String),
828 Import(String),
830 Transform(String),
832}
833
834impl std::fmt::Display for PreInitError {
835 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
836 match self {
837 Self::Engine(e) => write!(f, "failed to create wasmtime engine: {e}"),
838 Self::Compile(e) => write!(f, "failed to compile component: {e}"),
839 Self::Instantiate(e) => write!(f, "failed to instantiate component: {e}"),
840 Self::PythonInit(e) => write!(f, "Python initialization failed: {e}"),
841 Self::Import(e) => write!(f, "import failed during pre-init: {e}"),
842 Self::Transform(e) => write!(f, "component transform failed: {e}"),
843 }
844 }
845}
846
847impl std::error::Error for PreInitError {}
848
849#[cfg(test)]
850mod tests {
851 use super::*;
852
853 #[test]
854 fn test_preinit_error_display() {
855 let err = PreInitError::PythonInit("test error".to_string());
856 assert!(err.to_string().contains("test error"));
857 }
858
859 #[test]
860 fn test_preinit_error_import_display() {
861 let err = PreInitError::Import("numpy not found".to_string());
862 assert!(err.to_string().contains("numpy not found"));
863 }
864}