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//! ).await?;
35//! ```
36
37use 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
50/// Context for the pre-initialization runtime.
51struct PreInitCtx {
52    wasi: WasiCtx,
53    table: ResourceTable,
54    /// Temp directory for dummy files - must be kept alive during pre-init
55    #[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
74/// Pre-initialize a Python component with native extensions.
75///
76/// This function links the component with native extensions, runs the Python
77/// interpreter's initialization, optionally imports modules, and captures the
78/// initialized memory state into the returned component.
79///
80/// # Arguments
81///
82/// * `python_stdlib` - Path to Python standard library directory
83/// * `site_packages` - Optional path to site-packages directory
84/// * `imports` - Modules to import during pre-init (e.g., ["numpy", "pandas"])
85/// * `extensions` - Native extensions to link into the component
86///
87/// # Returns
88///
89/// The pre-initialized component bytes, ready for instantiation.
90///
91/// # Errors
92///
93/// Returns an error if pre-initialization fails (e.g., Python init error,
94/// import failure).
95pub 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    // Link the component with real WASI adapter.
104    let original_component = link_with_extensions(extensions)
105        .map_err(|e| anyhow!("Failed to link component with extensions: {}", e))?;
106
107    // Phase 1: Instrument the component (synchronous).
108    // This adds state accessor exports that wasmtime-wizer uses to read
109    // memory/global state for the snapshot.
110    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    // Phase 2: Instantiate and run the instrumented component.
116    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    // Set up WASI context with Python paths
124    let table = ResourceTable::new();
125
126    // Build PYTHONPATH from stdlib and site-packages
127    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    // Mount Python stdlib
140    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    // 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(
158                site_pkg,
159                "site-packages",
160                DirPerms::READ,
161                FilePerms::READ,
162            )?;
163        }
164        None
165    } else {
166        // Create empty temp dir for site-packages to avoid errors
167        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    // Create linker and add WASI
189    let mut linker = Linker::new(&engine);
190    wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
191
192    // Add stub implementations for the sandbox imports
193    // These are needed during pre-init but won't be called
194    add_sandbox_stubs(&mut linker)?;
195
196    // Instantiate the component
197    // This triggers Python initialization via wit-dylib's Interpreter::initialize()
198    let instance = linker.instantiate_async(&mut store, &component).await?;
199
200    // If imports are specified, call execute() to import them
201    if !imports.is_empty() {
202        call_execute_for_imports(&mut store, &instance, &imports).await?;
203    }
204
205    // CRITICAL: Call finalize-preinit to reset WASI state AFTER all imports.
206    // This clears file handles from the WASI adapter and wasi-libc so they
207    // don't get captured in the memory snapshot. Without this, restored
208    // instances get "unknown handle index" errors.
209    call_finalize_preinit(&mut store, &instance).await?;
210
211    // Phase 3: Snapshot the initialized state back into the component.
212    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    // Phase 4: Restore _initialize exports stripped by wasmtime-wizer.
224    //
225    // wasmtime-wizer removes _initialize exports from all pre-initialized modules,
226    // but the component's CoreInstance sections still reference them as instantiation
227    // arguments. We add back empty (no-op) _initialize functions so the component
228    // remains valid when loaded into wasmtime.
229    restore_initialize_exports(&snapshot_bytes)
230}
231
232/// Restore `_initialize` exports that wasmtime-wizer strips during snapshot.
233///
234/// wasmtime-wizer's rewrite step removes `_initialize` from all pre-initialized
235/// modules. However, the component's `CoreInstance` sections still reference
236/// `_initialize` as instantiation arguments. This function adds back no-op
237/// `_initialize` function exports to any module that's missing one.
238fn restore_initialize_exports(component_bytes: &[u8]) -> Result<Vec<u8>> {
239    // Pass 1: Find which modules have _initialize and which import it.
240    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            // Use a fresh parser at offset 0 for the module slice
252            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 {
263                            if import?.name == "_initialize" {
264                                any_module_imports_init = true;
265                            }
266                        }
267                    }
268                    _ => {}
269                }
270            }
271            module_index += 1;
272        }
273    }
274
275    if !any_module_imports_init {
276        return Ok(component_bytes.to_vec());
277    }
278
279    // Pass 2: Rebuild the component, adding _initialize to modules that lack it.
280    let mut component = wasm_encoder::Component::new();
281    module_index = 0;
282    let mut depth = 0u32;
283
284    for payload in wasmparser::Parser::new(0).parse_all(component_bytes) {
285        let payload = payload?;
286
287        // Track nesting depth — only process top-level sections
288        match &payload {
289            wasmparser::Payload::Version { .. } => {
290                if depth > 0 {
291                    // Nested component/module version — skip, handled by parent
292                    depth += 1;
293                    continue;
294                }
295                depth += 1;
296                continue; // Skip — Component::new() writes the header
297            }
298            wasmparser::Payload::End { .. } => {
299                depth -= 1;
300                continue; // Skip — finish() writes this
301            }
302            _ => {
303                if depth > 1 {
304                    // Inside a nested module/component — skip individual payloads
305                    continue;
306                }
307            }
308        }
309
310        match payload {
311            wasmparser::Payload::ModuleSection {
312                unchecked_range: range,
313                ..
314            } => {
315                let module_bytes = &component_bytes[range.start..range.end];
316
317                if !modules_with_init.contains(&module_index) {
318                    let patched = add_noop_initialize(module_bytes)?;
319                    component.section(&wasm_encoder::RawSection {
320                        id: wasm_encoder::ComponentSectionId::CoreModule as u8,
321                        data: &patched,
322                    });
323                } else {
324                    component.section(&wasm_encoder::RawSection {
325                        id: wasm_encoder::ComponentSectionId::CoreModule as u8,
326                        data: module_bytes,
327                    });
328                }
329                module_index += 1;
330            }
331            other => {
332                if let Some((id, range)) = other.as_section() {
333                    component.section(&wasm_encoder::RawSection {
334                        id,
335                        data: &component_bytes[range.start..range.end],
336                    });
337                }
338            }
339        }
340    }
341
342    Ok(component.finish())
343}
344
345/// Add a no-op `_initialize` function export to a core module.
346///
347/// Parses the module to find type/function counts, then rebuilds it
348/// section-by-section, appending a new type (if needed), function declaration,
349/// code body, and export entry for `_initialize`.
350fn add_noop_initialize(module_bytes: &[u8]) -> Result<Vec<u8>> {
351    use wasm_encoder::reencode::{Reencode, RoundtripReencoder};
352
353    let mut num_types = 0u32;
354    let mut num_imported_funcs = 0u32;
355    let mut num_defined_funcs = 0u32;
356    let mut noop_type_idx = None;
357
358    // First pass: count types/functions and find existing () -> () type
359    for payload in wasmparser::Parser::new(0).parse_all(module_bytes) {
360        match payload? {
361            wasmparser::Payload::TypeSection(reader) => {
362                for ty in reader.into_iter() {
363                    let ty = ty?;
364                    for sub in ty.types() {
365                        if let wasmparser::CompositeInnerType::Func(func_ty) =
366                            &sub.composite_type.inner
367                            && func_ty.params().is_empty()
368                            && func_ty.results().is_empty()
369                        {
370                            noop_type_idx = Some(num_types);
371                        }
372                        num_types += 1;
373                    }
374                }
375            }
376            wasmparser::Payload::ImportSection(reader) => {
377                for import in reader {
378                    if matches!(import?.ty, wasmparser::TypeRef::Func(_)) {
379                        num_imported_funcs += 1;
380                    }
381                }
382            }
383            wasmparser::Payload::FunctionSection(reader) => {
384                num_defined_funcs = reader.count();
385            }
386            wasmparser::Payload::CodeSectionStart { .. } => {}
387            _ => {}
388        }
389    }
390
391    let num_funcs = num_imported_funcs + num_defined_funcs;
392    let noop_type = noop_type_idx.unwrap_or(num_types);
393    let noop_func_index = num_funcs;
394    let needs_new_type = noop_type_idx.is_none();
395
396    // Second pass: rebuild module using reencode for most sections.
397    // For the code section, we use the saved range to create a CodeSectionReader.
398    let mut encoder = wasm_encoder::Module::new();
399    let mut reencode = RoundtripReencoder;
400
401    for payload in wasmparser::Parser::new(0).parse_all(module_bytes) {
402        match payload? {
403            wasmparser::Payload::Version { .. } => {}
404            wasmparser::Payload::TypeSection(reader) => {
405                let mut types = wasm_encoder::TypeSection::new();
406                reencode.parse_type_section(&mut types, reader)?;
407                if needs_new_type {
408                    types.ty().function([], []);
409                }
410                encoder.section(&types);
411            }
412            wasmparser::Payload::FunctionSection(reader) => {
413                let mut funcs = wasm_encoder::FunctionSection::new();
414                reencode.parse_function_section(&mut funcs, reader)?;
415                funcs.function(noop_type);
416                encoder.section(&funcs);
417            }
418            wasmparser::Payload::ExportSection(reader) => {
419                let mut exports = wasm_encoder::ExportSection::new();
420                reencode.parse_export_section(&mut exports, reader)?;
421                exports.export(
422                    "_initialize",
423                    wasm_encoder::ExportKind::Func,
424                    noop_func_index,
425                );
426                encoder.section(&exports);
427            }
428            wasmparser::Payload::CodeSectionStart { range, .. } => {
429                // Re-parse the code section from the saved range and reencode it,
430                // then append our noop function.
431                let section_data = &module_bytes[range.start..range.end];
432                let code_reader = wasmparser::CodeSectionReader::new(
433                    wasmparser::BinaryReader::new(section_data, 0),
434                )?;
435
436                let mut code = wasm_encoder::CodeSection::new();
437                reencode.parse_code_section(&mut code, code_reader)?;
438
439                // Append noop function body
440                let mut noop_func = wasm_encoder::Function::new([]);
441                noop_func.instructions().end();
442                code.function(&noop_func);
443                encoder.section(&code);
444            }
445            wasmparser::Payload::CodeSectionEntry(_) => {
446                // Already handled in CodeSectionStart above
447            }
448            wasmparser::Payload::End { .. } => {}
449            other => {
450                if let Some((id, range)) = other.as_section() {
451                    encoder.section(&wasm_encoder::RawSection {
452                        id,
453                        data: &module_bytes[range.start..range.end],
454                    });
455                }
456            }
457        }
458    }
459
460    Ok(encoder.finish())
461}
462
463/// Add stub implementations for sandbox imports during pre-init.
464fn add_sandbox_stubs(linker: &mut Linker<PreInitCtx>) -> Result<()> {
465    use wasmtime::component::Accessor;
466
467    // The component imports "invoke" for callbacks (wasmtime 40+ uses plain name)
468    linker.root().func_wrap_concurrent(
469        "invoke",
470        |_accessor: &Accessor<PreInitCtx>, (_name, _args): (String, String)| {
471            Box::pin(async move {
472                Ok((Result::<String, String>::Err(
473                    "callbacks not available during pre-init".into(),
474                ),))
475            })
476        },
477    )?;
478
479    // list-callbacks: func() -> list<callback-info>
480    linker.root().func_new(
481        "list-callbacks",
482        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
483         _func_ty: wasmtime::component::types::ComponentFunc,
484         _params: &[Val],
485         results: &mut [Val]| {
486            // Return empty list
487            results[0] = Val::List(vec![]);
488            Ok(())
489        },
490    )?;
491
492    // report-trace: func(lineno: u32, event-json: string, context-json: string)
493    linker.root().func_new(
494        "report-trace",
495        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
496         _func_ty: wasmtime::component::types::ComponentFunc,
497         _params: &[Val],
498         _results: &mut [Val]| {
499            // No-op - trace events during init can be ignored
500            Ok(())
501        },
502    )?;
503
504    // report-output: func(stream-id: u32, data: string)
505    linker.root().func_new(
506        "report-output",
507        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
508         _func_ty: wasmtime::component::types::ComponentFunc,
509         _params: &[Val],
510         _results: &mut [Val]| {
511            // No-op - output during init can be ignored
512            Ok(())
513        },
514    )?;
515
516    // Add network stubs (TCP and TLS interfaces)
517    add_network_stubs(linker)?;
518
519    Ok(())
520}
521
522/// TCP error type for pre-init stubs.
523/// This mirrors the WIT variant `tcp-error` so wasmtime can lower/lift it.
524#[derive(
525    wasmtime::component::ComponentType, wasmtime::component::Lift, wasmtime::component::Lower,
526)]
527#[component(variant)]
528enum PreInitTcpError {
529    #[component(name = "connection-refused")]
530    ConnectionRefused,
531    #[component(name = "connection-reset")]
532    ConnectionReset,
533    #[component(name = "timed-out")]
534    TimedOut,
535    #[component(name = "host-not-found")]
536    HostNotFound,
537    #[component(name = "io-error")]
538    IoError(String),
539    #[component(name = "not-permitted")]
540    NotPermitted(String),
541    #[component(name = "invalid-handle")]
542    InvalidHandle,
543}
544
545/// TLS error type for pre-init stubs.
546/// This mirrors the WIT variant `tls-error`.
547#[derive(
548    wasmtime::component::ComponentType, wasmtime::component::Lift, wasmtime::component::Lower,
549)]
550#[component(variant)]
551enum PreInitTlsError {
552    #[component(name = "tcp")]
553    Tcp(PreInitTcpError),
554    #[component(name = "handshake-failed")]
555    HandshakeFailed(String),
556    #[component(name = "certificate-error")]
557    CertificateError(String),
558    #[component(name = "invalid-handle")]
559    InvalidHandle,
560}
561
562/// Add stub implementations for network imports during pre-init.
563///
564/// These stubs return errors if called - networking isn't available during pre-init.
565/// The stubs are needed so the component can be instantiated.
566///
567/// Note: The WIT declares these as sync `func` but we use fiber-based async on the host
568/// (`func_wrap_async`), which appears blocking to the guest but allows async I/O on the host.
569fn add_network_stubs(linker: &mut Linker<PreInitCtx>) -> Result<()> {
570    // Get or create the eryx:net/tcp interface
571    let mut tcp_instance = linker
572        .instance("eryx:net/tcp@0.1.0")
573        .map_err(|e| e.context("Failed to get eryx:net/tcp instance"))?;
574
575    // tcp.connect: func(host: string, port: u16, timeout-ms: u32) -> result<tcp-handle, tcp-error>
576    tcp_instance.func_wrap_async(
577        "connect",
578        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
579         (_host, _port, _timeout_ms): (String, u16, u32)| {
580            Box::new(async move {
581                Ok((Result::<u32, PreInitTcpError>::Err(
582                    PreInitTcpError::NotPermitted(
583                        "networking not available during pre-init".into(),
584                    ),
585                ),))
586            })
587        },
588    )?;
589
590    // tcp.read: func(handle: tcp-handle, len: u32, timeout-ms: u32) -> result<list<u8>, tcp-error>
591    tcp_instance.func_wrap_async(
592        "read",
593        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
594         (_handle, _len, _timeout_ms): (u32, u32, u32)| {
595            Box::new(async move {
596                Ok((Result::<Vec<u8>, PreInitTcpError>::Err(
597                    PreInitTcpError::NotPermitted(
598                        "networking not available during pre-init".into(),
599                    ),
600                ),))
601            })
602        },
603    )?;
604
605    // tcp.write: func(handle: tcp-handle, timeout-ms: u32, data: list<u8>) -> result<u32, tcp-error>
606    tcp_instance.func_wrap_async(
607        "write",
608        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
609         (_handle, _timeout_ms, _data): (u32, u32, Vec<u8>)| {
610            Box::new(async move {
611                Ok((Result::<u32, PreInitTcpError>::Err(
612                    PreInitTcpError::NotPermitted(
613                        "networking not available during pre-init".into(),
614                    ),
615                ),))
616            })
617        },
618    )?;
619
620    // tcp.close: func(handle: tcp-handle)
621    tcp_instance.func_wrap(
622        "close",
623        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>, (_handle,): (u32,)| {
624            // No-op - handle doesn't exist anyway
625            Ok(())
626        },
627    )?;
628
629    // Get or create the eryx:net/tls interface
630    let mut tls_instance = linker
631        .instance("eryx:net/tls@0.1.0")
632        .map_err(|e| e.context("Failed to get eryx:net/tls instance"))?;
633
634    // tls.upgrade: func(tcp: tcp-handle, hostname: string, timeout-ms: u32) -> result<tls-handle, tls-error>
635    tls_instance.func_wrap_async(
636        "upgrade",
637        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
638         (_tcp_handle, _hostname, _timeout_ms): (u32, String, u32)| {
639            Box::new(async move {
640                Ok((Result::<u32, PreInitTlsError>::Err(
641                    PreInitTlsError::HandshakeFailed(
642                        "networking not available during pre-init".into(),
643                    ),
644                ),))
645            })
646        },
647    )?;
648
649    // tls.read: func(handle: tls-handle, len: u32, timeout-ms: u32) -> result<list<u8>, tls-error>
650    tls_instance.func_wrap_async(
651        "read",
652        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
653         (_handle, _len, _timeout_ms): (u32, u32, u32)| {
654            Box::new(async move {
655                Ok((Result::<Vec<u8>, PreInitTlsError>::Err(
656                    PreInitTlsError::HandshakeFailed(
657                        "networking not available during pre-init".into(),
658                    ),
659                ),))
660            })
661        },
662    )?;
663
664    // tls.write: func(handle: tls-handle, timeout-ms: u32, data: list<u8>) -> result<u32, tls-error>
665    tls_instance.func_wrap_async(
666        "write",
667        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>,
668         (_handle, _timeout_ms, _data): (u32, u32, Vec<u8>)| {
669            Box::new(async move {
670                Ok((Result::<u32, PreInitTlsError>::Err(
671                    PreInitTlsError::HandshakeFailed(
672                        "networking not available during pre-init".into(),
673                    ),
674                ),))
675            })
676        },
677    )?;
678
679    // tls.close: func(handle: tls-handle)
680    tls_instance.func_wrap(
681        "close",
682        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>, (_handle,): (u32,)| {
683            // No-op - handle doesn't exist anyway
684            Ok(())
685        },
686    )?;
687
688    Ok(())
689}
690
691/// Call the execute export to import modules during pre-init.
692async fn call_execute_for_imports(
693    store: &mut Store<PreInitCtx>,
694    instance: &Instance,
695    imports: &[String],
696) -> Result<()> {
697    // Find the execute function.
698    // Our WIT exports functions directly, not in an "exports" interface.
699    // Try direct export first, then fall back to exports interface.
700    let execute_func = if let Some(func) = instance.get_func(&mut *store, "execute") {
701        func
702    } else if let Some(func) = instance.get_func(&mut *store, "[async]execute") {
703        // Async exports may have [async] prefix
704        func
705    } else {
706        // Try looking in an "exports" interface (for compatibility)
707        let (_item, exports_idx) = instance
708            .get_export(&mut *store, None, "exports")
709            .ok_or_else(|| anyhow!("No 'exports' or 'execute' export found"))?;
710
711        let execute_idx = instance
712            .get_export_index(&mut *store, Some(&exports_idx), "execute")
713            .ok_or_else(|| anyhow!("No 'execute' in exports interface"))?;
714
715        instance
716            .get_func(&mut *store, execute_idx)
717            .ok_or_else(|| anyhow!("Could not get execute func from index"))?
718    };
719
720    // Generate import code
721    let import_code = imports
722        .iter()
723        .map(|module| format!("import {module}"))
724        .collect::<Vec<_>>()
725        .join("\n");
726
727    // Call execute with the import code
728    let args = [Val::String(import_code.clone())];
729    // Result placeholder - wasmtime will fill this with Val::Result
730    let mut results = vec![Val::Bool(false)];
731
732    execute_func
733        .call_async(&mut *store, &args, &mut results)
734        .await
735        .map_err(|e| e.context("Failed to execute imports during pre-init"))?;
736
737    // Check if the result was an error
738    // result<string, string> is represented as Val::Result(Result<Option<Box<Val>>, Option<Box<Val>>>)
739    match &results[0] {
740        Val::Result(Ok(_)) => {
741            // Success - imports completed
742            Ok(())
743        }
744        Val::Result(Err(Some(error_val))) => {
745            // Error - extract the error message
746            let error_msg = match error_val.as_ref() {
747                Val::String(s) => s.clone(),
748                other => format!("unexpected error value: {other:?}"),
749            };
750            Err(anyhow!(
751                "Pre-init import execution failed: {error_msg}\nImport code:\n{import_code}"
752            ))
753        }
754        Val::Result(Err(None)) => Err(anyhow!(
755            "Pre-init import execution failed with unknown error\nImport code:\n{import_code}"
756        )),
757        other => {
758            // Unexpected result type - log warning but don't fail
759            // This shouldn't happen, but be defensive
760            tracing::warn!("Unexpected result type from execute during pre-init: {other:?}");
761            Ok(())
762        }
763    }
764}
765
766/// Call the finalize-preinit export to reset WASI state after imports.
767async fn call_finalize_preinit(store: &mut Store<PreInitCtx>, instance: &Instance) -> Result<()> {
768    // Find the finalize-preinit function
769    let finalize_func = instance
770        .get_func(&mut *store, "finalize-preinit")
771        .ok_or_else(|| anyhow!("finalize-preinit export not found"))?;
772
773    // Call it (no arguments, no return value)
774    let args: [Val; 0] = [];
775    let mut results: [Val; 0] = [];
776
777    finalize_func
778        .call_async(&mut *store, &args, &mut results)
779        .await
780        .map_err(|e| e.context("Failed to call finalize-preinit"))?;
781
782    Ok(())
783}
784
785/// Errors that can occur during pre-initialization.
786#[derive(Debug, Clone)]
787#[non_exhaustive]
788pub enum PreInitError {
789    /// Failed to create wasmtime engine.
790    Engine(String),
791    /// Failed to compile component.
792    Compile(String),
793    /// Failed to instantiate component.
794    Instantiate(String),
795    /// Python initialization failed.
796    PythonInit(String),
797    /// Import failed during pre-init.
798    Import(String),
799    /// Component transform failed.
800    Transform(String),
801}
802
803impl std::fmt::Display for PreInitError {
804    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
805        match self {
806            Self::Engine(e) => write!(f, "failed to create wasmtime engine: {e}"),
807            Self::Compile(e) => write!(f, "failed to compile component: {e}"),
808            Self::Instantiate(e) => write!(f, "failed to instantiate component: {e}"),
809            Self::PythonInit(e) => write!(f, "Python initialization failed: {e}"),
810            Self::Import(e) => write!(f, "import failed during pre-init: {e}"),
811            Self::Transform(e) => write!(f, "component transform failed: {e}"),
812        }
813    }
814}
815
816impl std::error::Error for PreInitError {}
817
818#[cfg(test)]
819mod tests {
820    use super::*;
821
822    #[test]
823    fn test_preinit_error_display() {
824        let err = PreInitError::PythonInit("test error".to_string());
825        assert!(err.to_string().contains("test error"));
826    }
827
828    #[test]
829    fn test_preinit_error_import_display() {
830        let err = PreInitError::Import("numpy not found".to_string());
831        assert!(err.to_string().contains("numpy not found"));
832    }
833}