use std::fs;
pub(crate) mod bytecode;
pub(crate) mod exports;
pub(crate) mod transform;
pub(crate) mod js;
pub(crate) mod plugin;
pub(crate) mod wit;
use crate::exports::Exports;
pub use crate::js::JS;
pub use crate::plugin::Plugin;
pub use crate::wit::WitOptions;
use transform::SourceCodeSection;
use walrus::{
DataId, DataKind, ExportItem, FunctionBuilder, FunctionId, LocalId, MemoryId, Module, ValType,
};
use wasm_opt::{OptimizationOptions, ShrinkLevel};
use wasmtime::{Engine, Linker, Store};
use wasmtime_wasi::{WasiCtxBuilder, p2::pipe::MemoryInputPipe};
use anyhow::Result;
use wasmtime_wizer::Wizer;
#[derive(Debug, Clone, Default)]
pub enum LinkingKind {
#[default]
Static,
Dynamic,
}
#[derive(Debug, Clone, Default)]
pub enum SourceEmbedding {
#[default]
Uncompressed,
Compressed,
Omitted,
}
#[derive(Debug)]
pub(crate) struct Identifiers {
cabi_realloc: FunctionId,
invoke: FunctionId,
memory: MemoryId,
}
impl Identifiers {
fn new(cabi_realloc: FunctionId, invoke: FunctionId, memory: MemoryId) -> Self {
Self {
cabi_realloc,
invoke,
memory,
}
}
}
#[derive(Debug)]
pub(crate) struct BytecodeMetadata {
ptr: LocalId,
len: i32,
data_section: DataId,
}
impl BytecodeMetadata {
fn new(ptr: LocalId, len: i32, data_section: DataId) -> Self {
Self {
ptr,
len,
data_section,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct Generator {
pub(crate) plugin: Plugin,
pub(crate) linking: LinkingKind,
pub(crate) source_embedding: SourceEmbedding,
pub(crate) wit_opts: WitOptions,
pub(crate) function_exports: Exports,
js_runtime_config: Vec<u8>,
producer_version: Option<String>,
deterministic: bool,
}
impl Generator {
pub fn new(plugin: Plugin) -> Self {
Self {
plugin,
..Self::default()
}
}
pub fn linking(&mut self, linking: LinkingKind) -> &mut Self {
self.linking = linking;
self
}
pub fn source_embedding(&mut self, source_embedding: SourceEmbedding) -> &mut Self {
self.source_embedding = source_embedding;
self
}
pub fn wit_opts(&mut self, wit_opts: wit::WitOptions) -> &mut Self {
self.wit_opts = wit_opts;
self
}
#[cfg(feature = "plugin_internal")]
pub fn js_runtime_config(&mut self, js_runtime_config: Vec<u8>) -> &mut Self {
self.js_runtime_config = js_runtime_config;
self
}
pub fn producer_version(&mut self, producer_version: String) -> &mut Self {
self.producer_version = Some(producer_version);
self
}
pub fn deterministic(&mut self, deterministic: bool) -> &mut Self {
self.deterministic = deterministic;
self
}
}
impl Generator {
async fn generate_initial_module(&self) -> Result<Module> {
let config = transform::module_config();
let module = match &self.linking {
LinkingKind::Static => {
let engine = Engine::default();
let mut builder = WasiCtxBuilder::new();
builder
.stdin(MemoryInputPipe::new(self.js_runtime_config.clone()))
.inherit_stdout()
.inherit_stderr();
if self.deterministic {
deterministic_wasi_ctx::add_determinism_to_wasi_ctx_builder(&mut builder);
}
let wasi = builder.build_p1();
let mut store = Store::new(&engine, wasi);
let wasm = Wizer::new()
.init_func("initialize-runtime")
.run(&mut store, self.plugin.as_bytes(), async |store, module| {
let engine = store.engine();
let mut linker = Linker::new(engine);
wasmtime_wasi::p1::add_to_linker_async(&mut linker, |cx| cx)?;
linker.define_unknown_imports_as_traps(module)?;
let instance = linker.instantiate_async(store, module).await?;
Ok(instance)
})
.await?;
config.parse(&wasm)?
}
LinkingKind::Dynamic => Module::with_config(config),
};
Ok(module)
}
pub(crate) fn resolve_identifiers(&self, module: &mut Module) -> Result<Identifiers> {
match self.linking {
LinkingKind::Static => {
let cabi_realloc = module.exports.get_func("cabi_realloc")?;
let invoke = module.exports.get_func("invoke")?;
let ExportItem::Memory(memory) = module
.exports
.iter()
.find(|e| e.name == "memory")
.ok_or_else(|| anyhow::anyhow!("Missing memory export"))?
.item
else {
anyhow::bail!("Export with name memory must be of type memory")
};
Ok(Identifiers::new(cabi_realloc, invoke, memory))
}
LinkingKind::Dynamic => {
let import_namespace = self.plugin.import_namespace()?;
let cabi_realloc_type = module.types.add(
&[ValType::I32, ValType::I32, ValType::I32, ValType::I32],
&[ValType::I32],
);
let (cabi_realloc_fn_id, _) =
module.add_import_func(&import_namespace, "cabi_realloc", cabi_realloc_type);
let invoke_params = [
ValType::I32,
ValType::I32,
ValType::I32,
ValType::I32,
ValType::I32,
]
.as_slice();
let invoke_type = module.types.add(invoke_params, &[]);
let (invoke_fn_id, _) =
module.add_import_func(&import_namespace, "invoke", invoke_type);
let (memory_id, _) = module.add_import_memory(
&import_namespace,
"memory",
false,
false,
0,
None,
None,
);
Ok(Identifiers::new(
cabi_realloc_fn_id,
invoke_fn_id,
memory_id,
))
}
}
}
fn generate_main(
&self,
module: &mut Module,
js: &js::JS,
imports: &Identifiers,
) -> Result<BytecodeMetadata> {
let bytecode = bytecode::compile_source(&self.plugin, js.as_bytes())?;
let bytecode_len: i32 = bytecode.len().try_into()?;
let bytecode_data = module.data.add(DataKind::Passive, bytecode);
let mut main = FunctionBuilder::new(&mut module.types, &[], &[]);
let bytecode_ptr_local = module.locals.add(ValType::I32);
let mut instructions = main.func_body();
instructions
.i32_const(0) .i32_const(0) .i32_const(1) .i32_const(bytecode_len) .call(imports.cabi_realloc)
.local_tee(bytecode_ptr_local) .i32_const(0) .i32_const(bytecode_len) .memory_init(imports.memory, bytecode_data);
instructions
.local_get(bytecode_ptr_local) .i32_const(bytecode_len)
.i32_const(0) .i32_const(0) .i32_const(0) .call(imports.invoke);
let main = main.finish(vec![], &mut module.funcs);
module.exports.add("_start", main);
Ok(BytecodeMetadata::new(
bytecode_ptr_local,
bytecode_len,
bytecode_data,
))
}
fn generate_exports(
&self,
module: &mut Module,
identifiers: &Identifiers,
bc_metadata: &BytecodeMetadata,
) -> Result<()> {
if !self.function_exports.is_empty() {
let fn_name_ptr_local = module.locals.add(ValType::I32);
for export in &self.function_exports {
let js_export_bytes = export.js.as_bytes();
let js_export_len: i32 = js_export_bytes.len().try_into().unwrap();
let fn_name_data = module.data.add(DataKind::Passive, js_export_bytes.to_vec());
let mut export_fn = FunctionBuilder::new(&mut module.types, &[], &[]);
export_fn
.func_body()
.i32_const(0) .i32_const(0) .i32_const(1) .i32_const(bc_metadata.len) .call(identifiers.cabi_realloc)
.local_tee(bc_metadata.ptr)
.i32_const(0) .i32_const(bc_metadata.len) .memory_init(identifiers.memory, bc_metadata.data_section) .data_drop(bc_metadata.data_section)
.i32_const(0) .i32_const(0) .i32_const(1) .i32_const(js_export_len) .call(identifiers.cabi_realloc)
.local_tee(fn_name_ptr_local)
.i32_const(0) .i32_const(js_export_len) .memory_init(identifiers.memory, fn_name_data) .data_drop(fn_name_data)
.local_get(bc_metadata.ptr)
.i32_const(bc_metadata.len)
.i32_const(1) .local_get(fn_name_ptr_local)
.i32_const(js_export_len)
.call(identifiers.invoke);
let export_fn = export_fn.finish(vec![], &mut module.funcs);
module.exports.add(&export.wit, export_fn);
}
}
Ok(())
}
fn postprocess(&self, module: &mut Module) -> Result<Vec<u8>> {
match self.linking {
LinkingKind::Static => {
module.exports.remove("invoke")?;
module.exports.remove("compile-src")?;
let tempdir = tempfile::tempdir()?;
let tempfile_path = tempdir.path().join("temp.wasm");
module.emit_wasm_file(&tempfile_path)?;
OptimizationOptions::new_opt_level_3() .shrink_level(ShrinkLevel::Level0) .debug_info(false)
.run(&tempfile_path, &tempfile_path)?;
Ok(fs::read(&tempfile_path)?)
}
LinkingKind::Dynamic => Ok(module.emit_wasm()),
}
}
pub async fn generate(&mut self, js: &js::JS) -> Result<Vec<u8>> {
if self.wit_opts.defined() {
self.function_exports = exports::process_exports(
js,
self.wit_opts.unwrap_path(),
self.wit_opts.unwrap_world(),
)?;
}
let mut module = self.generate_initial_module().await?;
let identifiers = self.resolve_identifiers(&mut module)?;
let bc_metadata = self.generate_main(&mut module, js, &identifiers)?;
self.generate_exports(&mut module, &identifiers, &bc_metadata)?;
transform::add_producers_section(
&mut module.producers,
self.producer_version
.as_deref()
.unwrap_or(env!("CARGO_PKG_VERSION")),
);
match self.source_embedding {
SourceEmbedding::Omitted => {}
SourceEmbedding::Uncompressed => {
module.customs.add(SourceCodeSection::uncompressed(js)?);
}
SourceEmbedding::Compressed => {
module.customs.add(SourceCodeSection::compressed(js)?);
}
}
let wasm = self.postprocess(&mut module)?;
Ok(wasm)
}
}