Begin work on Go bindings (purego)

This commit is contained in:
PThorpe92 2025-01-21 10:21:55 -05:00
parent aded7d3896
commit 4be1f9c3cc
No known key found for this signature in database
GPG key ID: 66DB3FBACBDD05CC
12 changed files with 674 additions and 0 deletions

7
Cargo.lock generated
View file

@ -2572,6 +2572,13 @@ version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
[[package]]
name = "turso-go"
version = "0.0.13"
dependencies = [
"limbo_core",
]
[[package]]
name = "typenum"
version = "1.17.0"

View file

@ -7,6 +7,7 @@ members = [
"bindings/python",
"bindings/rust",
"bindings/wasm",
"bindings/go",
"cli",
"core",
"extensions/core",

23
bindings/go/Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[package]
name = "turso-go"
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[lib]
name = "_turso_go"
crate-type = ["cdylib"]
path = "rs_src/lib.rs"
[features]
default = ["io_uring"]
io_uring = ["limbo_core/io_uring"]
[dependencies]
limbo_core = { path = "../../core/" }
[target.'cfg(target_os = "linux")'.dependencies]
limbo_core = { path = "../../core/", features = ["io_uring"] }

18
bindings/go/cmd/main.go Normal file
View file

@ -0,0 +1,18 @@
// package main
//
// import (
// "fmt"
// )
//
// func main() {
// conn, err := lc.Open("new.db")
// if err != nil {
// panic(err)
// }
// fmt.Println("Connected to database")
// sql := "select c from t;"
// conn.Query(sql)
//
// conn.Close()
// fmt.Println("Connection closed")
// }

5
bindings/go/go.mod Normal file
View file

@ -0,0 +1,5 @@
module turso
go 1.23.4
require github.com/ebitengine/purego v0.8.2 // indirect

2
bindings/go/go.sum Normal file
View file

@ -0,0 +1,2 @@
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=

210
bindings/go/rs_src/lib.rs Normal file
View file

@ -0,0 +1,210 @@
mod statement;
mod types;
use limbo_core::{Connection, Database, LimboError, Value};
use std::{
ffi::{c_char, c_void},
rc::Rc,
str::FromStr,
sync::Arc,
};
#[no_mangle]
pub unsafe extern "C" fn db_open(path: *const c_char) -> *mut c_void {
if path.is_null() {
println!("Path is null");
return std::ptr::null_mut();
}
let path = unsafe { std::ffi::CStr::from_ptr(path) };
let path = path.to_str().unwrap();
let db_options = parse_query_str(path);
if let Ok(io) = get_io(&db_options.path) {
let db = Database::open_file(io.clone(), &db_options.path.to_string());
match db {
Ok(db) => {
println!("Opened database: {}", path);
let conn = db.connect();
return TursoConn::new(conn, io).to_ptr();
}
Err(e) => {
println!("Error opening database: {}", e);
return std::ptr::null_mut();
}
};
}
std::ptr::null_mut()
}
struct TursoConn<'a> {
conn: Rc<Connection>,
io: Arc<dyn limbo_core::IO>,
cursor_idx: usize,
cursor: Option<Vec<Value<'a>>>,
}
impl<'a> TursoConn<'_> {
fn new(conn: Rc<Connection>, io: Arc<dyn limbo_core::IO>) -> Self {
TursoConn {
conn,
io,
cursor_idx: 0,
cursor: None,
}
}
fn to_ptr(self) -> *mut c_void {
Box::into_raw(Box::new(self)) as *mut c_void
}
fn from_ptr(ptr: *mut c_void) -> &'static mut TursoConn<'a> {
if ptr.is_null() {
panic!("Null pointer");
}
unsafe { &mut *(ptr as *mut TursoConn) }
}
}
/// Close the database connection
/// # Safety
/// safely frees the connection's memory
#[no_mangle]
pub unsafe extern "C" fn db_close(db: *mut c_void) {
if !db.is_null() {
let _ = unsafe { Box::from_raw(db) };
}
}
#[allow(clippy::arc_with_non_send_sync)]
fn get_io(db_location: &DbType) -> Result<Arc<dyn limbo_core::IO>, LimboError> {
Ok(match db_location {
DbType::Memory => Arc::new(limbo_core::MemoryIO::new()?),
_ => {
#[cfg(target_family = "unix")]
if cfg!(all(target_os = "linux", feature = "io_uring")) {
Arc::new(limbo_core::UringIO::new()?)
} else {
Arc::new(limbo_core::UnixIO::new()?)
}
#[cfg(target_family = "windows")]
Arc::new(limbo_core::WindowsIO::new()?);
}
})
}
struct DbOptions {
path: DbType,
params: Parameters,
}
#[derive(Default, Debug, Clone)]
enum DbType {
File(String),
#[default]
Memory,
}
impl std::fmt::Display for DbType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DbType::File(path) => write!(f, "{}", path),
DbType::Memory => write!(f, ":memory:"),
}
}
}
#[derive(Debug, Clone, Default)]
struct Parameters {
mode: Mode,
cache: Option<Cache>,
vfs: Option<String>,
nolock: bool,
immutable: bool,
modeof: Option<String>,
}
impl FromStr for Parameters {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !s.contains('?') {
return Ok(Parameters::default());
}
let mut params = Parameters::default();
for param in s.split('?').nth(1).unwrap().split('&') {
let mut kv = param.split('=');
match kv.next() {
Some("mode") => params.mode = kv.next().unwrap().parse().unwrap(),
Some("cache") => params.cache = Some(kv.next().unwrap().parse().unwrap()),
Some("vfs") => params.vfs = Some(kv.next().unwrap().to_string()),
Some("nolock") => params.nolock = true,
Some("immutable") => params.immutable = true,
Some("modeof") => params.modeof = Some(kv.next().unwrap().to_string()),
_ => {}
}
}
Ok(params)
}
}
#[derive(Default, Debug, Clone, Copy)]
enum Cache {
Shared,
#[default]
Private,
}
impl FromStr for Cache {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"shared" => Ok(Cache::Shared),
_ => Ok(Cache::Private),
}
}
}
#[allow(clippy::enum_variant_names)]
#[derive(Default, Debug, Clone, Copy)]
enum Mode {
ReadOnly,
ReadWrite,
#[default]
ReadWriteCreate,
}
impl FromStr for Mode {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"readonly" | "ro" => Ok(Mode::ReadOnly),
"readwrite" | "rw" => Ok(Mode::ReadWrite),
"readwritecreate" | "rwc" => Ok(Mode::ReadWriteCreate),
_ => Ok(Mode::default()),
}
}
}
// At this point we don't have configurable parameters but many
// DSN's are going to have query parameters
fn parse_query_str(mut path: &str) -> DbOptions {
if path == ":memory:" {
return DbOptions {
path: DbType::Memory,
params: Parameters::default(),
};
}
if path.starts_with("sqlite://") {
path = &path[10..];
}
if path.contains('?') {
let parameters = Parameters::from_str(path).unwrap();
let path = &path[..path.find('?').unwrap()];
DbOptions {
path: DbType::File(path.to_string()),
params: parameters,
}
} else {
DbOptions {
path: DbType::File(path.to_string()),
params: Parameters::default(),
}
}
}

