Skip to content

Commit

Permalink
wasi: add WASI demo stub
Browse files Browse the repository at this point in the history
It's far from working, but it already properly calls back
from a WebAssembly instance to the host for VFS operations.
We also want the same for virtual WAL and we'll have a solid foundation
for running libSQL in a Wasm runtime.
  • Loading branch information
psarna committed Nov 24, 2023
1 parent f2511f7 commit 9df7fb2
Show file tree
Hide file tree
Showing 4 changed files with 412 additions and 1 deletion.
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ members = [
"xtask",
]

exclude = [ "./libsql-sqlite3/ext/crr" ]
exclude = [
"./libsql-sqlite3/ext/crr",
"./libsql-sqlite3/ext/libsql-wasi-demo",
]

[profile.release]
codegen-units = 1
Expand Down
11 changes: 11 additions & 0 deletions libsql-sqlite3/ext/libsql-wasi-demo/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "libsql-wasi-demo"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0.75"
wasmtime = "15.0.0"
wasmtime-wasi = "15.0.0"
330 changes: 330 additions & 0 deletions libsql-sqlite3/ext/libsql-wasi-demo/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
mod memory;

use anyhow::Context;
use wasmtime::{Caller, Engine, Linker, Memory, Module, Store};
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder};

// FIXME: add any state we need to pass - WasiCtx is here to get free println! and stuff
type State = WasiCtx;

fn get_memory(caller: &mut Caller<'_, State>) -> Memory {
caller.get_export("memory").unwrap().into_memory().unwrap()
}

fn host_open_fd(
mut caller: Caller<'_, State>,
name: i32,
flags: i32,
) -> anyhow::Result<i64> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);

let name = memory::read_cstr(memory, name)?;

println!("HOST OPEN_FD CALLED: {name:?} {flags:0o}");

let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(name)?;
let file = Box::new(file);

Ok(Box::into_raw(file) as i64)
}

fn host_delete(
mut caller: Caller<'_, State>,
vfs: i32,
name: i32,
sync_dir: i32,
) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);

println!("HOST DELETE CALLED");
Ok(0)
}

fn host_access(
mut caller: Caller<'_, State>,
vfs: i32,
name: i32,
flags: i32,
res_out: i32,
) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);

println!("HOST ACCESS CALLED");
Ok(0)
}

fn host_full_pathname(
mut caller: Caller<'_, State>,
vfs: i32,
name: i32,
n_out: i32,
out: i32,
) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);

let name = memory::read_cstr(memory, name)?;
let out = memory::slice_mut(memory, out, n_out as usize)?;

out[..name.len()].copy_from_slice(name.as_bytes());
Ok(0)
}

fn host_randomness(
mut caller: Caller<'_, State>,
vfs: i32,
n_byte: i32,
out: i32,
) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);

println!("HOST RANDOMNESS CALLED");
Ok(0)
}

fn host_sleep(mut caller: Caller<'_, State>, vfs: i32, microseconds: i32) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);

println!("HOST SLEEP CALLED");
Ok(0)
}

fn host_current_time(mut caller: Caller<'_, State>, vfs: i32, out: i32) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);

println!("HOST CURRENT TIME CALLED");
Ok(0)
}

fn host_get_last_error(
mut caller: Caller<'_, State>,
vfs: i32,
i: i32,
out: i32,
) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);

println!("HOST GET LAST ERROR CALLED");
Ok(0)
}

fn host_current_time_64(mut caller: Caller<'_, State>, vfs: i32, out: i32) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);

println!("HOST CURRENT TIME 64 CALLED");
Ok(0)
}

/*
Based on C:
LIBSQL_IMPORT("close") int libsql_wasi_close(sqlite3_file*);
LIBSQL_IMPORT("read") int libsql_wasi_read(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst);
LIBSQL_IMPORT("write") int libsql_wasi_write(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst);
LIBSQL_IMPORT("truncate") int libsql_wasi_truncate(sqlite3_file*, sqlite3_int64 size);
LIBSQL_IMPORT("sync") int libsql_wasi_sync(sqlite3_file*, int flags);
LIBSQL_IMPORT("file_size") int libsql_wasi_file_size(sqlite3_file*, sqlite3_int64 *pSize);
LIBSQL_IMPORT("lock") int libsql_wasi_lock(sqlite3_file*, int);
LIBSQL_IMPORT("unlock") int libsql_wasi_unlock(sqlite3_file*, int);
LIBSQL_IMPORT("check_reserved_lock") int libsql_wasi_check_reserved_lock(sqlite3_file*, int *pResOut);
LIBSQL_IMPORT("file_control") int libslq_wasi_file_control(sqlite3_file*, int op, void *pArg);
LIBSQL_IMPORT("sector_size") int libsql_wasi_sector_size(sqlite3_file*);
LIBSQL_IMPORT("device_characteristics") int libsql_wasi_device_characteristics(sqlite3_file*);
*/

fn host_close(mut caller: Caller<'_, State>, file: i32) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);
// TODO: read the file pointer from guest memory and feed it to Box::from_raw
println!("HOST CLOSE CALLED");

Ok(0)
}

fn host_read(
mut caller: Caller<'_, State>,
file: i32,
buf: i32,
amt: i32,
offset: i64,
) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);

println!("HOST READ CALLED");
Ok(0)
}

fn host_write(
mut caller: Caller<'_, State>,
file: i32,
buf: i32,
amt: i32,
offset: i64,
) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);

println!("HOST WRITE CALLED");
Ok(0)
}

