RustでSQLite3のextension(拡張ライブラリ)を作成する

2023.09.28

Introduction

以前SQLiteのextensionを使う記事を書いたのですが、
今回はextensionを作るほうです。

sqlite-loadable-rsを使えばRustでSQLite用extensionを作成できるので、
やってみましょう。

Environment

  • MacBook Pro (13-inch, M1, 2020)
  • OS : MacOS 13.5.2
  • Linux VM : ubuntu23(OrbStack使用)
  • Rust : 1.72.1

Setup

sqlite-loadable-rsクレートはRustを使用してSQLite拡張を構築するためのライブラリです。
詳細についてはここに紹介記事がありますのでご確認ください。

sqlite-loadable-rsのリポジトリにはそのまま動くサンプルがあるので、
Githubからcloneして動かしてみましょう。

Linux VM環境を作成

私の環境(M1 mac)だと動かなかったので、Linux VM環境を用意します。
OrbStack使ったら簡単に動いたのでそれを使います。
OrbStackを起動したらMachinesからUbuntu23を選択して起動します。
起動したら↓のようにsshでログインできます。
とても楽です。

% ssh <作成したVMの名前>@orb

Ubuntuで必要なライブラリなどのインストールをします。

% sudo apt update
% sudo apt-get install git
% sudo apt-get install -y sqlite3
% sudo apt-get install -y gcc
% sudo apt-get install libssl-dev

私の環境ではビルド時にssl関連のエラーがでたので、
そうなった場合は↓のコマンドでパスをしらべて環境変数に設定。

% dpkg -L libssl-dev | grep lib
% dpkg -L libssl-dev | grep include

#自分の環境では↓を設定
% export OPENSSL_INCLUDE_DIR=/usr/include/openssl/
% export OPENSSL_LIB_DIR=/usr/lib/x86_64-linux-gnu/

そしてRustとNodeのインストールをしておきます。

# Rust
% curl https://sh.rustup.rs -sSf | sh
% export PATH=$PATH:$HOME/.cargo/bin

# nodeとnpm
% sudo apt install nodejs npm

Try sqlite-loadable-rs

では次に、sqlite-loadable-rsにあるサンプルの拡張ライブラリを動かしてみましょう。
sqlite-loadable-rsをcloneしてhello extentionをうごかしてみます。

% git clone https://github.com/asg017/sqlite-loadable-rs.git
% cd sqlite-loadable-rs

Rustで記述された、hello extentionをビルドします。  

% cargo build --example hello --release

sqliteを起動して、↑でビルドしたモジュールをloadします。
hello関数が使えるようになってますね。

% sqlite3
SQLite version 3.40.1 2022-12-28 14:03:47
・・・

sqlite> .load target/release/examples/libhello
sqlite> select hello("world!");
hello, world!

sqlite> .quit

Create SQlite Extension

次に、Sqlite Extensionを作ってみます。
fooという関数を定義し、その中ではhttpbin.orgにアクセスして
自身のIPをとってくるという(特に意味のない)処理をしてみます。

Cargoでプロジェクトを作成して依存ライブラリをaddします。

% cargo new sqlite-plugin && cd sqlite-plugin
% cargo add sqlite-loadable
% cargo add reqwest

Cargo.tomlは↓のような感じです。

[dependencies]
reqwest = { version = "0.11.20", features = ["blocking", "json"] }
sqlite-loadable = "0.0.5"

[[example]]
name = "foo"
crate-type = ["cdylib"]

sqlite-plugin/examplesディレクトリを作成し、
foo.rsファイル(extension本体)を作成します。

use sqlite_loadable::prelude::*;
use sqlite_loadable::{api, define_scalar_function, Result};
use std::collections::HashMap;

pub fn foo(context: *mut sqlite3_context, values: &[*mut sqlite3_value]) -> Result<()> {
    let key = api::value_text(values.get(0).expect("1st argument as key"))?;
    let name = api::value_text(values.get(1).expect("2nd argument as name"))?;

    //外部サイトに接続してデータ取得
    let resp = reqwest::blocking::get("https://httpbin.org/ip")
        .unwrap().json::<HashMap<String, String>>().unwrap();

    api::result_text(context, format!("1st:{}, 2nd:{}, ip:{:?}", key,name,resp))?;
    Ok(())
}

