Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
23 changes: 23 additions & 0 deletions crates/herkos-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"]
Comment thread
arnoox marked this conversation as resolved.

[dependencies]
wasmparser = { workspace = true }
anyhow = { workspace = true }
heck = { workspace = true }

[dev-dependencies]
wat = { workspace = true }
File renamed without changes.
126 changes: 126 additions & 0 deletions crates/herkos-core/src/c_ffi.rs
Original file line number Diff line number Diff line change
@@ -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(),
}
}
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions crates/herkos/src/lib.rs → crates/herkos-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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();
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion crates/herkos-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion crates/herkos-tests/build.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down
27 changes: 26 additions & 1 deletion crates/herkos-tests/data/wat/conversions.wat
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)))
59 changes: 59 additions & 0 deletions crates/herkos-tests/tests/numeric_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
3 changes: 1 addition & 2 deletions crates/herkos/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
2 changes: 1 addition & 1 deletion crates/herkos/src/main.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
2 changes: 1 addition & 1 deletion crates/herkos/tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
Expand Down
Loading