View file

@ -0,0 +1,160 @@
use crate::types::ResultCode;
use crate::TursoConn;
use limbo_core::{Rows, Statement, StepResult, Value};
use std::ffi::{c_char, c_void};
#[no_mangle]
pub extern "C" fn db_prepare(ctx: *mut c_void, query: *const c_char) -> *mut c_void {
if ctx.is_null() || query.is_null() {
return std::ptr::null_mut();
}
let query_str = unsafe { std::ffi::CStr::from_ptr(query) }.to_str().unwrap();
let db = TursoConn::from_ptr(ctx);
let stmt = db.conn.prepare(query_str.to_string());
match stmt {
Ok(stmt) => TursoStatement::new(stmt, db).to_ptr(),
Err(_) => std::ptr::null_mut(),
}
}
struct TursoStatement<'a> {
statement: Statement,
conn: &'a TursoConn<'a>,
}
impl<'a> TursoStatement<'a> {
fn new(statement: Statement, conn: &'a TursoConn<'a>) -> Self {
TursoStatement { statement, conn }
}
fn to_ptr(self) -> *mut c_void {
Box::into_raw(Box::new(self)) as *mut c_void
}
fn from_ptr(ptr: *mut c_void) -> &'static mut TursoStatement<'a> {
if ptr.is_null() {
panic!("Null pointer");
}
unsafe { &mut *(ptr as *mut TursoStatement) }
}
}
#[no_mangle]
pub extern "C" fn db_get_columns(ctx: *mut c_void) -> *const c_void {
if ctx.is_null() {
return std::ptr::null();
}
let stmt = TursoStatement::from_ptr(ctx);
let columns = stmt.statement.columns();
let mut column_names = Vec::new();
for column in columns {
column_names.push(column.name().to_string());
}
let c_string = std::ffi::CString::new(column_names.join(",")).unwrap();
c_string.into_raw() as *const c_void
}
struct TursoRows<'a> {
rows: Rows<'a>,
conn: &'a mut TursoConn<'a>,
}
impl<'a> TursoRows<'a> {
fn new(rows: Rows<'a>, conn: &'a mut TursoConn<'a>) -> Self {
TursoRows { rows, conn }
}
fn to_ptr(self) -> *mut c_void {
Box::into_raw(Box::new(self)) as *mut c_void
}
fn from_ptr(ptr: *mut c_void) -> &'static mut TursoRows<'a> {
if ptr.is_null() {
panic!("Null pointer");
}
unsafe { &mut *(ptr as *mut TursoRows) }
}
}
#[no_mangle]
pub extern "C" fn rows_next(ctx: *mut c_void, rows_ptr: *mut c_void) -> ResultCode {
if rows_ptr.is_null() || ctx.is_null() {
return ResultCode::Error;
}
let rows = unsafe { &mut *(rows_ptr as *mut Rows) };
let conn = TursoConn::from_ptr(ctx);
match rows.next_row() {
Ok(StepResult::Row(row)) => {
conn.cursor = Some(row.values);
ResultCode::Row
}
Ok(StepResult::Done) => {
// No more rows
ResultCode::Done
}
Ok(StepResult::IO) => {
let _ = conn.io.run_once();
ResultCode::Io
}
Ok(StepResult::Busy) => ResultCode::Busy,
Ok(StepResult::Interrupt) => ResultCode::Interrupt,
Err(_) => ResultCode::Error,
}
}
#[no_mangle]
pub extern "C" fn rows_get_value(ctx: *mut c_void, col_idx: usize) -> *const c_char {
if ctx.is_null() {
return std::ptr::null();
}
let conn = TursoConn::from_ptr(ctx);
if let Some(ref cursor) = conn.cursor {
if let Some(value) = cursor.get(col_idx) {
let c_string = std::ffi::CString::new(value.to_string()).unwrap();
return c_string.into_raw(); // Caller must free this pointer
}
}
std::ptr::null() // No data or invalid index
}
// Free the returned string
#[no_mangle]
pub extern "C" fn free_c_string(s: *mut c_char) {
if !s.is_null() {
unsafe { drop(std::ffi::CString::from_raw(s)) };
}
}
#[no_mangle]
pub extern "C" fn rows_get_string(
ctx: *mut c_void,
rows_ptr: *mut c_void,
col_idx: i32,
) -> *const c_char {
if rows_ptr.is_null() || ctx.is_null() {
return std::ptr::null();
}
let _rows = unsafe { &mut *(rows_ptr as *mut Rows) };
let conn = TursoConn::from_ptr(ctx);
if col_idx > conn.cursor_idx as i32 || conn.cursor.is_none() {
return std::ptr::null();
}
if let Some(values) = &conn.cursor {
let value = &values[col_idx as usize];
match value {
Value::Text(s) => {
return s.as_ptr() as *const i8;
}
_ => return std::ptr::null(),
}
};
std::ptr::null()
}
#[no_mangle]
pub extern "C" fn rows_close(rows_ptr: *mut c_void) {
if !rows_ptr.is_null() {
let _ = unsafe { Box::from_raw(rows_ptr as *mut Rows) };
}
}