#[sqlite_entrypoint]
pub fn sqlite3_foo_init(db: *mut sqlite3) -> Result<()> {
    let flags = FunctionFlags::UTF8 | FunctionFlags::DETERMINISTIC;
    define_scalar_function(db, "foo", 2, foo, flags)?;
    Ok(())
}

foo関数はそのまま"foo"という名前のscalar関数として登録されます。
この関数では引数を2つ受け取り、内部では自身のIPアドレスを外部サイトから取得して
引数とともに返します。

sqlite3_foo_init関数が外部に公開される拡張ライブラリ用のエントリーポイントです。
コンパイルされたファイルは"libfoo.so"(OSによっては.dylib/.dll)となります。

ではextensionをビルドしてみましょう。

% cargo build --example foo --release

ビルドに成功すると、target/release/examplesにlibfooファイルができてます。

% ls ./sqlite-plugin/target/release/examples/
foo-bf6dda611cc76dbe.d  libfoo-bf6dda611cc76dbe.so  libfoo.d  libfoo.so

SQLiteのシェルから使う

sqlite3を起動してextensionをloadして使ってみます。
ちゃんと動いてますね。

% sqlite3 mydb.sqlite3
sqlite> .load target/release/examples/libfoo
sqlite> select foo("hello","world");
1st:hello, 2nd:world, ip:{"origin": "xxx.xxx.xxx.xxx"}

Nodeから使ってみる

プログラムからも使ってみます。
Node用SQlite3モジュールをインストールして、Nodeからアクセスしてみましょう。

% npm install sqlite3

↓のようにloadExtension関数を使って簡単に実装できます。

var sqlite3 = require('sqlite3');

//sqlite3データベースのパスを指定
let db = new sqlite3.Database("<your sqlite3 database file>", (err) => {
    if (err) {
        console.error(err.message)
        throw err
    }else{
        console.log('Connected to the SQLite database.');
    }
});

//libfooへのパスを指定
db.loadExtension('<path/your/target/release/examples/libfoo>');

db.all('select foo("hello","world")', (error, rows) => {
    console.log(rows);
});

db.close();

rustqliteから使ってみる

Rustからもextensionにアクセスしてみます。
rusqliteを使えばさきほどと同じように実行可能です。

適当なRustプロジェクトを作成し、Cargo.tomlにrussqliteを記述します。
※load_extensionをfeaturesで記述すること

[dependencies]
rusqlite = { version = "0.29.0", features = ["bundled","load_extension"] }

Rustコードは↓のような感じです。
LoadExtensionGuardとload_extensionを使ってextensionをloadします。
unsafeを使ってるのはしょうがないところ。

use rusqlite::{Connection, Result,LoadExtensionGuard};

fn load_my_extension(conn: &Connection) -> Result<(),rusqlite::Error> {
    unsafe {
        let _guard = LoadExtensionGuard::new(conn)?;
        //libfooへのパスを指定
        conn.load_extension("path/your/target/release/examples/libfoo", None)
    }
}

fn main() -> Result<(),rusqlite::Error>{
    let conn = Connection::open_in_memory()?;

    if let Ok(_value) = load_my_extension(&conn) {
        let mut stmt = conn.prepare("select foo('hello','world')")?;
        let mut rows = stmt.query([])?;
        if let Some(row) = rows.next()? {
            let res:String = row.get(0)?;
            println!("{:?}",res);
        }
    } else {
        println!("load_my_extension Err");
    }
    Ok(())
}

問題なく実行可能です。

% cargo run
"1st:hello, 2nd:world, ip:{\"origin\": \"xxx.xxx.xxx.xxx\"}"

Summary

というわけで、RustでSqliteのextensionを実装して使ってみました。
今回はシンプルな関数を実装しましたが、virtual table用のモジュールも作成できます。

virtual tableを使えばいろいろなオブジェクト(ex.csv,s3,kvsなど)を
テーブルとしてアクセスできるので、さらに便利に使えそうです。

References