Files
turso/bindings/wasm/lib.rs
Pekka Enberg a369329988 Merge 'Add OPFS support to Wasm bindings' from Elijah Morgan
Lots of cleanup still left to do. Draft PR for adding support for OPFS
for WASM build (add support for limbo in browser).
Overall explanation of the architecture: this follows the sqlite wasm
architecture for OPFS.
main <> (limbo-worker.js) limbo (VFS - opfs.js) <> opfs-sync-proxy
The main thread loads limbo-worker.js which bootstraps the opfs.js and
opfs-sync-proxy.js and then launches the limbo-wasm.js.
At that point it can be used with worker.postmessage and
worker.onmessage interactions from the main thread.
The VFS provided by opfs.js provides a sync API by offloading async
operations to opfs-sync-proxy.js. This is done through SharedArrayBuffer
and Atomic.wait() to make the actual async operations appear synchronous
for limbo.
resolves #531

Closes #594
2025-01-07 10:36:18 +02:00

416 lines
11 KiB
Rust

use js_sys::{Array, Object};
use limbo_core::{
maybe_init_database_file, BufferPool, OpenFlags, Pager, Result, WalFile, WalFileShared,
};
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use wasm_bindgen::prelude::*;
#[allow(dead_code)]
#[wasm_bindgen]
pub struct Database {
db: Arc<limbo_core::Database>,
conn: Rc<limbo_core::Connection>,
}
#[allow(clippy::arc_with_non_send_sync)]
#[wasm_bindgen]
impl Database {
#[wasm_bindgen(constructor)]
pub fn new(path: &str) -> Database {
let io: Arc<dyn limbo_core::IO> = Arc::new(PlatformIO { vfs: VFS::new() });
let file = io
.open_file(path, limbo_core::OpenFlags::Create, false)
.unwrap();
maybe_init_database_file(&file, &io).unwrap();
let page_io = Rc::new(DatabaseStorage::new(file));
let db_header = Pager::begin_open(page_io.clone()).unwrap();
// ensure db header is there
io.run_once().unwrap();
let page_size = db_header.borrow().page_size;
let wal_path = format!("{}-wal", path);
let wal_shared = WalFileShared::open_shared(&io, wal_path.as_str(), page_size).unwrap();
let buffer_pool = Rc::new(BufferPool::new(page_size as usize));
let wal = Rc::new(RefCell::new(WalFile::new(
io.clone(),
db_header.borrow().page_size as usize,
wal_shared.clone(),
buffer_pool.clone(),
)));
let db = limbo_core::Database::open(io, page_io, wal, wal_shared, buffer_pool).unwrap();
let conn = db.connect();
Database { db, conn }
}
#[wasm_bindgen]
pub fn exec(&self, _sql: &str) {
let _res = self.conn.execute(_sql).unwrap();
}
#[wasm_bindgen]
pub fn prepare(&self, _sql: &str) -> Statement {
let stmt = self.conn.prepare(_sql).unwrap();
Statement::new(RefCell::new(stmt), false)
}
}
#[wasm_bindgen]
pub struct RowIterator {
inner: RefCell<limbo_core::Statement>,
}
#[wasm_bindgen]
impl RowIterator {
fn new(inner: RefCell<limbo_core::Statement>) -> Self {
Self { inner }
}
#[wasm_bindgen]
pub fn next(&mut self) -> JsValue {
match self.inner.borrow_mut().step() {
Ok(limbo_core::StepResult::Row(row)) => {
let row_array = Array::new();
for value in row.values {
let value = to_js_value(value);
row_array.push(&value);
}
JsValue::from(row_array)
}
Ok(limbo_core::StepResult::IO) => JsValue::UNDEFINED,
Ok(limbo_core::StepResult::Done) | Ok(limbo_core::StepResult::Interrupt) => {
JsValue::UNDEFINED
}
Ok(limbo_core::StepResult::Busy) => JsValue::UNDEFINED,
Err(e) => panic!("Error: {:?}", e),
}
}
}
#[wasm_bindgen]
pub struct Statement {
inner: RefCell<limbo_core::Statement>,
raw: bool,
}
#[wasm_bindgen]
impl Statement {
fn new(inner: RefCell<limbo_core::Statement>, raw: bool) -> Self {
Self { inner, raw }
}
#[wasm_bindgen]
pub fn raw(mut self, toggle: Option<bool>) -> Self {
self.raw = toggle.unwrap_or(true);
self
}
pub fn get(&self) -> JsValue {
match self.inner.borrow_mut().step() {
Ok(limbo_core::StepResult::Row(row)) => {
let row_array = js_sys::Array::new();
for value in row.values {
let value = to_js_value(value);
row_array.push(&value);
}
JsValue::from(row_array)
}
Ok(limbo_core::StepResult::IO)
| Ok(limbo_core::StepResult::Done)
| Ok(limbo_core::StepResult::Interrupt)
| Ok(limbo_core::StepResult::Busy) => JsValue::UNDEFINED,
Err(e) => panic!("Error: {:?}", e),
}
}
pub fn all(&self) -> js_sys::Array {
let array = js_sys::Array::new();
loop {
match self.inner.borrow_mut().step() {
Ok(limbo_core::StepResult::Row(row)) => {
let row_array = js_sys::Array::new();
for value in row.values {
let value = to_js_value(value);
row_array.push(&value);
}
array.push(&row_array);
}
Ok(limbo_core::StepResult::IO) => {}
Ok(limbo_core::StepResult::Interrupt) => break,
Ok(limbo_core::StepResult::Done) => break,
Ok(limbo_core::StepResult::Busy) => break,
Err(e) => panic!("Error: {:?}", e),
}
}
array
}
#[wasm_bindgen]
pub fn iterate(self) -> JsValue {
let iterator = RowIterator::new(self.inner);
let iterator_obj = Object::new();
// Define the next method that will be called by JavaScript
let next_fn = js_sys::Function::new_with_args(
"",
"const value = this.iterator.next();
const done = value === undefined;
return {
value,
done
};",
);
js_sys::Reflect::set(&iterator_obj, &JsValue::from_str("next"), &next_fn).unwrap();
js_sys::Reflect::set(
&iterator_obj,
&JsValue::from_str("iterator"),
&JsValue::from(iterator),
)
.unwrap();
let symbol_iterator = js_sys::Function::new_no_args("return this;");
js_sys::Reflect::set(&iterator_obj, &js_sys::Symbol::iterator(), &symbol_iterator).unwrap();
JsValue::from(iterator_obj)
}
}
fn to_js_value(value: limbo_core::Value) -> JsValue {
match value {
limbo_core::Value::Null => JsValue::null(),
limbo_core::Value::Integer(i) => {
if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
JsValue::from(i as i32)
} else {
JsValue::from(i)
}
}
limbo_core::Value::Float(f) => JsValue::from(f),
limbo_core::Value::Text(t) => JsValue::from_str(t),
limbo_core::Value::Blob(b) => js_sys::Uint8Array::from(b.as_slice()).into(),
}
}
pub struct File {
vfs: VFS,
fd: i32,
}
#[allow(dead_code)]
impl File {
fn new(vfs: VFS, fd: i32) -> Self {
Self { vfs, fd }
}
}
impl limbo_core::File for File {
fn lock_file(&self, _exclusive: bool) -> Result<()> {
// TODO
Ok(())
}
fn unlock_file(&self) -> Result<()> {
// TODO
Ok(())
}
fn pread(&self, pos: usize, c: Rc<limbo_core::Completion>) -> Result<()> {
let r = match &*c {
limbo_core::Completion::Read(r) => r,
_ => unreachable!(),
};
{
let mut buf = r.buf_mut();
let buf: &mut [u8] = buf.as_mut_slice();
let nr = self.vfs.pread(self.fd, buf, pos);
assert!(nr >= 0);
}
r.complete();
Ok(())
}
fn pwrite(
&self,
pos: usize,
buffer: Rc<std::cell::RefCell<limbo_core::Buffer>>,
c: Rc<limbo_core::Completion>,
) -> Result<()> {
let w = match &*c {
limbo_core::Completion::Write(w) => w,
_ => unreachable!(),
};
let buf = buffer.borrow();
let buf: &[u8] = buf.as_slice();
self.vfs.pwrite(self.fd, buf, pos);
w.complete(buf.len() as i32);
Ok(())
}
fn sync(&self, c: Rc<limbo_core::Completion>) -> Result<()> {
self.vfs.sync(self.fd);
c.complete(0);
Ok(())
}
fn size(&self) -> Result<u64> {
Ok(self.vfs.size(self.fd))
}
}
pub struct PlatformIO {
vfs: VFS,
}
impl limbo_core::IO for PlatformIO {
fn open_file(
&self,
path: &str,
_flags: OpenFlags,
_direct: bool,
) -> Result<Rc<dyn limbo_core::File>> {
let fd = self.vfs.open(path, "a+");
Ok(Rc::new(File {
vfs: VFS::new(),
fd,
}))
}
fn run_once(&self) -> Result<()> {
Ok(())
}
fn generate_random_number(&self) -> i64 {
let random_f64 = Math_random();
(random_f64 * i64::MAX as f64) as i64
}
fn get_current_time(&self) -> String {
let date = Date::new();
date.toISOString()
}
}
#[wasm_bindgen]
extern "C" {
fn Math_random() -> f64;
}
#[wasm_bindgen]
extern "C" {
type Date;
#[wasm_bindgen(constructor)]
fn new() -> Date;
#[wasm_bindgen(method, getter)]
fn toISOString(this: &Date) -> String;
}
pub struct DatabaseStorage {
file: Rc<dyn limbo_core::File>,
}
impl DatabaseStorage {
pub fn new(file: Rc<dyn limbo_core::File>) -> Self {
Self { file }
}
}
impl limbo_core::DatabaseStorage for DatabaseStorage {
fn read_page(&self, page_idx: usize, c: Rc<limbo_core::Completion>) -> Result<()> {
let r = match c.as_ref() {
limbo_core::Completion::Read(r) => r,
_ => unreachable!(),
};
let size = r.buf().len();
assert!(page_idx > 0);
if !(512..=65536).contains(&size) || size & (size - 1) != 0 {
return Err(limbo_core::LimboError::NotADB);
}
let pos = (page_idx - 1) * size;
self.file.pread(pos, c)?;
Ok(())
}
fn write_page(
&self,
page_idx: usize,
buffer: Rc<std::cell::RefCell<limbo_core::Buffer>>,
c: Rc<limbo_core::Completion>,
) -> Result<()> {
let size = buffer.borrow().len();
let pos = (page_idx - 1) * size;
self.file.pwrite(pos, buffer, c)?;
Ok(())
}
fn sync(&self, _c: Rc<limbo_core::Completion>) -> Result<()> {
todo!()
}
}
#[cfg(all(feature = "web", feature = "nodejs"))]
compile_error!("Features 'web' and 'nodejs' cannot be enabled at the same time");
#[cfg(feature = "web")]
#[wasm_bindgen(module = "/src/web-vfs.js")]
extern "C" {
type VFS;
#[wasm_bindgen(constructor)]
fn new() -> VFS;
#[wasm_bindgen(method)]
fn open(this: &VFS, path: &str, flags: &str) -> i32;
#[wasm_bindgen(method)]
fn close(this: &VFS, fd: i32) -> bool;
#[wasm_bindgen(method)]
fn pwrite(this: &VFS, fd: i32, buffer: &[u8], offset: usize) -> i32;
#[wasm_bindgen(method)]
fn pread(this: &VFS, fd: i32, buffer: &mut [u8], offset: usize) -> i32;
#[wasm_bindgen(method)]
fn size(this: &VFS, fd: i32) -> u64;
#[wasm_bindgen(method)]
fn sync(this: &VFS, fd: i32);
}
#[cfg(feature = "nodejs")]
#[wasm_bindgen(module = "/vfs.js")]
extern "C" {
type VFS;
#[wasm_bindgen(constructor)]
fn new() -> VFS;
#[wasm_bindgen(method)]
fn open(this: &VFS, path: &str, flags: &str) -> i32;
#[wasm_bindgen(method)]
fn close(this: &VFS, fd: i32) -> bool;
#[wasm_bindgen(method)]
fn pwrite(this: &VFS, fd: i32, buffer: &[u8], offset: usize) -> i32;
#[wasm_bindgen(method)]
fn pread(this: &VFS, fd: i32, buffer: &mut [u8], offset: usize) -> i32;
#[wasm_bindgen(method)]
fn size(this: &VFS, fd: i32) -> u64;
#[wasm_bindgen(method)]
fn sync(this: &VFS, fd: i32);
}
#[wasm_bindgen(start)]
pub fn init() {
console_error_panic_hook::set_once();
}