View file

@ -0,0 +1,14 @@
#[repr(C)]
pub enum ResultCode {
Error = -1,
Ok = 0,
Row = 1,
Busy = 2,
Done = 3,
Io = 4,
Interrupt = 5,
Invalid = 6,
Null = 7,
NoMem = 8,
ReadOnly = 9,
}

79
bindings/go/stmt.go Normal file
View file

@ -0,0 +1,79 @@
package turso
import (
"database/sql/driver"
"fmt"
"io"
)
type stmt struct {
ctx uintptr
sql string
}
type rows struct {
ctx uintptr
rowsPtr uintptr
columns []string
err error
}
func (ls *stmt) Query(args []driver.Value) (driver.Rows, error) {
var dbPrepare func(uintptr, uintptr) uintptr
getExtFunc(&dbPrepare, "db_prepare")
queryPtr := toCString(ls.sql)
defer freeCString(queryPtr)
rowsPtr := dbPrepare(ls.ctx, queryPtr)
if rowsPtr == 0 {
return nil, fmt.Errorf("failed to prepare query")
}
var colFunc func(uintptr, uintptr) uintptr
getExtFunc(&colFunc, "columns")
rows := &rows{
ctx: ls.ctx,
rowsPtr: rowsPtr,
}
return rows, nil
}
func (lr *rows) Columns() []string {
return lr.columns
}
func (lr *rows) Close() error {
var rowsClose func(uintptr)
getExtFunc(&rowsClose, "rows_close")
rowsClose(lr.rowsPtr)
return nil
}
func (lr *rows) Next(dest []driver.Value) error {
var rowsNext func(uintptr, uintptr) int32
getExtFunc(&rowsNext, "rows_next")
status := rowsNext(lr.ctx, lr.rowsPtr)
switch ResultCode(status) {
case Row:
for i := range dest {
getExtFunc(&rowsGetValue, "rows_get_value")
valPtr := rowsGetValue(lr.ctx, int32(i))
if valPtr != 0 {
val := cStringToGoString(valPtr)
dest[i] = val
freeCString(valPtr)
} else {
dest[i] = nil
}
}
return nil
case 0: // No more rows
return io.EOF
default:
return fmt.Errorf("unexpected status: %d", status)
}
}

