diff --git a/Cargo.lock b/Cargo.lock index 9963e89..ac1ca02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -358,6 +358,15 @@ version = "0.1.1" dependencies = [ "anyhow", "clap", + "herkos-core", + "wat", +] + +[[package]] +name = "herkos-core" +version = "0.1.1" +dependencies = [ + "anyhow", "heck", "wasmparser 0.227.1", "wat", @@ -376,7 +385,7 @@ version = "0.1.0" dependencies = [ "anyhow", "criterion", - "herkos", + "herkos-core", "herkos-runtime", "wat", ] diff --git a/Cargo.toml b/Cargo.toml index f44aa1f..40baed4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,12 @@ [workspace] resolver = "2" -members = ["crates/herkos-runtime", "crates/herkos", "crates/herkos-tests"] +members = ["crates/herkos-runtime", "crates/herkos-core", "crates/herkos", "crates/herkos-tests"] -exclude = ["examples/c-to-wasm-to-rust", "examples/inter-module-lending"] +exclude = [ + "examples/c-to-wasm-to-rust", + "examples/inter-module-lending", + "examples/herkos-bootstrap", +] [workspace.dependencies] anyhow = "1" diff --git a/crates/herkos-core/Cargo.toml b/crates/herkos-core/Cargo.toml new file mode 100644 index 0000000..ea1d2f2 --- /dev/null +++ b/crates/herkos-core/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "herkos-core" +version = "0.1.1" +edition = "2021" +description = "Compile-Time Memory Isolation via WebAssembly and Rust Transpilation — core library" +license = "Apache-2.0" +repository = "https://github.com/arnoox/herkos" +homepage = "https://github.com/arnoox/herkos" +authors = ["Arnaud Riess"] +keywords = ["webassembly", "wasm", "transpiler", "rust", "isolation"] +categories = ["wasm", "development-tools::build-utils"] +readme = "../../README.md" + +[lib] +crate-type = ["rlib", "cdylib"] + +[dependencies] +wasmparser = { workspace = true } +anyhow = { workspace = true } +heck = { workspace = true } + +[dev-dependencies] +wat = { workspace = true } diff --git a/crates/herkos/src/backend/mod.rs b/crates/herkos-core/src/backend/mod.rs similarity index 100% rename from crates/herkos/src/backend/mod.rs rename to crates/herkos-core/src/backend/mod.rs diff --git a/crates/herkos/src/backend/safe.rs b/crates/herkos-core/src/backend/safe.rs similarity index 100% rename from crates/herkos/src/backend/safe.rs rename to crates/herkos-core/src/backend/safe.rs diff --git a/crates/herkos-core/src/c_ffi.rs b/crates/herkos-core/src/c_ffi.rs new file mode 100644 index 0000000..192cfdd --- /dev/null +++ b/crates/herkos-core/src/c_ffi.rs @@ -0,0 +1,126 @@ +//! C FFI interface for the herkos transpiler. +//! +//! This module exports C-compatible functions for transpiling WebAssembly binaries. +//! The caller is responsible for freeing allocated memory using `herkos_free`. + +use std::os::raw::{c_char, c_int, c_uchar}; +use std::ptr; +use std::slice; + +use crate::{transpile, TranspileOptions}; + +/// Result structure for C FFI calls +#[repr(C)] +pub struct HerkosResult { + /// Pointer to the output Rust string (must be freed with herkos_free) + pub output: *mut c_char, + /// Length of the output string in bytes (not including null terminator) + pub output_len: usize, + /// Error code: 0 on success, non-zero on failure + pub error_code: c_int, + /// Error message (null if no error) + pub error_msg: *mut c_char, +} + +/// Error codes +const HERKOS_OK: c_int = 0; +const HERKOS_ERROR_TRANSPILE: c_int = 1; +const HERKOS_ERROR_INVALID_INPUT: c_int = 2; + +/// Transpile WebAssembly bytes to Rust source code using default options. +/// +/// # Arguments +/// * `wasm_bytes` - Pointer to the WebAssembly binary data +/// * `wasm_len` - Length of the WebAssembly binary in bytes +/// +/// # Returns +/// A `HerkosResult` struct containing: +/// - `output`: Pointer to allocated Rust source code (must be freed with `herkos_free`) +/// - `output_len`: Length of the output string in bytes +/// - `error_code`: 0 on success, non-zero on error +/// - `error_msg`: Null on success, error message string on failure +/// +/// # Safety +/// - The caller must ensure `wasm_bytes` points to valid memory of at least `wasm_len` bytes +/// - The returned `output` pointer must be freed using `herkos_free` +/// - The returned `error_msg` pointer (if not null) must be freed using `herkos_free` +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn herkos_transpile(wasm_bytes: *const c_uchar, wasm_len: usize) -> HerkosResult { + // Validate input pointers + if wasm_bytes.is_null() { + let msg = "wasm_bytes pointer is null"; + return HerkosResult { + output: ptr::null_mut(), + output_len: 0, + error_code: HERKOS_ERROR_INVALID_INPUT, + error_msg: allocate_string(msg), + }; + } + + if wasm_len == 0 { + let msg = "wasm_len is 0"; + return HerkosResult { + output: ptr::null_mut(), + output_len: 0, + error_code: HERKOS_ERROR_INVALID_INPUT, + error_msg: allocate_string(msg), + }; + } + + // Convert raw pointer to safe slice + let wasm_slice = unsafe { slice::from_raw_parts(wasm_bytes, wasm_len) }; + + // Use default transpilation options + let options = TranspileOptions::default(); + + // Perform transpilation + match transpile(wasm_slice, &options) { + Ok(rust_code) => { + let output = allocate_string(&rust_code); + let output_len = rust_code.len(); + HerkosResult { + output, + output_len, + error_code: HERKOS_OK, + error_msg: ptr::null_mut(), + } + } + Err(e) => { + let error_msg = format!("{:#}", e); + HerkosResult { + output: ptr::null_mut(), + output_len: 0, + error_code: HERKOS_ERROR_TRANSPILE, + error_msg: allocate_string(&error_msg), + } + } + } +} + +/// Free memory allocated by herkos functions. +/// +/// # Arguments +/// * `ptr` - Pointer to memory allocated by herkos (e.g., output from `herkos_transpile`) +/// +/// # Safety +/// - The pointer must have been returned by a herkos function +/// - The pointer must not have been freed already +/// - After calling this function, the pointer must not be used +#[allow(clippy::not_unsafe_ptr_arg_deref)] +#[no_mangle] +pub extern "C" fn herkos_free(ptr: *mut c_char) { + if !ptr.is_null() { + unsafe { + let _ = std::ffi::CString::from_raw(ptr); + } + } +} + +/// Helper function to allocate a Rust string as C-compatible memory +fn allocate_string(s: &str) -> *mut c_char { + match std::ffi::CString::new(s) { + Ok(cstring) => cstring.into_raw(), + Err(_) => ptr::null_mut(), + } +} diff --git a/crates/herkos/src/codegen/constructor.rs b/crates/herkos-core/src/codegen/constructor.rs similarity index 100% rename from crates/herkos/src/codegen/constructor.rs rename to crates/herkos-core/src/codegen/constructor.rs diff --git a/crates/herkos/src/codegen/export.rs b/crates/herkos-core/src/codegen/export.rs similarity index 100% rename from crates/herkos/src/codegen/export.rs rename to crates/herkos-core/src/codegen/export.rs diff --git a/crates/herkos/src/codegen/function.rs b/crates/herkos-core/src/codegen/function.rs similarity index 100% rename from crates/herkos/src/codegen/function.rs rename to crates/herkos-core/src/codegen/function.rs diff --git a/crates/herkos/src/codegen/instruction.rs b/crates/herkos-core/src/codegen/instruction.rs similarity index 100% rename from crates/herkos/src/codegen/instruction.rs rename to crates/herkos-core/src/codegen/instruction.rs diff --git a/crates/herkos/src/codegen/mod.rs b/crates/herkos-core/src/codegen/mod.rs similarity index 100% rename from crates/herkos/src/codegen/mod.rs rename to crates/herkos-core/src/codegen/mod.rs diff --git a/crates/herkos/src/codegen/module.rs b/crates/herkos-core/src/codegen/module.rs similarity index 100% rename from crates/herkos/src/codegen/module.rs rename to crates/herkos-core/src/codegen/module.rs diff --git a/crates/herkos/src/codegen/traits.rs b/crates/herkos-core/src/codegen/traits.rs similarity index 100% rename from crates/herkos/src/codegen/traits.rs rename to crates/herkos-core/src/codegen/traits.rs diff --git a/crates/herkos/src/codegen/types.rs b/crates/herkos-core/src/codegen/types.rs similarity index 100% rename from crates/herkos/src/codegen/types.rs rename to crates/herkos-core/src/codegen/types.rs diff --git a/crates/herkos/src/codegen/utils.rs b/crates/herkos-core/src/codegen/utils.rs similarity index 100% rename from crates/herkos/src/codegen/utils.rs rename to crates/herkos-core/src/codegen/utils.rs diff --git a/crates/herkos/src/ir/builder/analysis.rs b/crates/herkos-core/src/ir/builder/analysis.rs similarity index 100% rename from crates/herkos/src/ir/builder/analysis.rs rename to crates/herkos-core/src/ir/builder/analysis.rs diff --git a/crates/herkos/src/ir/builder/assembly.rs b/crates/herkos-core/src/ir/builder/assembly.rs similarity index 100% rename from crates/herkos/src/ir/builder/assembly.rs rename to crates/herkos-core/src/ir/builder/assembly.rs diff --git a/crates/herkos/src/ir/builder/core.rs b/crates/herkos-core/src/ir/builder/core.rs similarity index 100% rename from crates/herkos/src/ir/builder/core.rs rename to crates/herkos-core/src/ir/builder/core.rs diff --git a/crates/herkos/src/ir/builder/mod.rs b/crates/herkos-core/src/ir/builder/mod.rs similarity index 100% rename from crates/herkos/src/ir/builder/mod.rs rename to crates/herkos-core/src/ir/builder/mod.rs diff --git a/crates/herkos/src/ir/builder/translate.rs b/crates/herkos-core/src/ir/builder/translate.rs similarity index 100% rename from crates/herkos/src/ir/builder/translate.rs rename to crates/herkos-core/src/ir/builder/translate.rs diff --git a/crates/herkos/src/ir/lower_phis.rs b/crates/herkos-core/src/ir/lower_phis.rs similarity index 100% rename from crates/herkos/src/ir/lower_phis.rs rename to crates/herkos-core/src/ir/lower_phis.rs diff --git a/crates/herkos/src/ir/mod.rs b/crates/herkos-core/src/ir/mod.rs similarity index 100% rename from crates/herkos/src/ir/mod.rs rename to crates/herkos-core/src/ir/mod.rs diff --git a/crates/herkos/src/ir/types.rs b/crates/herkos-core/src/ir/types.rs similarity index 99% rename from crates/herkos/src/ir/types.rs rename to crates/herkos-core/src/ir/types.rs index c331e28..d5b75c4 100644 --- a/crates/herkos/src/ir/types.rs +++ b/crates/herkos-core/src/ir/types.rs @@ -581,12 +581,11 @@ pub enum UnOp { I32WrapI64, // i64 → i32 (truncate to low 32 bits) I64ExtendI32S, // i32 → i64 (sign-extend) I64ExtendI32U, // i32 → i64 (zero-extend) - // Sign-extension ops (Wasm sign-extension-ops proposal) - I32Extend8S, // sign-extend 8-bit value in i32 - I32Extend16S, // sign-extend 16-bit value in i32 - I64Extend8S, // sign-extend 8-bit value in i64 - I64Extend16S, // sign-extend 16-bit value in i64 - I64Extend32S, // sign-extend 32-bit value in i64 + I32Extend8S, // i32 → i32 (sign-extend low 8 bits) + I32Extend16S, // i32 → i32 (sign-extend low 16 bits) + I64Extend8S, // i64 → i64 (sign-extend low 8 bits) + I64Extend16S, // i64 → i64 (sign-extend low 16 bits) + I64Extend32S, // i64 → i64 (sign-extend low 32 bits) // Conversions: float → integer (trapping on NaN/overflow) I32TruncF32S, diff --git a/crates/herkos/src/lib.rs b/crates/herkos-core/src/lib.rs similarity index 96% rename from crates/herkos/src/lib.rs rename to crates/herkos-core/src/lib.rs index 18f3b6a..3c86c44 100644 --- a/crates/herkos/src/lib.rs +++ b/crates/herkos-core/src/lib.rs @@ -1,9 +1,10 @@ -//! herkos — WebAssembly to Rust transpiler. +//! herkos-core — WebAssembly to Rust transpiler. //! //! This crate provides the core transpilation pipeline that converts WebAssembly //! modules into memory-safe Rust source code. pub mod backend; +pub mod c_ffi; pub mod codegen; pub mod ir; pub mod optimizer; @@ -53,7 +54,7 @@ impl Default for TranspileOptions { /// /// # Example /// ```no_run -/// use herkos::{transpile, TranspileOptions}; +/// use herkos_core::{transpile, TranspileOptions}; /// /// let wasm_bytes = std::fs::read("input.wasm").unwrap(); /// let options = TranspileOptions::default(); diff --git a/crates/herkos/src/optimizer/dead_blocks.rs b/crates/herkos-core/src/optimizer/dead_blocks.rs similarity index 100% rename from crates/herkos/src/optimizer/dead_blocks.rs rename to crates/herkos-core/src/optimizer/dead_blocks.rs diff --git a/crates/herkos/src/optimizer/mod.rs b/crates/herkos-core/src/optimizer/mod.rs similarity index 100% rename from crates/herkos/src/optimizer/mod.rs rename to crates/herkos-core/src/optimizer/mod.rs diff --git a/crates/herkos/src/parser/mod.rs b/crates/herkos-core/src/parser/mod.rs similarity index 100% rename from crates/herkos/src/parser/mod.rs rename to crates/herkos-core/src/parser/mod.rs diff --git a/crates/herkos-tests/Cargo.toml b/crates/herkos-tests/Cargo.toml index b7ddd42..6728e31 100644 --- a/crates/herkos-tests/Cargo.toml +++ b/crates/herkos-tests/Cargo.toml @@ -13,7 +13,7 @@ herkos-runtime = { path = "../herkos-runtime" } [build-dependencies] anyhow = { workspace = true } wat = { workspace = true } -herkos = { path = "../herkos" } +herkos-core = { path = "../herkos-core" } [dev-dependencies] criterion = "0.8.2" diff --git a/crates/herkos-tests/build.rs b/crates/herkos-tests/build.rs index 045ebc0..c2fcef9 100644 --- a/crates/herkos-tests/build.rs +++ b/crates/herkos-tests/build.rs @@ -1,5 +1,5 @@ use anyhow::{Context, Result}; -use herkos::{transpile, TranspileOptions}; +use herkos_core::{transpile, TranspileOptions}; use std::env; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/herkos-tests/data/wat/conversions.wat b/crates/herkos-tests/data/wat/conversions.wat index bf4731e..8ddb18e 100644 --- a/crates/herkos-tests/data/wat/conversions.wat +++ b/crates/herkos-tests/data/wat/conversions.wat @@ -35,6 +35,26 @@ (func (param i32) (result f32) local.get 0 f32.reinterpret_i32) + ;; i32.extend8_s: sign-extend from i8 + (func (param i32) (result i32) + local.get 0 + i32.extend8_s) + ;; i32.extend16_s: sign-extend from i16 + (func (param i32) (result i32) + local.get 0 + i32.extend16_s) + ;; i64.extend8_s: sign-extend i64 from i8 + (func (param i64) (result i64) + local.get 0 + i64.extend8_s) + ;; i64.extend16_s: sign-extend i64 from i16 + (func (param i64) (result i64) + local.get 0 + i64.extend16_s) + ;; i64.extend32_s: sign-extend i64 from i32 + (func (param i64) (result i64) + local.get 0 + i64.extend32_s) (export "wrap_i64" (func 0)) (export "extend_i32_s" (func 1)) (export "extend_i32_u" (func 2)) @@ -43,4 +63,9 @@ (export "demote_f64" (func 5)) (export "promote_f32" (func 6)) (export "reinterpret_f32" (func 7)) - (export "reinterpret_i32" (func 8))) + (export "reinterpret_i32" (func 8)) + (export "extend8_s" (func 9)) + (export "extend16_s" (func 10)) + (export "i64_extend8_s" (func 11)) + (export "i64_extend16_s" (func 12)) + (export "i64_extend32_s" (func 13))) diff --git a/crates/herkos-tests/tests/numeric_ops.rs b/crates/herkos-tests/tests/numeric_ops.rs index 9b33974..ef54f54 100644 --- a/crates/herkos-tests/tests/numeric_ops.rs +++ b/crates/herkos-tests/tests/numeric_ops.rs @@ -168,6 +168,65 @@ fn test_i64_extend_i32_u() { ); } +#[test] +fn test_i32_extend8_s() { + let mut conversions_mod = conversions::new().unwrap(); + // Sign-extend from i8 to i32 + assert_eq!(conversions_mod.extend8_s(0x42).unwrap(), 0x42); + assert_eq!(conversions_mod.extend8_s(0xFF).unwrap(), -1); // 0xFF as i8 = -1 + assert_eq!(conversions_mod.extend8_s(0x7F).unwrap(), 127); // 0x7F as i8 = 127 + assert_eq!(conversions_mod.extend8_s(0x80).unwrap(), -128); // 0x80 as i8 = -128 +} + +#[test] +fn test_i32_extend16_s() { + let mut conversions_mod = conversions::new().unwrap(); + // Sign-extend from i16 to i32 + assert_eq!(conversions_mod.extend16_s(0x4242).unwrap(), 0x4242); + assert_eq!(conversions_mod.extend16_s(0xFFFF).unwrap(), -1); // 0xFFFF as i16 = -1 + assert_eq!(conversions_mod.extend16_s(0x7FFF).unwrap(), 32767); // max positive i16 + assert_eq!(conversions_mod.extend16_s(0x8000).unwrap(), -32768); // min negative i16 +} + +#[test] +fn test_i64_extend8_s() { + let mut conversions_mod = conversions::new().unwrap(); + // Sign-extend from i8 to i64 + assert_eq!(conversions_mod.i64_extend8_s(0x42).unwrap(), 0x42i64); + assert_eq!(conversions_mod.i64_extend8_s(0xFF).unwrap(), -1i64); // 0xFF as i8 = -1 + assert_eq!(conversions_mod.i64_extend8_s(0x7F).unwrap(), 127i64); + assert_eq!(conversions_mod.i64_extend8_s(0x80).unwrap(), -128i64); +} + +#[test] +fn test_i64_extend16_s() { + let mut conversions_mod = conversions::new().unwrap(); + // Sign-extend from i16 to i64 + assert_eq!(conversions_mod.i64_extend16_s(0x4242).unwrap(), 0x4242i64); + assert_eq!(conversions_mod.i64_extend16_s(0xFFFF).unwrap(), -1i64); // 0xFFFF as i16 = -1 + assert_eq!(conversions_mod.i64_extend16_s(0x7FFF).unwrap(), 32767i64); + assert_eq!(conversions_mod.i64_extend16_s(0x8000).unwrap(), -32768i64); +} + +#[test] +fn test_i64_extend32_s() { + let mut conversions_mod = conversions::new().unwrap(); + // Sign-extend from i32 to i64 + assert_eq!( + conversions_mod.i64_extend32_s(0x42424242).unwrap(), + 0x42424242i64 + ); + assert_eq!(conversions_mod.i64_extend32_s(0xFFFFFFFF).unwrap(), -1i64); // -1 as i32 + assert_eq!( + conversions_mod.i64_extend32_s(0x7FFFFFFF).unwrap(), + i32::MAX as i64 + ); + assert_eq!( + conversions_mod.i64_extend32_s(0x80000000).unwrap(), + i32::MIN as i64 + ); +} + #[test] fn test_f64_convert_i32_s() { let mut conversions_mod = conversions::new().unwrap(); diff --git a/crates/herkos/Cargo.toml b/crates/herkos/Cargo.toml index e3a4c30..6af2e93 100644 --- a/crates/herkos/Cargo.toml +++ b/crates/herkos/Cargo.toml @@ -12,10 +12,9 @@ categories = ["wasm", "development-tools::build-utils"] readme = "../../README.md" [dependencies] -wasmparser = { workspace = true } +herkos-core = { path = "../herkos-core" } anyhow = { workspace = true } clap = { workspace = true } -heck = { workspace = true } [dev-dependencies] wat = { workspace = true } diff --git a/crates/herkos/src/main.rs b/crates/herkos/src/main.rs index 6670ca6..852bb54 100644 --- a/crates/herkos/src/main.rs +++ b/crates/herkos/src/main.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; use clap::Parser; -use herkos::{transpile, TranspileOptions}; +use herkos_core::{transpile, TranspileOptions}; use std::fs; use std::path::PathBuf; diff --git a/crates/herkos/tests/e2e.rs b/crates/herkos/tests/e2e.rs index 95efdf1..87b2f28 100644 --- a/crates/herkos/tests/e2e.rs +++ b/crates/herkos/tests/e2e.rs @@ -3,7 +3,7 @@ //! These tests verify the complete pipeline: Wasm → IR → Rust source. use anyhow::{Context, Result}; -use herkos::{transpile, TranspileOptions}; +use herkos_core::{transpile, TranspileOptions}; /// Helper to transpile WAT source to Rust code. fn transpile_wat(wat_source: &str) -> Result {