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