0
0
mirror of https://github.com/tursodatabase/libsql.git synced 2025-07-17 19:54:57 +00:00

wasi: refactor to libsql-wasi crate

The crate still has a main.rs file for demo purposes,
but the logic is extracted to a library.
This commit is contained in:
Piotr Sarna
2023-11-27 08:22:05 +01:00
parent c24c91c2b8
commit a5cd4f3d81
8 changed files with 92 additions and 78 deletions

View File

@ -0,0 +1,17 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Wasmtime error: {0}")]
WasmtimeError(#[from] wasmtime::Error),
#[error("Memory access error: {0}")]
MemoryAccessError(#[from] wasmtime::MemoryAccessError),
#[error("WASI error: {0}")]
WasiError(#[from] wasmtime_wasi::Error),
#[error("Memory error: {0}")]
MemoryError(&'static str),
#[error("I/O Error: {0}")]
IoError(#[from] std::io::Error),
#[error("Internal Error: {0}")]
InternalError(#[from] Box<dyn std::error::Error + Sync + Send>),
#[error("Runtime error: {0}")]
RuntimeError(&'static str),
}

View File

@ -0,0 +1,36 @@
pub mod error;
pub mod memory;
mod vfs;
use wasmtime::{Engine, Instance, Linker, Module, Store};
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder};
pub use error::Error;
pub type Result<T> = std::result::Result<T, Error>;
pub type State = WasiCtx;
pub fn new_linker(engine: &Engine) -> Result<Linker<State>> {
let mut linker = Linker::new(engine);
vfs::link(&mut linker)?;
wasmtime_wasi::add_to_linker(&mut linker, |s| s)?;
Ok(linker)
}
pub fn instantiate(
linker: &Linker<State>,
libsql_wasm_path: impl AsRef<std::path::Path>,
) -> Result<(Store<State>, Instance)> {
let wasi_ctx = WasiCtxBuilder::new()
.inherit_stdio()
.inherit_args()
.map_err(|e| crate::error::Error::InternalError(Box::new(e)))?
.build();
let libsql_module = Module::from_file(linker.engine(), libsql_wasm_path.as_ref())?;
let mut store = Store::new(linker.engine(), wasi_ctx);
let instance = linker.instantiate(&mut store, &libsql_module)?;
Ok((store, instance))
}

View File

@ -0,0 +1,51 @@
use libsql_wasi::{instantiate, new_linker, Error, Result};
fn main() -> Result<()> {
tracing_subscriber::fmt::try_init().ok();
let engine = wasmtime::Engine::default();
let linker = new_linker(&engine)?;
let (mut store, instance) = instantiate(&linker, "../../libsql.wasm")?;
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")
.ok_or_else(|| Error::RuntimeError("no memory 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))?;
free.call(&mut store, sql)?;
if rc != 0 {
return Err(Error::RuntimeError("Failed to execute SQL"));
}
let sql = malloc.call(&mut store, 64)?;
memory.write(
&mut store,
sql as usize,
b"CREATE TABLE testme(id, v1, v2);\0",
)?;
let rc = exec_func.call(&mut store, (db, sql))?;
free.call(&mut store, sql)?;
let _ = close_func.call(&mut store, db)?;
free.call(&mut store, db_path)?;
println!("rc: {rc}");
Ok(())
}

View File

@ -0,0 +1,67 @@
// Shamelessly stolen from Honza - thx man!!!
use crate::{Error, Result};
pub type Ptr = i32;
pub fn slice(memory: &[u8], ptr: Ptr, len: usize) -> Result<&[u8]> {
let ptr = ptr as usize;
assert!(ptr != 0 && ptr <= memory.len(), "Invalid pointer");
assert!(ptr + len <= memory.len(), "Invalid pointer and length");
Ok(&memory[ptr..][..len])
}
pub fn slice_mut(memory: &mut [u8], ptr: Ptr, len: usize) -> Result<&mut [u8]> {
let ptr = ptr as usize;
assert!(ptr != 0 && ptr <= memory.len(), "Invalid pointer");
assert!(ptr + len <= memory.len(), "Invalid pointer and length");
Ok(&mut memory[ptr..][..len])
}
pub fn read_vec(memory: &[u8], ptr: Ptr, len: usize) -> Result<Vec<u8>> {
slice(memory, ptr, len).map(|slice| slice.to_vec())
}
pub fn read_cstr(memory: &[u8], cstr_ptr: Ptr) -> Result<String> {
let Some(data) = read_cstr_bytes(memory, cstr_ptr) else {
return Err(Error::MemoryError("Invalid pointer to C string"));
};
String::from_utf8(data).map_err(|_| Error::MemoryError("Invalid UTF-8 in C string"))
}
pub fn read_cstr_or_null(memory: &[u8], cstr_ptr: Ptr) -> Result<Option<String>> {
if cstr_ptr != 0 {
read_cstr(memory, cstr_ptr).map(Some)
} else {
Ok(None)
}
}
pub fn read_cstr_lossy(memory: &[u8], cstr_ptr: Ptr) -> String {
match read_cstr_bytes(memory, cstr_ptr) {
Some(data) => match String::from_utf8(data) {
Ok(string) => string,
Err(err) => String::from_utf8_lossy(err.as_bytes()).into_owned(),
},
None => String::new(),
}
}
pub fn read_cstr_bytes(memory: &[u8], cstr_ptr: Ptr) -> Option<Vec<u8>> {
let cstr_ptr = cstr_ptr as usize;
if cstr_ptr == 0 || cstr_ptr >= memory.len() {
return None;
}
let data = &memory[cstr_ptr..];
let mut strlen = 0;
loop {
match data.get(strlen) {
None => return None,
Some(0) => break,
Some(_) => strlen += 1,
}
}
Some(data[..strlen].to_vec())
}

View File

@ -0,0 +1,319 @@
use crate::{memory, State};
// anyhow is used in wasmtime_wasi for error wrapping
use anyhow::Result;
use wasmtime::{Caller, Linker, Memory};
const SQLITE_DATAONLY: i32 = 0x00010;
const SQLITE_IOERR_READ: i32 = 266;
const SQLITE_IOERR_SHORT_READ: i32 = 522;
const SQLITE_IOERR_WRITE: i32 = 778;
const SQLITE_ACCESS_EXISTS: i32 = 0;
const SQLITE_ACCESS_READWRITE: i32 = 1;
/* Reference from C:
typedef struct libsql_wasi_file {
const struct sqlite3_io_methods* pMethods;
int64_t fd;
} libsql_wasi_file;
#[repr(C)]
struct LibsqlWasiFile {
ptr: *mut std::ffi::c_void,
fd: i64,
}
*/
fn get_memory(caller: &mut Caller<'_, State>) -> Memory {
caller.get_export("memory").unwrap().into_memory().unwrap()
}
fn get_file(memory: &[u8], file_ptr: i32) -> &'static mut std::fs::File {
let file_fd = i64::from_le_bytes(
memory[file_ptr as usize + 8..file_ptr as usize + 8 + 8]
.try_into()
.unwrap(),
);
let file: &'static mut std::fs::File = unsafe { &mut *(file_fd as *mut std::fs::File) };
tracing::debug!("Metadata: {:?}", file.metadata());
file
}
fn open_fd(mut caller: Caller<'_, State>, name: i32, flags: i32) -> Result<i64> {
let memory = get_memory(&mut caller);
let memory = memory.data_mut(&mut caller);
let name = memory::read_cstr(memory, name)?;
tracing::debug!("Opening a file on host: {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 delete(mut caller: Caller<'_, State>, _vfs: i32, name: i32, sync_dir: i32) -> Result<i32> {
let memory = get_memory(&mut caller);
let memory = memory.data_mut(&mut caller);
let name = memory::read_cstr(memory, name)?;
tracing::debug!("HOST DELETE: {name:?}, sync_dir={sync_dir}");
let _ = std::fs::remove_file(&name);
Ok(0)
}
fn access(
mut caller: Caller<'_, State>,
_vfs: i32,
name: i32,
flags: i32,
res_out: i32,
) -> Result<i32> {
let memory = get_memory(&mut caller);
let memory = memory.data_mut(&mut caller);
let name = memory::read_cstr(memory, name)?;
tracing::debug!("HOST ACCESS: {name:?} {flags:x}");
let res_out = memory::slice_mut(memory, res_out, 4)?;
if flags == SQLITE_ACCESS_EXISTS {
if std::fs::metadata(&name).is_ok() {
res_out[0] = 1;
} else {
res_out[0] = 0;
}
} else if flags == SQLITE_ACCESS_READWRITE {
if std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&name)
.is_ok()
{
res_out[0] = 1;
} else {
res_out[0] = 0;
}
} else {
res_out[0] = 0;
}
Ok(0)
}
fn full_pathname(
mut caller: Caller<'_, State>,
_vfs: i32,
name: i32,
n_out: i32,
out: i32,
) -> Result<i32> {
let memory = get_memory(&mut caller);
let memory = memory.data_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 randomness(mut caller: Caller<'_, State>, _vfs: i32, n_byte: i32, out: i32) -> Result<i32> {
use rand::Rng;
let memory = get_memory(&mut caller);
let memory = memory.data_mut(&mut caller);
let out = memory::slice_mut(memory, out, n_byte as usize)?;
let mut rng = rand::thread_rng();
rng.fill(out);
tracing::debug!("HOST RANDOMNESS: {n_byte} {out:0x?}");
Ok(0)
}
fn sleep(mut caller: Caller<'_, State>, _vfs: i32, microseconds: i32) -> Result<i32> {
let memory = get_memory(&mut caller);
let (_memory, _state) = memory.data_and_store_mut(&mut caller);
tracing::debug!("HOST SLEEP: {microseconds}ms");
std::thread::sleep(std::time::Duration::from_micros(microseconds as u64));
Ok(0)
}
fn current_time(mut caller: Caller<'_, State>, _vfs: i32, out: i32) -> Result<i32> {
let memory = get_memory(&mut caller);
let memory = memory.data_mut(&mut caller);
tracing::debug!("HOST CURRENT TIME");
let out = memory::slice_mut(memory, out, 8)?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as f64;
out[0..8].copy_from_slice(&now.to_le_bytes());
Ok(0)
}
fn get_last_error(mut caller: Caller<'_, State>, _vfs: i32, i: i32, out: i32) -> Result<i32> {
let memory = get_memory(&mut caller);
let memory = memory.data_mut(&mut caller);
tracing::debug!("HOST GET LAST ERROR: STUB");
let out = memory::slice_mut(memory, out, i as usize)?;
out[0] = 0;
Ok(0)
}
fn current_time_64(mut caller: Caller<'_, State>, _vfs: i32, out: i32) -> Result<i32> {
let memory = get_memory(&mut caller);
let memory = memory.data_mut(&mut caller);
tracing::debug!("HOST CURRENT TIME 64");
let out = memory::slice_mut(memory, out, 8)?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
out[0..8].copy_from_slice(&now.to_le_bytes());
Ok(0)
}
fn close(mut caller: Caller<'_, State>, file: i32) -> Result<i32> {
let memory = get_memory(&mut caller);
let memory = memory.data_mut(&mut caller);
let file_fd = i64::from_le_bytes(
memory[file as usize + 8..file as usize + 8 + 8]
.try_into()
.unwrap(),
);
let _file = unsafe { Box::from_raw(file_fd as *mut std::fs::File) };
Ok(0)
}
fn read(mut caller: Caller<'_, State>, file: i32, buf: i32, amt: i32, offset: i64) -> Result<i32> {
use std::io::{Read, Seek};
let memory = get_memory(&mut caller);
let memory = memory.data_mut(&mut caller);
tracing::debug!("HOST READ CALLED: {amt} bytes starting at {offset}");
let file = get_file(memory, file);
file.seek(std::io::SeekFrom::Start(offset as u64))?;
let buf = memory::slice_mut(memory, buf, amt as usize)?;
match file.read_exact(buf) {
Ok(_) => Ok(0),
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
tracing::debug!("(short read)");
// VFS layer expects filling the buffer with zeros on short reads
buf.fill(0);
Ok(SQLITE_IOERR_SHORT_READ)
}
Err(e) => {
tracing::error!("read error: {e}");
Ok(SQLITE_IOERR_READ)
}
}
}
fn write(mut caller: Caller<'_, State>, file: i32, buf: i32, amt: i32, offset: i64) -> Result<i32> {
use std::io::{Seek, Write};
let memory = get_memory(&mut caller);
let memory = memory.data_mut(&mut caller);
tracing::debug!("HOST WRITE CALLED: {amt} bytes starting at {offset}");
let file = get_file(memory, file);
file.seek(std::io::SeekFrom::Start(offset as u64))?;
let buf = memory::slice(memory, buf, amt as usize)?;
match file.write_all(buf) {
Ok(_) => Ok(0),
Err(e) => {
tracing::error!("write error: {e}");
Ok(SQLITE_IOERR_WRITE)
}
}
}
fn truncate(mut caller: Caller<'_, State>, file: i32, size: i64) -> Result<i32> {
let memory = get_memory(&mut caller);
let memory = memory.data_mut(&mut caller);
let file = get_file(memory, file);
file.set_len(size as u64)?;
tracing::debug!("HOST TRUNCATE: {size} bytes");
Ok(0)
}
fn sync(mut caller: Caller<'_, State>, file: i32, flags: i32) -> Result<i32> {
let memory = get_memory(&mut caller);
let memory = memory.data_mut(&mut caller);
tracing::debug!("HOST SYNC: flags={flags:x}");
let file = get_file(memory, file);
if flags & SQLITE_DATAONLY != 0 {
file.sync_data()?;
} else {
file.sync_all()?;
}
Ok(0)
}
fn file_size(mut caller: Caller<'_, State>, file: i32, size_ptr: i32) -> Result<i32> {
let memory = get_memory(&mut caller);
let memory = memory.data_mut(&mut caller);
tracing::debug!("HOST FILE SIZE");
let file = get_file(memory, file);
let file_size = file.metadata()?.len() as i64;
memory[size_ptr as usize..size_ptr as usize + 8].copy_from_slice(&file_size.to_le_bytes());
Ok(0)
}
pub fn link(linker: &mut Linker<State>) -> Result<()> {
// VFS methods required by sqlite3_vfs
linker.func_wrap("libsql_host", "open_fd", open_fd)?;
linker.func_wrap("libsql_host", "delete", delete)?;
linker.func_wrap("libsql_host", "access", access)?;
linker.func_wrap("libsql_host", "full_pathname", full_pathname)?;
linker.func_wrap("libsql_host", "randomness", randomness)?;
linker.func_wrap("libsql_host", "sleep", sleep)?;
linker.func_wrap("libsql_host", "current_time", current_time)?;
linker.func_wrap("libsql_host", "get_last_error", get_last_error)?;
linker.func_wrap("libsql_host", "current_time_64", current_time_64)?;
// IO methods required by sqlite3_io_methods
linker.func_wrap("libsql_host", "close", close)?;
linker.func_wrap("libsql_host", "read", read)?;
linker.func_wrap("libsql_host", "write", write)?;
linker.func_wrap("libsql_host", "truncate", truncate)?;
linker.func_wrap("libsql_host", "sync", sync)?;
linker.func_wrap("libsql_host", "file_size", file_size)?;
// NOTICE: locking is handled as no-ops in the VFS layer,
// it is expected to be handled by the upper layers at the moment.
Ok(())
}