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                        // Each section entry is a *group* since the compact
263                        // imports proposal, so flatten before matching names.
264                        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    // Pass 2: Rebuild the component, adding _initialize to modules that lack it.
282    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        // Track nesting depth — only process top-level sections
290        match &payload {
291            wasmparser::Payload::Version { .. } => {
292                if depth > 0 {
293                    // Nested component/module version — skip, handled by parent
294                    depth += 1;
295                    continue;
296                }
297                depth += 1;
298                continue; // Skip — Component::new() writes the header
299            }
300            wasmparser::Payload::End { .. } => {
301                depth -= 1;
302                continue; // Skip — finish() writes this
303            }
304            _ => {
305                if depth > 1 {
306                    // Inside a nested module/component — skip individual payloads
307                    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
347/// Add a no-op `_initialize` function export to a core module.
348///
349/// Parses the module to find type/function counts, then rebuilds it
350/// section-by-section, appending a new type (if needed), function declaration,
351/// code body, and export entry for `_initialize`.
352fn 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    // First pass: count types/functions and find existing () -> () type
361    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                // Must flatten each group: this count defines where the
380                // defined-function index space starts, so a group that expands
381                // to several imported functions has to contribute all of them.
382                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    // Second pass: rebuild module using reencode for most sections.
402    // For the code section, we use the saved range to create a CodeSectionReader.
403    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                // Re-parse the code section from the saved range and reencode it,
435                // then append our noop function.
436                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                // Append noop function body
445                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                // Already handled in CodeSectionStart above
452            }
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
468/// Add stub implementations for sandbox imports during pre-init.
469fn add_sandbox_stubs(linker: &mut Linker<PreInitCtx>) -> Result<()> {
470    use wasmtime::component::Accessor;
471
472    // The component imports "invoke" for callbacks (wasmtime 40+ uses plain name)
473    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    // list-callbacks: func() -> list<callback-info>
485    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            // Return empty list
492            results[0] = Val::List(vec![]);
493            Ok(())
494        },
495    )?;
496
497    // report-trace: func(lineno: u32, event-json: string, context-json: string)
498    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            // No-op - trace events during init can be ignored
505            Ok(())
506        },
507    )?;
508
509    // get-execution-options: func() -> execution-options
510    // Pre-initialization does not execute user code, so no options are needed.
511    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    // report-output: func(stream-id: u32, data: string)
526    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            // No-op - output during init can be ignored
533            Ok(())
534        },
535    )?;
536
537    // Add network stubs (TCP and TLS interfaces)
538    add_network_stubs(linker)?;
539
540    Ok(())
541}
542
543/// TCP error type for pre-init stubs.
544/// This mirrors the WIT variant `tcp-error` so wasmtime can lower/lift it.
545#[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/// TLS error type for pre-init stubs.
567/// This mirrors the WIT variant `tls-error`.
568#[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
583/// Add stub implementations for network imports during pre-init.
584///
585/// These stubs return errors if called - networking isn't available during pre-init.
586/// The stubs are needed so the component can be instantiated.
587///
588/// Note: The WIT declares these as sync `func` but we use fiber-based async on the host
589/// (`func_wrap_async`), which appears blocking to the guest but allows async I/O on the host.
590fn add_network_stubs(linker: &mut Linker<PreInitCtx>) -> Result<()> {
591    // Get or create the eryx:net/tcp interface
592    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.connect: func(host: string, port: u16, timeout-ms: u32) -> result<tcp-handle, tcp-error>
597    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.read: func(handle: tcp-handle, len: u32, timeout-ms: u32) -> result<list<u8>, tcp-error>
612    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.write: func(handle: tcp-handle, timeout-ms: u32, data: list<u8>) -> result<u32, tcp-error>
627    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.close: func(handle: tcp-handle)
642    tcp_instance.func_wrap(
643        "close",
644        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>, (_handle,): (u32,)| {
645            // No-op - handle doesn't exist anyway
646            Ok(())
647        },
648    )?;
649
650    // Get or create the eryx:net/tls interface
651    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.upgrade: func(tcp: tcp-handle, hostname: string, timeout-ms: u32) -> result<tls-handle, tls-error>
656    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.read: func(handle: tls-handle, len: u32, timeout-ms: u32) -> result<list<u8>, tls-error>
671    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.write: func(handle: tls-handle, timeout-ms: u32, data: list<u8>) -> result<u32, tls-error>
686    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.close: func(handle: tls-handle)
701    tls_instance.func_wrap(
702        "close",
703        |_ctx: wasmtime::StoreContextMut<'_, PreInitCtx>, (_handle,): (u32,)| {
704            // No-op - handle doesn't exist anyway
705            Ok(())
706        },
707    )?;
708
709    Ok(())
710}
711
712/// Call the execute export to import modules during pre-init.
713async fn call_execute_for_imports(
714    store: &mut Store<PreInitCtx>,
715    instance: &Instance,
716    imports: &[String],
717) -> Result<()> {
718    // Find the execute function.
719    // Our WIT exports functions directly, not in an "exports" interface.
720    // Try direct export first, then fall back to exports interface.
721    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        // Async exports may have [async] prefix
725        func
726    } else {
727        // Try looking in an "exports" interface (for compatibility)
728        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    // Generate import code
742    let import_code = imports
743        .iter()
744        .map(|module| format!("import {module}"))
745        .collect::<Vec<_>>()
746        .join("\n");
747
748    // Call execute with the import code
749    let args = [Val::String(import_code.clone())];
750    // Result placeholder - wasmtime will fill this with Val::Result
751    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    // Check if the result was an error
759    // result<string, string> is represented as Val::Result(Result<Option<Box<Val>>, Option<Box<Val>>>)
760    match &results[0] {
761        Val::Result(Ok(_)) => {
762            // Success - imports completed
763            Ok(())
764        }
765        Val::Result(Err(Some(error_val))) => {
766            // Error - extract the error message
767            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            // Unexpected result type - log warning but don't fail
780            // This shouldn't happen, but be defensive
781            tracing::warn!("Unexpected result type from execute during pre-init: {other:?}");
782            Ok(())
783        }
784    }
785}
786
787/// Call the finalize-preinit export to reset WASI state after imports.
788async fn call_finalize_preinit(store: &mut Store<PreInitCtx>, instance: &Instance) -> Result<()> {
789    // Find the finalize-preinit function
790    let finalize_func = instance
791        .get_func(&mut *store, "finalize-preinit")
792        .ok_or_else(|| anyhow!("finalize-preinit export not found"))?;
793
794    // Call it (no arguments, no return value)
795    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/// Errors that can occur during pre-initialization.
807#[derive(Debug, Clone)]
808#[non_exhaustive]
809pub enum PreInitError {
810    /// Failed to create wasmtime engine.
811    Engine(String),
812    /// Failed to compile component.
813    Compile(String),
814    /// Failed to instantiate component.
815    Instantiate(String),
816    /// Python initialization failed.
817    PythonInit(String),
818    /// Import failed during pre-init.
819    Import(String),
820    /// Component transform failed.
821    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}