fn host_truncate(mut caller: Caller<'_, State>, file: i32, size: i64) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);
println!("HOST TRUNCATE CALLED");
Ok(0)
}

fn host_sync(mut caller: Caller<'_, State>, file: i32, flags: i32) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);
println!("HOST SYNC CALLED");
Ok(0)
}

fn host_file_size(mut caller: Caller<'_, State>, file: i32, size: i32) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);
println!("HOST FILE SIZE CALLED");
Ok(0)
}

fn host_lock(mut caller: Caller<'_, State>, file: i32, lock: i32) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);
println!("HOST LOCK CALLED");
Ok(0)
}

fn host_unlock(mut caller: Caller<'_, State>, file: i32, lock: i32) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);
println!("HOST UNLOCK CALLED");
Ok(0)
}

fn host_check_reserved_lock(
mut caller: Caller<'_, State>,
file: i32,
reserved_lock: i32,
) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);
println!("HOST CHECK RESERVED LOCK CALLED");
Ok(0)
}

fn host_file_control(
mut caller: Caller<'_, State>,
file: i32,
op: i32,
arg: i32,
) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);

println!("HOST FILE CONTROL CALLED");
Ok(0)
}

fn host_sector_size(mut caller: Caller<'_, State>, file: i32) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);

println!("HOST SECTOR SIZE CALLED");
Ok(0)
}

fn host_device_characteristics(mut caller: Caller<'_, State>, file: i32) -> anyhow::Result<i32> {
let memory = get_memory(&mut caller);
let (memory, state) = memory.data_and_store_mut(&mut caller);

println!("HOST DEVICE CHARACTERISTICS CALLED");
Ok(0)
}

fn link_host_functions(linker: &mut Linker<State>) -> anyhow::Result<()> {
linker.func_wrap("libsql_host", "open_fd", host_open_fd)?;
linker.func_wrap("libsql_host", "delete", host_delete)?;
linker.func_wrap("libsql_host", "access", host_access)?;
linker.func_wrap("libsql_host", "full_pathname", host_full_pathname)?;
linker.func_wrap("libsql_host", "randomness", host_randomness)?;
linker.func_wrap("libsql_host", "sleep", host_sleep)?;
linker.func_wrap("libsql_host", "current_time", host_current_time)?;
linker.func_wrap("libsql_host", "get_last_error", host_get_last_error)?;
linker.func_wrap("libsql_host", "current_time_64", host_current_time_64)?;

linker.func_wrap("libsql_host", "close", host_close)?;
linker.func_wrap("libsql_host", "read", host_read)?;
linker.func_wrap("libsql_host", "write", host_write)?;
linker.func_wrap("libsql_host", "truncate", host_truncate)?;
linker.func_wrap("libsql_host", "sync", host_sync)?;
linker.func_wrap("libsql_host", "file_size", host_file_size)?;
linker.func_wrap("libsql_host", "lock", host_lock)?;
linker.func_wrap("libsql_host", "unlock", host_unlock)?;
linker.func_wrap("libsql_host", "check_reserved_lock", host_check_reserved_lock)?;
linker.func_wrap("libsql_host", "file_control", host_file_control)?;
linker.func_wrap("libsql_host", "sector_size", host_sector_size)?;
linker.func_wrap("libsql_host", "device_characteristics", host_device_characteristics)?;
Ok(())
}

fn main() -> anyhow::Result<()> {
let engine = Engine::default();

let libsql_module = Module::from_file(&engine, "../../libsql.wasm")?;

let mut linker = Linker::new(&engine);
link_host_functions(&mut linker)?;
wasmtime_wasi::add_to_linker(&mut linker, |s| s)?;

std::fs::create_dir("/tmp/wasm-demo").ok();
// FIXME: we might as well not need it with VFS and virtual WAL, it's here to make stuff easier for debugging
let wasi = WasiCtxBuilder::new()
.inherit_stdio()
.inherit_args()?
.build();

let mut store = Store::new(&engine, wasi);
let instance = linker.instantiate(&mut store, &libsql_module)?;

let malloc = instance.get_typed_func::<i32, i32>(&mut store, "malloc")?;
let free = instance.get_typed_func::<i32, ()>(&mut store, "free")?;

let memory = instance
.get_memory(&mut store, "memory")
.context("memory export not found")?;

let db_path = malloc.call(&mut store, 16)?;
memory.write(&mut store, db_path as usize, b"/tmp/wasm-demo.db\0")?;

let libsql_wasi_init = instance.get_typed_func::<(), ()>(&mut store, "libsql_wasi_init")?;
let open_func = instance.get_typed_func::<i32, i32>(&mut store, "libsql_wasi_open_db")?;
let exec_func = instance.get_typed_func::<(i32, i32), i32>(&mut store, "libsql_wasi_exec")?;
let close_func = instance.get_typed_func::<i32, i32>(&mut store, "sqlite3_close")?;

libsql_wasi_init.call(&mut store, ())?;
let db = open_func.call(&mut store, db_path)?;
let sql = malloc.call(&mut store, 64)?;
memory.write(&mut store, sql as usize, b"PRAGMA journal_mode=WAL;\0")?;
let rc = exec_func.call(&mut store, (db, sql))?;
let _ = free.call(&mut store, sql)?;
let _ = close_func.call(&mut store, db)?;
let _ = free.call(&mut store, db_path)?;

println!("rc: {rc}");

Ok(())
}
Loading

0 comments on commit 9df7fb2

Please sign in to comment.