127
bindings/go/turso.go Normal file
View file

@ -0,0 +1,127 @@
package turso
import (
"database/sql"
"database/sql/driver"
"errors"
"log/slog"
"os"
"sync"
"unsafe"
"github.com/ebitengine/purego"
)
const (
turso = "../../target/debug/lib_turso_go.so"
)
func toGoStr(ptr uintptr, length int) string {
if ptr == 0 {
return ""
}
uptr := unsafe.Pointer(ptr)
s := (*string)(uptr)
if s == nil {
// redundant
return ""
}
return *s
}
func init() {
slib, err := purego.Dlopen(turso, purego.RTLD_LAZY)
if err != nil {
slog.Error("Error opening turso library: ", err)
os.Exit(1)
}
lib = slib
sql.Register("turso", &tursoDriver{})
}
type tursoDriver struct {
tursoCtx
}
func toCString(s string) uintptr {
b := append([]byte(s), 0)
return uintptr(unsafe.Pointer(&b[0]))
}
func getExtFunc(ptr interface{}, name string) {
purego.RegisterLibFunc(ptr, lib, name)
}
type conn struct {
ctx uintptr
sync.Mutex
writeTimeFmt string
lastInsertID int64
lastAffected int64
}
func newConn() *conn {
return &conn{
0,
sync.Mutex{},
"2006-01-02 15:04:05",
0,
0,
}
}
func open(dsn string) (*conn, error) {
var open func(uintptr) uintptr
getExtFunc(&open, ExtDBOpen)
c := newConn()
path := toCString(dsn)
ctx := open(path)
c.ctx = ctx
return c, nil
}
type tursoCtx struct {
conn *conn
tx *sql.Tx
err error
rows *sql.Rows
stmt *sql.Stmt
}
func (lc tursoCtx) Open(dsn string) (driver.Conn, error) {
conn, err := open(dsn)
if err != nil {
return nil, err
}
nc := tursoCtx{conn: conn}
return nc, nil
}
func (lc tursoCtx) Close() error {
var closedb func(uintptr) uintptr
getExtFunc(&closedb, ExtDBClose)
closedb(lc.conn.ctx)
return nil
}
// TODO: Begin not implemented
func (lc tursoCtx) Begin() (driver.Tx, error) {
return nil, nil
}
func (ls tursoCtx) Prepare(sql string) (driver.Stmt, error) {
var prepare func(uintptr, uintptr) uintptr
getExtFunc(&prepare, ExtDBPrepare)
s := toCString(sql)
statement := prepare(ls.conn.ctx, s)
if statement == 0 {
return nil, errors.New("no rows")
}
ls.stmt = stmt{
ctx: statement,
}
}
return nil, nil
}

28
bindings/go/types.go Normal file
View file

@ -0,0 +1,28 @@
package turso
type ResultCode int
const (
Error ResultCode = -1
Ok ResultCode = 0
Row ResultCode = 1
Busy ResultCode = 2
Done ResultCode = 3
Io ResultCode = 4
Interrupt ResultCode = 5
Invalid ResultCode = 6
Null ResultCode = 7
NoMem ResultCode = 8
ReadOnly ResultCode = 9
ExtDBOpen string = "db_open"
ExtDBClose string = "db_close"
ExtDBPrepare string = "db_prepare"
)
var (
lib uintptr
dbPrepare func(uintptr, uintptr) uintptr
rowsNext func(rowsPtr uintptr) int32
rowsGetValue func(rowsPtr uintptr, colIdx uint) uintptr
freeCString func(strPtr uintptr)
)