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:
17
libsql-sqlite3/ext/libsql-wasi/src/error.rs
Normal file
17
libsql-sqlite3/ext/libsql-wasi/src/error.rs
Normal 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),
|
||||
}
|
36
libsql-sqlite3/ext/libsql-wasi/src/lib.rs
Normal file
36
libsql-sqlite3/ext/libsql-wasi/src/lib.rs
Normal 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))
|
||||
}
|
51
libsql-sqlite3/ext/libsql-wasi/src/main.rs
Normal file
51
libsql-sqlite3/ext/libsql-wasi/src/main.rs
Normal 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(())
|
||||
}
|
67
libsql-sqlite3/ext/libsql-wasi/src/memory.rs
Normal file
67
libsql-sqlite3/ext/libsql-wasi/src/memory.rs
Normal 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())
|
||||
}
|
319
libsql-sqlite3/ext/libsql-wasi/src/vfs.rs
Normal file
319
libsql-sqlite3/ext/libsql-wasi/src/vfs.rs
Normal 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(())
|
||||
}
|
Reference in New Issue
Block a user