//! Minimal WASIp1 (preview1) I/O layer for the `wasm` / `wasm-gc` targets.
//!
//! Self-contained on purpose: a wasm-only registry dependency (like miniio)
//! would break the `native` build of this same executable package, so we keep
//! just the WASI calls toml-cli needs (stdin, args, read-only file access,
//! exit) plus the tiny memory helpers they rely on.
//!
//! Memory helpers and WASI calling conventions adapted from
//! `moonbit-community/miniio` (Apache-2.0), trimmed to the read-only subset.
//! The resulting wasm imports only `wasi_snapshot_preview1`, so it runs on any
//! WASIp1 host (moonrun, wasmtime, wasmer, ...).
// --- memory helpers (inline wasm) ---
///|
pub extern "wasm" fn store32(offset : Int, value : Int) =
#|(func (param i32) (param i32) local.get 0 local.get 1 i32.store)
///|
pub extern "wasm" fn load32(offset : Int) -> Int =
#|(func (param i32) (result i32) local.get 0 i32.load)
///|
#owned(bytes)
pub extern "wasm" fn bytes2ptr(bytes : Bytes) -> Int =
#|(func (param i32) (result i32) local.get 0)
///|
pub fn malloc(size : Int) -> Int {
bytes2ptr(Bytes::make(size, b'\x00'))
}
///|
pub extern "wasm" fn free(position : Int) =
#|(func (param i32) local.get 0 call $moonbit.decref)
// --- WASI preview1 imports ---
///|
fn fd_read_ffi(fd : Int, iovs : Int, iovs_len : Int, nread : Int) -> UInt = "wasi_snapshot_preview1" "fd_read"
///|
fn fd_close_ffi(fd : Int) -> UInt = "wasi_snapshot_preview1" "fd_close"
///|
fn fd_prestat_get_ffi(fd : Int, buf : Int) -> UInt = "wasi_snapshot_preview1" "fd_prestat_get"
///|
fn path_open_ffi(
dirfd : Int,
dirflags : UInt,
path : Int,
path_len : Int,
oflags : UInt,
fs_rights_base : UInt64,
fs_rights_inheriting : UInt64,
fs_flags : UInt,
fd : Int,
) -> UInt = "wasi_snapshot_preview1" "path_open"
///|
fn args_sizes_get_ffi(argc : Int, size : Int) -> UInt = "wasi_snapshot_preview1" "args_sizes_get"
///|
fn args_get_ffi(argv : Int, argv_buf : Int) -> UInt = "wasi_snapshot_preview1" "args_get"
///|
fn exit_internal(code : Int) -> Unit = "wasi_snapshot_preview1" "proc_exit"
// --- stdin ---
///|
fn fd_read_once(fd : Int, buf : Bytes) -> Int {
let iovs = malloc(8)
defer free(iovs)
store32(iovs, bytes2ptr(buf))
store32(iovs + 4, buf.length())
let nread = malloc(4)
defer free(nread)
if fd_read_ffi(fd, iovs, 1, nread) != 0 {
return -1
}
load32(nread)
}
///|
fn read_fd_loop(fd : Int) -> Bytes {
let chunks = []
while true {
let buf = Bytes::make(4096, b'\x00')
let n = fd_read_once(fd, buf)
if n <= 0 {
break
}
chunks.push(buf[:n].to_owned())
if n < 4096 {
break
}
}
let all : Array[Byte] = []
for chunk in chunks {
for byte in chunk {
all.push(byte)
}
}
Bytes::from_array(all)
}
// --- args ---
///|
fn args_internal() -> Array[String] {
let argc_ptr = malloc(4)
defer free(argc_ptr)
let size_ptr = malloc(4)
defer free(size_ptr)
if args_sizes_get_ffi(argc_ptr, size_ptr) != 0 {
return []
}
let argc = load32(argc_ptr)
let size = load32(size_ptr)
if argc == 0 {
return []
}
let argv_ptr = malloc(argc * 4)
defer free(argv_ptr)
let argv_buf = Bytes::make(size, b'\x00')
let argv_buf_ptr = bytes2ptr(argv_buf)
if args_get_ffi(argv_ptr, argv_buf_ptr) != 0 {
return []
}
let result = []
for i in 0.. Int? {
for fd in 3..<32 {
let buf = malloc(8)
defer free(buf)
if fd_prestat_get_ffi(fd, buf) == 0 && load32(buf) == 0 {
return Some(fd)
}
}
None
}
///|
/// Read a file as UTF-8 text relative to the first preopened directory;
/// `None` on failure (e.g. no preopen, missing file, or WASI sandbox denies it).
fn read_text_file_internal(path : String) -> String? {
let dirfd = match find_preopen_dir() {
Some(fd) => fd
None => return None
}
let rel = match path.strip_prefix("/") {
Some(rest) => rest.to_owned()
None => path
}
let path_bytes = @utf8.encode(rel)
let path_ptr = bytes2ptr(path_bytes)
let fd_ptr = malloc(4)
defer free(fd_ptr)
let err = path_open_ffi(
dirfd,
0,
path_ptr,
path_bytes.length(),
0,
0x26, // FD_READ | FD_SEEK | FD_TELL
0,
0,
fd_ptr,
)
if err != 0 {
return None
}
let fd = load32(fd_ptr)
defer ignore(fd_close_ffi(fd))
Some(@utf8.decode_lossy(read_fd_loop(fd).view()))
}