Skip to main content

eryx_runtime/
preinit.rs

1//! Pre-initialization support for linked Python components.
2//!
3//! This module provides functionality to pre-initialize Python components
4//! after linking. Pre-initialization runs the Python interpreter's startup
5//! code and captures the initialized memory state into the component, avoiding
6//! the initialization cost at runtime.
7//!
8//! # How It Works
9//!
10//! 1. We link the component with real WASI imports
11//! 2. We use `wasmtime-wizer` to instrument the component (adding state accessors)
12//! 3. We instantiate the instrumented component - Python initializes
13//! 4. Optionally run imports (e.g., `import numpy`) to capture more state
14//! 5. Call `finalize-preinit` to reset WASI file handle state
15//! 6. The memory state is captured and embedded into the original component
16//! 7. The resulting component starts with Python already initialized
17//!
18//! # Performance Impact
19//!
20//! - First build with pre-init: ~3-4 seconds (one-time cost)
21//! - Per-execution after pre-init: ~1-5ms (vs ~450-500ms without)
22//!
23//! # Example
24//!
25//! ```rust,ignore
26//! use eryx_runtime::preinit::pre_initialize;
27//!
28//! // Pre-initialize with native extensions
29//! let preinit_component = pre_initialize(
30//!     &python_stdlib_path,
31//!     Some(&site_packages_path),
32//!     &["numpy", "pandas"],  // Modules to import during pre-init
33//!     &native_extensions,
34//!     Some("import numpy as np; arr = np.zeros(10)"),  // Optional setup code
35//! ).await?;
36//! ```
37
38use 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
51/// Context for the pre-initialization runtime.
52struct PreInitCtx {
53    wasi: WasiCtx,
54    table: ResourceTable,
55    /// Temp directory for dummy files - must be kept alive during pre-init
56    #[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
75/// Pre-initialize a Python component with native extensions.
76///
77/// This function links the component with native extensions, runs the Python
78/// interpreter's initialization, optionally imports modules, and captures the
79/// initialized memory state into the returned component.
80///
81/// # Arguments
82///
83/// * `python_stdlib` - Path to Python standard library directory
84/// * `site_packages` - Optional path to site-packages directory
85/// * `imports` - Modules to import during pre-init (e.g., ["numpy", "pandas"])
86/// * `extensions` - Native extensions to link into the component
87/// * `setup_code` - Optional Python code to execute after imports, baked into
88///   the snapshot. Use this to pre-create objects (e.g., a Jinja2
89///   `SandboxedEnvironment`) so every sandbox starts with them in COW memory.
90///
91/// # Returns
92///
93/// The pre-initialized component bytes, ready for instantiation.
94///
95/// # Errors
96///
97/// Returns an error if pre-initialization fails (e.g., Python init error,
98/// import failure, or setup code exception).
99pub 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    // Link the component with real WASI adapter.
109    let original_component = link_with_extensions(extensions)
110        .map_err(|e| anyhow!("Failed to link component with extensions: {}", e))?;
111
112    // Phase 1: Instrument the component (synchronous).
113    // This adds state accessor exports that wasmtime-wizer uses to read
114    // memory/global state for the snapshot.
115    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    // Phase 2: Instantiate and run the instrumented component.
121    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    // Set up WASI context with Python paths
129    let table = ResourceTable::new();
130
131    // Build PYTHONPATH from stdlib and site-packages
132    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    // Mount Python stdlib
145    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    // Mount site-packages if provided
155    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        // Create empty temp dir for site-packages to avoid errors
162        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    // Create linker and add WASI
179    let mut linker = Linker::new(&engine);
180    wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
181
182    // Add stub implementations for the sandbox imports
183    // These are needed during pre-init but won't be called
184    add_sandbox_stubs(&mut linker)?;
185
186    // Instantiate the component
187    // This triggers Python initialization via wit-dylib's Interpreter::initialize()
188    let instance = linker.instantiate_async(&mut store, &component).await?;
189
190    // If imports are specified, call execute() to import them
191    if !imports.is_empty() {
192        call_execute_for_imports(&mut store, &instance, &imports).await?;
193    }
194
195    // If setup code is provided, execute it after imports so its state
196    // (variables, objects, etc.) gets captured in the Wizer snapshot.
197    if let Some(code) = setup_code {
198        call_execute_code(&mut store, &instance, code, "setup code").await?;
199    }
200
201    // CRITICAL: Call finalize-preinit to reset WASI state AFTER all imports.
202    // This clears file handles from the WASI adapter and wasi-libc so they
203    // don't get captured in the memory snapshot. Without this, restored
204    // instances get "unknown handle index" errors.
205    call_finalize_preinit(&mut store, &instance).await?;
206
207    // Phase 3: Snapshot the initialized state back into the component.
208    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    // Phase 4: Restore _initialize exports stripped by wasmtime-wizer.
220    //
221    // wasmtime-wizer removes _initialize exports from all pre-initialized modules,
222    // but the component's CoreInstance sections still reference them as instantiation
223    // arguments. We add back empty (no-op) _initialize functions so the component
224    // remains valid when loaded into wasmtime.
225    restore_initialize_exports(&snapshot_bytes)
226}
227
228/// Restore `_initialize` exports that wasmtime-wizer strips during snapshot.
229///
230/// wasmtime-wizer's rewrite step removes `_initialize` from all pre-initialized
231/// modules. However, the component's `CoreInstance` sections still reference
232/// `_initialize` as instantiation arguments. This function adds back no-op
233/// `_initialize` function exports to any module that's missing one.
234/// Slice `bytes` by a wasmparser section range.
235///
236/// wasmparser 0.257 widened section ranges to `u64` so it can describe 64-bit
237/// modules. Everything here is parsed from an in-memory slice, so the narrowing
238/// only fails on a target where the offsets genuinely do not fit in a `usize` —
239/// in which case the buffer could not have been read in the first place.
240fn 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    // Pass 1: Find which modules have _initialize and which import it.
248    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            // Use a fresh parser at offset 0 for the module slice
260            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                        // Each section entry is a *group* since the compact
271                        // imports proposal, so flatten before matching names.
272                        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    // Pass 2: Rebuild the component, adding _initialize to modules that lack it.
290    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        // Track nesting depth — only process top-level sections
298        match &payload {
299            wasmparser::Payload::Version { .. } => {
300                if depth > 0 {
301                    // Nested component/module version — skip, handled by parent
302                    depth += 1;
303                    continue;
304                }
305                depth += 1;
306                continue; // Skip — Component::new() writes the header
307            }
308            wasmparser::Payload::End { .. } => {
309                depth -= 1;
310                continue; // Skip — finish() writes this
311            }
312            _ => {
313                if depth > 1 {
314                    // Inside a nested module/component — skip individual payloads
315                    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
355/// Add a no-op `_initialize` function export to a core module.
356///
357/// Parses the module to find type/function counts, then rebuilds it
358/// section-by-section, appending a new type (if needed), function declaration,
359/// code body, and export entry for `_initialize`.
360fn 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    // First pass: count types/functions and find existing () -> () type
369    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                // Must flatten each group: this count defines where the
388                // defined-function index space starts, so a group that expands
389                // to several imported functions has to contribute all of them.
390                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    // Second pass: rebuild module using reencode for most sections.
410    // For the code section, we use the saved range to create a CodeSectionReader.
411    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                // Re-parse the code section from the saved range and reencode it,
443                // then append our noop function.
444                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                // Append noop function body
453                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                // Already handled in CodeSectionStart above
460            }
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
476/// Add stub implementations for sandbox imports during pre-init.
477fn add_sandbox_stubs(linker: &mut Linker<PreInitCtx>) -> Result<()> {
478    use wasmtime::component::Accessor;
479
480    // The component imports "invoke" for callbacks (wasmtime 40+ uses plain name)
481    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    // list-callbacks: func() -> list<callback-info>
493    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            // Return empty list
500            results[0] = Val::List(vec![]);
501            Ok(())
502        },
503    )?;
504
505    // report-trace: func(lineno: u32, event-json: string, context-json: string)
506    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            // No-op - trace events during init can be ignored
513            Ok(())
514        },
515    )?;
516
517    // get-execution-options: func() -> execution-options
518    // Pre-initialization does not execute user code, so no options are needed.
519    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    // report-output: func(stream-id: u32, data: string)
534    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            // No-op - output during init can be ignored
541            Ok(())
542        },
543    )?;
544
545    // Add network stubs (TCP and TLS interfaces)
546    add_network_stubs(linker)?;
547
548    Ok(())
549}
550
551/// TCP error type for pre-init stubs.
552/// This mirrors the WIT variant `tcp-error` so wasmtime can lower/lift it.
553#[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/// TLS error type for pre-init stubs.
575/// This mirrors the WIT variant `tls-error`.
576#[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
591/// Add stub implementations for network imports during pre-init.
592///
593/// These stubs return errors if called - networking isn't available during pre-init.
594/// The stubs are needed so the component can be instantiated.
595///
596/// Note: The WIT declares these as sync `func` but we use fiber-based async on the host
597/// (`func_wrap_async`), which appears blocking to the guest but allows async I/O on the host.
598fn add_network_stubs(linker: &mut Linker<PreInitCtx>) -> Result<()> {
599    // Get or create the eryx:net/tcp interface
600    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.connect: func(host: string, port: u16, timeout-ms: u32) -> result<tcp-handle, tcp-error>
605    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.read: func(handle: tcp-handle, len: u32, timeout-ms: u32) -> result<list<u8>, tcp-error>
620    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.write: func(handle: tcp-handle, timeout-ms: u32, data: list<u8>) -> result<u32, tcp-error>
635    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.close: func(handle: tcp-handle)
650    tcp_instance.func_wrap(
651        "close",
652        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>, (_handle,): (u32,)| {
653            // No-op - handle doesn't exist anyway
654            Ok(())
655        },
656    )?;
657
658    // Get or create the eryx:net/tls interface
659    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.upgrade: func(tcp: tcp-handle, hostname: string, timeout-ms: u32) -> result<tls-handle, tls-error>
664    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.read: func(handle: tls-handle, len: u32, timeout-ms: u32) -> result<list<u8>, tls-error>
679    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.write: func(handle: tls-handle, timeout-ms: u32, data: list<u8>) -> result<u32, tls-error>
694    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.close: func(handle: tls-handle)
709    tls_instance.func_wrap(
710        "close",
711        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>, (_handle,): (u32,)| {
712            // No-op - handle doesn't exist anyway
713            Ok(())
714        },
715    )?;
716
717    Ok(())
718}
719
720/// Call the execute export to import modules during pre-init.
721async 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
735/// Call the execute export with arbitrary Python code during pre-init.
736///
737/// The `label` is used in error messages to identify what kind of code failed
738/// (e.g., "imports", "setup code").
739async 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
776/// Find the `execute` export function on the WASM instance.
777fn 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
797/// Call the finalize-preinit export to reset WASI state after imports.
798async fn call_finalize_preinit(store: &mut Store<PreInitCtx>, instance: &Instance) -> Result<()> {
799    // Find the finalize-preinit function
800    let finalize_func = instance
801        .get_func(&mut *store, "finalize-preinit")
802        .ok_or_else(|| anyhow!("finalize-preinit export not found"))?;
803
804    // Call it (no arguments, no return value)
805    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/// Errors that can occur during pre-initialization.
817#[derive(Debug, Clone)]
818#[non_exhaustive]
819pub enum PreInitError {
820    /// Failed to create wasmtime engine.
821    Engine(String),
822    /// Failed to compile component.
823    Compile(String),
824    /// Failed to instantiate component.
825    Instantiate(String),
826    /// Python initialization failed.
827    PythonInit(String),
828    /// Import failed during pre-init.
829    Import(String),
830    /// Component transform failed.
831    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}