ic-sqlite-vfs: NICP / Nim 連携のための変更設計書
- 対象リポジトリ:
humandebri/ic-sqlite-vfs
- 想定利用側:
dumblepy/nicp_cdk + nim-ic-sqlite
- 作成日: 2026-08-24
- 対象バージョン:
ic-sqlite-vfs 2.0.0 / main ブランチ確認時点
1. 目的
本設計書は、Nim で実装された Internet Computer Canister から ic-sqlite-vfs を Rust static library 経由で利用するために、ic-sqlite-vfs 本体にどの変更が必要かを整理する。
前提アーキテクチャは以下とする。
Nim Canister
│
│ Nim API
▼
nim-ic-sqlite
│
│ C ABI
▼
nicp-sqlite-ffi (Rust staticlib)
│
▼
ic-sqlite-vfs
│
├── SQLite C
├── icstable sqlite3_vfs
└── Stable Memory
重要な設計原則は以下である。
ic-sqlite-vfs に Nim 固有コードを入れない。
- C ABI は
nim-ic-sqlite 側の Rust wrapper が担当する。
ic-sqlite-vfs は Rust ライブラリとして汎用性を維持する。
- NICP 固有の build/lifecycle 管理は
nicp_cdk が担当する。
ic-sqlite-vfs 側の変更は、可能な限り upstream に受け入れやすい汎用改善に限定する。
2. 結論
2.1 必須度の整理
| 変更 |
必須度 |
理由 |
wasm32-wasip1 の sqlite-precompiled 正式対応 |
P0 / 強く推奨 |
NICP の WASI ビルドターゲットと揃えるため |
| precompiled SQLite archive の target 名整理 |
P0 |
現在は WASI 用 object を wasm32-unknown-unknown ディレクトリに格納しており意味が不整合 |
wasm32-wasip1 build/CI test |
P0 |
upstream 更新時の回帰防止 |
| Stable Memory backend の外部注入 API |
P1 / 条件付き |
NICP 側が MemoryManager を所有する構成にする場合に必要 |
Memory trait の public abstraction 化 |
P1 / 条件付き |
同上 |
C ABI (extern "C") の追加 |
不要 |
nim-ic-sqlite の wrapper crate が担当すべき |
crate-type = ["staticlib"] の追加 |
不要 |
wrapper crate を staticlib にすれば依存 Rust code はそこへリンクできる |
NICP lifecycle (init, post_upgrade) の追加 |
不要 |
Canister framework 側の責務 |
| Nim/NICP feature の追加 |
不要 |
upstream を特定言語・CDKに依存させないため |
最重要結論
最初の NICP PoC を成立させるだけなら、ic-sqlite-vfs 本体を変更しなくてもよい。
sqlite-bundled feature を使用して nicp-sqlite-ffi を wasm32-wasip1 向けにビルドし、ic-sqlite-vfs 自身の MemoryManager<DefaultMemoryImpl> を利用すればよい。
ただし production の標準構成としては、少なくとも以下を upstream に実装することを推奨する。
P0: sqlite-precompiled + wasm32-wasip1 正式対応
さらに、将来的に NICP の Stable Storage と SQLite が 同一 MemoryManager を共有し、その MemoryManager を NICP 側で所有する設計へ移行する場合は、以下が必要になる。
P1: 外部 Stable Memory backend を Db に注入できる API
2.2 MemoryId / MemoryManager に関する確定方針
今回の設計では、MemoryId(120) を ic-sqlite-vfs 本体では予約しない。
ic-sqlite-vfs の責務は、consumer から渡された1つの logical memory を SQLite の保存領域として利用することであり、MemoryId namespace の割当ポリシーは consumer/framework 側が所有する。
したがって、NICP では例えば以下を採用できる。
MemoryId(0) -> NICP StableValue
MemoryId(1) -> NICP StableTable
...
MemoryId(120) -> SQLite reserved by nicp_cdk
一方、Rust consumer は従来通り以下のように明示的に選択できる。
const SQLITE_MEMORY_ID: MemoryId = MemoryId::new(120);
ここで最も重要な制約は、同一raw stable memoryに対して複数のlive MemoryManagerを独立に動作させないことである。
推奨構成:
raw IC stable memory
│
▼
one MemoryManager
│
├── MemoryId(0) -> application/framework storage
├── MemoryId(1) -> application/framework storage
└── MemoryId(120) -> SQLite
避ける構成:
NICP MemoryManager ────┐
├── same raw stable memory
SQLite MemoryManager ──┘
ic-sqlite-vfs の MemoryManager fork は ic-stable-structures 0.7 互換レイアウトを持つが、レイアウト互換性は「2つのmanager instanceを同時に利用してよい」ことを意味しない。allocation metadataやbucket情報を各instanceがheap上に保持するため、同時利用はstale stateを生む可能性がある。
このため長期設計では、ic-sqlite-vfs が consumer-owned VirtualMemory / StableMemory backendを受け取れることをP1要件とする。
3. 現状コードの制約
3.1 crate type
現在の Cargo.toml は以下である。
[lib]
crate-type = ["cdylib", "rlib"]
staticlib は定義されていない。
しかし、これは問題ではない。
nim-ic-sqlite 側に以下の wrapper crate を作成すればよい。
[lib]
crate-type = ["staticlib"]
[dependencies]
ic-sqlite-vfs = { version = "2.0.0", ... }
最終的な static archive は wrapper crate 側で生成する。
したがって ic-sqlite-vfs 自身に staticlib crate-type を追加する必要はない。
3.2 sqlite-precompiled が wasm32-unknown-unknown に固定されている
現在の build.rs は以下の制限を持つ。
fn link_precompiled(manifest_dir: &Path, target: &str) {
if target != "wasm32-unknown-unknown" {
panic!("sqlite-precompiled currently supports only wasm32-unknown-unknown");
}
let lib_dir = manifest_dir.join("vendor/sqlite/wasm32-unknown-unknown/lib");
...
}
そのため以下は失敗する。
cargo build \
--target wasm32-wasip1 \
--no-default-features \
--features sqlite-precompiled
NICP は Nim/C/Rust を WASI ベースでリンクするため、Rust wrapper も wasm32-wasip1 へ統一するのが望ましい。
3.3 precompiled archive の実体とディレクトリ名が一致していない
現在の build script は SQLite を実際には以下でコンパイルしている。
しかし出力先は以下である。
vendor/sqlite/wasm32-unknown-unknown/lib/libsqlite3.a
つまり現在は、概念的に以下の状態になっている。
wasm32-wasip1 object code
↓
vendor/sqlite/wasm32-unknown-unknown/
これは NICP 対応以前に、target semantics と artifact path が一致していない。
4. P0: wasm32-wasip1 precompiled support
4.1 目的
以下を正式なサポート対象にする。
cargo build \
--target wasm32-wasip1 \
--no-default-features \
--features sqlite-precompiled
NICP 側の推奨依存は最終的に以下とする。
ic-sqlite-vfs = {
version = "2.x",
default-features = false,
features = ["sqlite-precompiled"]
}
4.2 vendor directory の変更
推奨構成:
vendor/sqlite/
├── src/
│ ├── sqlite3.c
│ └── sqlite3.h
├── bindings/
├── build-flags.txt
└── wasm32-wasip1/
└── lib/
└── libsqlite3.a
既存互換性を維持する必要がある場合は一時的に以下も残せる。
vendor/sqlite/
├── wasm32-unknown-unknown/
│ └── lib/libsqlite3.a
└── wasm32-wasip1/
└── lib/libsqlite3.a
ただし、実体が同じ WASI object であるなら、長期的には wasm32-wasip1 を canonical path とする。
4.3 build.rs の変更
現状
if target != "wasm32-unknown-unknown" {
panic!("sqlite-precompiled currently supports only wasm32-unknown-unknown");
}
推奨
fn precompiled_sqlite_dir(manifest_dir: &Path, target: &str) -> PathBuf {
match target {
"wasm32-wasip1" => {
manifest_dir.join("vendor/sqlite/wasm32-wasip1/lib")
}
// compatibility mode; only retain if existing consumers need it
"wasm32-unknown-unknown" => {
manifest_dir.join("vendor/sqlite/wasm32-unknown-unknown/lib")
}
other => {
panic!(
"sqlite-precompiled is not available for target {other}; \
use sqlite-bundled or a supported precompiled target"
);
}
}
}
fn link_precompiled(manifest_dir: &Path, target: &str) {
let lib_dir = precompiled_sqlite_dir(manifest_dir, target);
println!(
"cargo:rerun-if-changed={}",
lib_dir.join("libsqlite3.a").display()
);
println!("cargo:rustc-link-search=native={}", lib_dir.display());
println!("cargo:rustc-link-lib=static=sqlite3");
}
4.4 build-sqlite-precompiled.sh の変更
現状
OUT_DIR="vendor/sqlite/wasm32-unknown-unknown"
"$CC_BIN" \
--target=wasm32-wasip1 \
...
推奨
TARGET="${SQLITE_WASM_TARGET:-wasm32-wasip1}"
OUT_DIR="vendor/sqlite/$TARGET"
"$CC_BIN" \
--target="$TARGET" \
...
ただし SQLite の precompiled artifact として公式にサポートする target を一つに絞る場合は、より単純に以下でよい。
OUT_DIR="vendor/sqlite/wasm32-wasip1"
"$CC_BIN" \
--target=wasm32-wasip1 \
...
この方が artifact の意味が明確である。
4.5 Cargo package include の更新
現在は以下のみが package に含まれている。
"/vendor/sqlite/wasm32-unknown-unknown/lib/libsqlite3.a",
以下へ変更する。
"/vendor/sqlite/wasm32-wasip1/lib/libsqlite3.a",
互換 target を残す場合は両方を含める。
4.6 .cargo/config.toml
現在は wasm32_unknown_unknown 向けだけに C compiler wrapper が設定されている。
NICP 連携では wasm32-wasip1 を直接 target とするため、sqlite-bundled も正式にサポートするなら以下も検討する。
[env]
CC_SHELL_ESCAPED_FLAGS = "1"
CFLAGS_wasm32_wasip1 = "--target=wasm32-wasip1"
ただし compiler path を crate 内で固定するより、利用側から次の環境変数で指定可能にする方が portability が高い。
CC_wasm32_wasip1=/path/to/clang
AR_wasm32_wasip1=/path/to/llvm-ar
設計としては、
sqlite-precompiled: compiler 不要
sqlite-bundled: WASI SDK compiler が必要
と明確に分離する。
5. P0: CI / compatibility test
最低限、以下を CI matrix に追加する。
Rust target feature
--------------------------------------------
wasm32-unknown-unknown sqlite-precompiled existing compatibility
wasm32-wasip1 sqlite-precompiled NICP / WASI path
wasm32-wasip1 sqlite-bundled source-build fallback
例:
cargo build \
--target wasm32-wasip1 \
--no-default-features \
--features sqlite-precompiled
および:
cargo build \
--target wasm32-wasip1 \
--no-default-features \
--features sqlite-bundled
Acceptance Criteria
wasm32-wasip1 build が成功する。
libsqlite3.a が正しい target で生成される。
- SQLite symbol が final Rust artifact から解決される。
ic0.stable64_* imports が維持される。
sqlite-precompiled 使用時に host C compiler を要求しない。
6. Stable Memory 所有権の問題
ここが NICP 連携で最も重要な設計ポイントである。
現在の ic-sqlite-vfs は以下の構造になっている。
DefaultMemoryImpl
│
▼
Ic0StableMemory
│
▼
MemoryManager<DefaultMemoryImpl>
│
▼
VirtualMemory<DefaultMemoryImpl>
│
▼
DbMemory
│
▼
Db::init(memory)
現在の型定義:
pub type DbMemory = VirtualMemory<DefaultMemoryImpl>;
したがって Db::init() は実質的に、ic-sqlite-vfs が定義する DefaultMemoryImpl に結びついた VirtualMemory しか受け取れない。
7. NICP 側 MemoryManager を使いたい場合の問題
将来的な理想構成を以下とする。
raw IC stable memory
│
▼
one shared MemoryManager
│
┌────────────────┼────────────────┐
▼ ▼ ▼
MemoryId(1) MemoryId(2) MemoryId(120)
│ │ │
IcStableValue IcStableTable SQLite
この場合、MemoryManager は Canister 全体で一つだけ存在するべきである。
独立した二つの MemoryManager instance を同じ raw stable memory に対して使う構成は避ける。
NG:
NICP MemoryManager ──────┐
├── same raw stable memory
Rust MemoryManager ──────┘
両方が allocation metadata を heap に cache するため、片側が bucket allocation を行った後、もう片側の in-memory state が stale になる可能性がある。
8. P1: 外部 Stable Memory backend API
8.1 この変更が必要になる条件
次の構成を採用する場合のみ必要である。
nicp_cdk owns MemoryManager
│
├── Nim stable structures
│
└── selected VirtualMemory
│
▼
ic-sqlite-vfs
一方、最初の実装を以下にするなら不要である。
ic-sqlite-vfs / Rust wrapper owns MemoryManager
│
├── SQLite MemoryId
└── C ABI で Nim へ VM read/write を提供
したがって P1 は NICP PoC の blocker ではない。
8.2 現在の内部 Memory trait
ic-sqlite-vfs には既に以下の概念が存在する。
pub trait Memory {
fn identity(&self) -> MemoryBackendIdentity;
fn size(&self) -> u64;
fn grow(&self, pages: u64) -> i64;
fn read(&self, offset: u64, dst: &mut [u8]);
fn write(&self, offset: u64, src: &[u8]);
}
これは外部 backend injection の基盤として十分近い。
しかし現在は stable module 自体が crate private であり、public API として外部 crate から実装することを前提としていない。
さらに DbMemory が以下に固定されている。
VirtualMemory<DefaultMemoryImpl>
そのため単純に Memory trait を re-export するだけでは不十分である。
9. P1 API 設計案
既存 2.x API を壊さず、additive に拡張することを推奨する。
9.1 避ける設計
以下のように既存 alias をいきなり generic 化する変更は避ける。
pub type DbMemory<M> = VirtualMemory<M>;
理由:
Db::init(DbMemory) の既存 source compatibility を壊しやすい。
- thread-local memory registry が現在 concrete type を前提としている。
DbHandle を generic 化すると public API 全体へ型 parameter が波及する。
9.2 推奨: type-erased backend を追加する
新しい public abstraction を追加する。
概念例:
pub trait StableMemoryBackend: 'static {
fn identity(&self) -> StableMemoryIdentity;
fn size(&self) -> u64;
fn grow(&self, pages: u64) -> i64;
fn read(&self, offset: u64, dst: &mut [u8]);
fn write(&self, offset: u64, src: &[u8]);
}
identity は clone された memory handle でも同一領域として判定できなければならない。
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct StableMemoryIdentity {
backend: u64,
memory: u64,
}
具体的な値の意味は API contract として固定しすぎず、同一 process / canister instance 内での重複登録判定に利用する。
その上で type erased handle を用意する。
#[derive(Clone)]
pub struct ExternalDbMemory {
inner: Rc<dyn StableMemoryBackend>,
}
新 API:
impl DbHandle {
pub fn init_external(
memory: ExternalDbMemory,
) -> Result<Self, DbError>;
}
既存 API は維持する。
impl Db {
pub fn init(memory: DbMemory) -> Result<(), DbError>;
}
impl DbHandle {
pub fn init(memory: DbMemory) -> Result<Self, DbError>;
}
10. 内部 memory registry の変更案
現在は概念的に以下である。
thread_local! {
static DB_MEMORY: RefCell<Vec<(ContextId, DbMemory)>> = ...;
}
これを private abstraction へ変更する。
trait RegisteredMemory {
fn identity(&self) -> StableMemoryIdentity;
fn size(&self) -> u64;
fn grow(&self, pages: u64) -> i64;
fn read(&self, offset: u64, dst: &mut [u8]);
fn write(&self, offset: u64, src: &[u8]);
}
registry は以下のように type erase する。
thread_local! {
static DB_MEMORY: RefCell<
Vec<(ContextId, Rc<dyn RegisteredMemory>)>
> = const { RefCell::new(Vec::new()) };
}
これにより以下の両方を同じ VFS backend として利用できる。
VirtualMemory<DefaultMemoryImpl>
ExternalDbMemory
SQLite/VFS 上位層からは byte-addressed memory であることだけを要求する。
11. NICP callback backend の例
nicp-sqlite-ffi 側で以下の Rust type を実装できるようにする。
struct NicpMemory {
memory_id: u8,
}
impl StableMemoryBackend for NicpMemory {
fn identity(&self) -> StableMemoryIdentity {
StableMemoryIdentity::new(
NICP_BACKEND_ID,
self.memory_id as u64,
)
}
fn size(&self) -> u64 {
unsafe { nicp_vm_size(self.memory_id) }
}
fn grow(&self, pages: u64) -> i64 {
unsafe { nicp_vm_grow(self.memory_id, pages) }
}
fn read(&self, offset: u64, dst: &mut [u8]) {
unsafe {
nicp_vm_read(
self.memory_id,
offset,
dst.as_mut_ptr(),
dst.len(),
)
}
}
fn write(&self, offset: u64, src: &[u8]) {
unsafe {
nicp_vm_write(
self.memory_id,
offset,
src.as_ptr(),
src.len(),
)
}
}
}
この設計なら ic-sqlite-vfs 自体は NICP を知らない。
ic-sqlite-vfs
│
│ StableMemoryBackend
▼
nicp-sqlite-ffi
│
│ C ABI callback
▼
nicp_cdk VirtualMemory
12. ただし P1 は後回しにするべき理由
P1 は memory layer の内部設計変更を伴うため、SQLite の NICP PoC と同時に実装すべきではない。
推奨する初期構成は以下である。
Rust side
MemoryManager<DefaultMemoryImpl>
│
├── MemoryId(120) -> SQLite
├── MemoryId(1) -> NICP stable area
└── MemoryId(2) -> NICP stable area
Rust wrapper から Nim へ次を公開する。
uint64_t nicp_vm_size(uint8_t memory_id);
int64_t nicp_vm_grow(uint8_t memory_id, uint64_t pages);
int32_t nicp_vm_read(
uint8_t memory_id,
uint64_t offset,
uint8_t *dst,
uint32_t len
);
int32_t nicp_vm_write(
uint8_t memory_id,
uint64_t offset,
const uint8_t *src,
uint32_t len
);
この方式なら ic-sqlite-vfs の MemoryManager が唯一の manager になる。
NICP の stable structures はこの VM API 上へ移行できる。
利点
- upstream 変更が最小。
- MemoryManager layout を二重実装しなくてよい。
- allocation metadata の ownership が一箇所になる。
- SQLite PoC と stable-storage redesign を分離できる。
欠点
- NICP の Stable Memory core が Rust 実装へ一部依存する。
- SQLite を使わない project でも同じ MemoryManager を利用したい場合、別 package 化が必要になる。
したがって長期的に NICP 自身が manager を所有したい場合にのみ P1 を進める。
13. C ABI は ic-sqlite-vfs に追加しない
以下のような API を ic-sqlite-vfs 本体へ直接追加することは推奨しない。
#[no_mangle]
pub extern "C" fn ic_sqlite_execute(...) -> i32 {
...
}
理由:
- Rust crate としての責務を越える。
- ABI versioning が crate の public Rust API と結合する。
- Nim 以外の language binding 要件が入り始める。
- row/value/error memory ownership が core crate を複雑化する。
- C ABI の最適形は利用言語によって異なる。
C ABI は以下に置く。
nim-ic-sqlite/
└── rust/
└── nicp-sqlite-ffi/
14. staticlib も ic-sqlite-vfs に追加しない
以下の変更も原則不要である。
[lib]
crate-type = ["cdylib", "rlib", "staticlib"]
理由:
ic-sqlite-vfs は Rust library dependency として維持し、最終 linking boundary を wrapper crate にする方が責務が明確だからである。
ic-sqlite-vfs = Rust API crate
nicp-sqlite-ffi = foreign-language ABI crate
最終 static archive:
のみを Nim/clang linker へ渡す。
15. ic-cdk dependency
ic-sqlite-vfs の core DB/VFS 自体は direct ic0.stable64_* import を利用している。
Canister Candid API は canister-api feature によって分離されている。
NICP wrapper では以下を使用しない。
features = ["canister-api"]
つまり ic-sqlite-vfs の reference canister API を final Wasm に export する必要はない。
将来的な改善として、ic-cdk dependency 自体を optional にして canister-api にだけ紐付けられるかは検討可能だが、NICP integration の必須変更ではない。
候補:
[features]
canister-api = ["dep:ic-cdk", "dep:candid", "dep:serde"]
[dependencies]
ic-cdk = { version = "...", optional = true }
ただし core API で candid/serde type を使用している箇所がないことを確認した上で行う。
これは binary size と dependency surface を減らす改善であり、P2 とする。
16. Lifecycle API は変更しない
ic-sqlite-vfs README の典型利用では以下を要求する。
canister_init
-> MemoryManager init
-> Db::init
-> Db::migrate
canister_post_upgrade
-> MemoryManager init
-> Db::init
-> Db::migrate
しかし lifecycle export は利用 framework の責務である。
Rust Canister:
Nim Canister:
nicp_cdk lifecycle pragmas
したがって ic-sqlite-vfs へ lifecycle hook を追加しない。
17. Async / inter-canister API も変更しない
現在の同期 closure API は維持すべきである。
Db::update(|connection| {
...
})
SQLite transaction 中の await を禁止する設計は Internet Computer の execution model と整合する。
Nim binding 側でも同じ制約を表現する。
db.update:
db.execute(...)
この block を async procedure にしない。
ic-sqlite-vfs 側へ async API を追加する必要はない。
18. 推奨する変更セット
Phase A: upstream 最小変更
対象: ic-sqlite-vfs
1. wasm32-wasip1 sqlite-precompiled support
2. vendor path を wasm32-wasip1 に整合
3. build script 修正
4. Cargo package include 修正
5. CI build matrix 追加
6. docs 更新
この時点では memory API を変更しない。
Release impact
原則 additive change のため minor release で対応可能。
例:
19. Phase B: NICP integration PoC
対象: nim-ic-sqlite
ic-sqlite-vfs を dependency とする。
[lib]
crate-type = ["staticlib"]
[dependencies.ic-sqlite-vfs]
version = "2.1"
default-features = false
features = ["sqlite-precompiled"]
C ABI:
init
execute
query
migration
error
を wrapper crate に実装する。
この段階では ic-sqlite-vfs MemoryManager が stable memory allocation を所有する。
20. Phase C: Stable Memory architecture の評価
PoC 後に以下を判断する。
Option 1
Rust MemoryManager を NICP ecosystem 共通 allocator とする
この場合 ic-sqlite-vfs の追加変更は不要。
Option 2
NICP が MemoryManager を所有する
この場合 P1 の external backend API を ic-sqlite-vfs upstream に提案する。
21. P1 の upstream PR を分離する
P0 と P1 は同じ PR にしない。
推奨 PR 構成:
PR 1
Support precompiled SQLite on wasm32-wasip1
内容:
- build.rs
- vendor path
- precompile script
- Cargo include
- documentation
- CI
PR 2
Allow database memory backends to be supplied externally
内容:
- memory abstraction
- type erasure
- external backend API
- identity semantics
- compatibility tests
理由:
P0 は小さく明確で upstream に受け入れられやすい。
P1 は API design と compatibility discussion が必要になる。
22. P0 Acceptance Criteria
以下をすべて満たした時点で完了とする。
23. P1 Acceptance Criteria
P1 を実施する場合は以下を満たす。
24. テスト設計
24.1 Build test
cargo check host
cargo test host
cargo build wasm32-wasip1 sqlite-precompiled
cargo build wasm32-wasip1 sqlite-bundled
24.2 staticlib consumer fixture
NICP そのものを ic-sqlite-vfs CI dependency にする必要はない。
代わりに小さな fixture crate を用意する。
tests/fixtures/staticlib-consumer/
├── Cargo.toml
└── src/lib.rs
[lib]
crate-type = ["staticlib"]
use ic_sqlite_vfs::{Db, DefaultMemoryImpl, MemoryId, MemoryManager};
#[unsafe(no_mangle)]
pub extern "C" fn smoke_init() -> i32 {
let manager = MemoryManager::init(DefaultMemoryImpl::default());
let memory = manager.get(MemoryId::new(120));
match Db::init(memory) {
Ok(_) => 0,
Err(_) => -1,
}
}
これを wasm32-wasip1 staticlib として build する。
これにより foreign-language consumer と同じ linkage path を upstream CI で検証できる。
25. 互換性
Stable layout
P0 では一切変更しない。
raw stable memory
-> MemoryManager 0.7-compatible layout
-> VirtualMemory
-> ic-sqlite-vfs superblock
-> SQLite image
precompiled target support は binary build artifact の問題であり stable data format の問題ではない。
したがって upgrade compatibility に影響を与えないことを必須条件とする。
26. リスク
Risk 1: WASI libc symbol duplication
Nim/C/Rust を一つの Wasm へ static-link するため、Rust と clang が別々の libc/object を持ち込むと duplicate symbol が発生する可能性がある。
これは主として NICP / wrapper build layer の問題であり、ic-sqlite-vfs core API の変更で解決すべきではない。
必要に応じて Rust wrapper の build flags で external WASI libc を共有する。
Risk 2: precompiled archive の ABI mismatch
libsqlite3.a を wasm32-wasip1 用として配布するなら、実際の compile target、WASI SDK、SQLite compile flags を reproducible にする必要がある。
最低限以下を記録する。
SQLite version
WASI SDK version
clang version
compile target
build-flags.txt hash
Risk 3: external memory API が過剰設計になる
NICP の initial integration だけなら external backend API は不要である。
先に導入すると、upstream public API と stable memory identity semantics を不必要に複雑化する可能性がある。
したがって P1 は PoC 後に実際の ownership requirement が確定してから実施する。
27. 推奨最終方針
ic-sqlite-vfs に今すぐ入れる変更
ic-sqlite-vfs
└── wasm32-wasip1 support
├── sqlite-precompiled
├── correct vendor path
├── build script
├── package include
├── docs
└── CI
nim-ic-sqlite に入れるもの
nim-ic-sqlite
├── C ABI
├── Rust staticlib wrapper
├── Db/Statement/Row/Value Nim API
├── migrations binding
├── error conversion
└── MemoryManager lifecycle glue
nicp_cdk に入れるもの
nicp_cdk
├── Rust staticlib build integration
├── wasm32-wasip1 toolchain support
├── init/preUpgrade/postUpgrade
└── Stable Memory abstraction redesign
28. 最終判断
ic-sqlite-vfs 本体へ大規模な NICP 対応を入れるべきではない。
最も妥当な境界は以下である。
generic
│
▼
ic-sqlite-vfs
│
Rust API only
│
▼
nicp-sqlite-ffi
│
C ABI
│
▼
nim-ic-sqlite
│
Nim API
│
▼
nicp_cdk
短期的には、ic-sqlite-vfs の必須修正を wasm32-wasip1 の正式 build support に限定する。
Stable Memory ownership については、まず Rust 側 MemoryManager を唯一の manager として PoC と upgrade persistence を成立させる。
その後、NICP が manager ownership を持つ必要性が明確になった場合のみ、ic-sqlite-vfs に 外部 Stable Memory backend injection を追加する。
この順序にすることで、
- upstream 改修を小さくできる
- SQLite と NICP の問題を切り分けられる
- stable format compatibility risk を抑えられる
nim-ic-sqlite の開発を先行できる
という利点がある。
29. 参照した現状コード
ic-sqlite-vfs: NICP / Nim 連携のための変更設計書
humandebri/ic-sqlite-vfsdumblepy/nicp_cdk+nim-ic-sqliteic-sqlite-vfs 2.0.0/mainブランチ確認時点1. 目的
本設計書は、Nim で実装された Internet Computer Canister から
ic-sqlite-vfsを Rust static library 経由で利用するために、ic-sqlite-vfs本体にどの変更が必要かを整理する。前提アーキテクチャは以下とする。
重要な設計原則は以下である。
ic-sqlite-vfsに Nim 固有コードを入れない。nim-ic-sqlite側の Rust wrapper が担当する。ic-sqlite-vfsは Rust ライブラリとして汎用性を維持する。nicp_cdkが担当する。ic-sqlite-vfs側の変更は、可能な限り upstream に受け入れやすい汎用改善に限定する。2. 結論
2.1 必須度の整理
wasm32-wasip1のsqlite-precompiled正式対応wasm32-unknown-unknownディレクトリに格納しており意味が不整合wasm32-wasip1build/CI testMemorytrait の public abstraction 化extern "C") の追加nim-ic-sqliteの wrapper crate が担当すべきcrate-type = ["staticlib"]の追加staticlibにすれば依存 Rust code はそこへリンクできるinit,post_upgrade) の追加最重要結論
最初の NICP PoC を成立させるだけなら、
ic-sqlite-vfs本体を変更しなくてもよい。sqlite-bundledfeature を使用してnicp-sqlite-ffiをwasm32-wasip1向けにビルドし、ic-sqlite-vfs自身のMemoryManager<DefaultMemoryImpl>を利用すればよい。ただし production の標準構成としては、少なくとも以下を upstream に実装することを推奨する。
さらに、将来的に NICP の Stable Storage と SQLite が 同一 MemoryManager を共有し、その MemoryManager を NICP 側で所有する設計へ移行する場合は、以下が必要になる。
2.2 MemoryId / MemoryManager に関する確定方針
今回の設計では、
MemoryId(120)をic-sqlite-vfs本体では予約しない。ic-sqlite-vfsの責務は、consumer から渡された1つの logical memory を SQLite の保存領域として利用することであり、MemoryId namespace の割当ポリシーは consumer/framework 側が所有する。したがって、NICP では例えば以下を採用できる。
一方、Rust consumer は従来通り以下のように明示的に選択できる。
ここで最も重要な制約は、同一raw stable memoryに対して複数のlive MemoryManagerを独立に動作させないことである。
推奨構成:
避ける構成:
ic-sqlite-vfsの MemoryManager fork はic-stable-structures 0.7互換レイアウトを持つが、レイアウト互換性は「2つのmanager instanceを同時に利用してよい」ことを意味しない。allocation metadataやbucket情報を各instanceがheap上に保持するため、同時利用はstale stateを生む可能性がある。このため長期設計では、
ic-sqlite-vfsが consumer-owned VirtualMemory / StableMemory backendを受け取れることをP1要件とする。3. 現状コードの制約
3.1 crate type
現在の
Cargo.tomlは以下である。staticlibは定義されていない。しかし、これは問題ではない。
nim-ic-sqlite側に以下の wrapper crate を作成すればよい。最終的な static archive は wrapper crate 側で生成する。
したがって
ic-sqlite-vfs自身にstaticlibcrate-type を追加する必要はない。3.2 sqlite-precompiled が wasm32-unknown-unknown に固定されている
現在の
build.rsは以下の制限を持つ。そのため以下は失敗する。
NICP は Nim/C/Rust を WASI ベースでリンクするため、Rust wrapper も
wasm32-wasip1へ統一するのが望ましい。3.3 precompiled archive の実体とディレクトリ名が一致していない
現在の build script は SQLite を実際には以下でコンパイルしている。
しかし出力先は以下である。
つまり現在は、概念的に以下の状態になっている。
これは NICP 対応以前に、target semantics と artifact path が一致していない。
4. P0: wasm32-wasip1 precompiled support
4.1 目的
以下を正式なサポート対象にする。
NICP 側の推奨依存は最終的に以下とする。
4.2 vendor directory の変更
推奨構成:
既存互換性を維持する必要がある場合は一時的に以下も残せる。
ただし、実体が同じ WASI object であるなら、長期的には
wasm32-wasip1を canonical path とする。4.3 build.rs の変更
現状
推奨
4.4 build-sqlite-precompiled.sh の変更
現状
推奨
ただし SQLite の precompiled artifact として公式にサポートする target を一つに絞る場合は、より単純に以下でよい。
この方が artifact の意味が明確である。
4.5 Cargo package include の更新
現在は以下のみが package に含まれている。
"/vendor/sqlite/wasm32-unknown-unknown/lib/libsqlite3.a",以下へ変更する。
"/vendor/sqlite/wasm32-wasip1/lib/libsqlite3.a",互換 target を残す場合は両方を含める。
4.6 .cargo/config.toml
現在は
wasm32_unknown_unknown向けだけに C compiler wrapper が設定されている。NICP 連携では
wasm32-wasip1を直接 target とするため、sqlite-bundledも正式にサポートするなら以下も検討する。ただし compiler path を crate 内で固定するより、利用側から次の環境変数で指定可能にする方が portability が高い。
設計としては、
sqlite-precompiled: compiler 不要sqlite-bundled: WASI SDK compiler が必要と明確に分離する。
5. P0: CI / compatibility test
最低限、以下を CI matrix に追加する。
例:
および:
Acceptance Criteria
wasm32-wasip1build が成功する。libsqlite3.aが正しい target で生成される。ic0.stable64_*imports が維持される。sqlite-precompiled使用時に host C compiler を要求しない。6. Stable Memory 所有権の問題
ここが NICP 連携で最も重要な設計ポイントである。
現在の
ic-sqlite-vfsは以下の構造になっている。現在の型定義:
したがって
Db::init()は実質的に、ic-sqlite-vfsが定義するDefaultMemoryImplに結びついた VirtualMemory しか受け取れない。7. NICP 側 MemoryManager を使いたい場合の問題
将来的な理想構成を以下とする。
この場合、MemoryManager は Canister 全体で一つだけ存在するべきである。
独立した二つの MemoryManager instance を同じ raw stable memory に対して使う構成は避ける。
両方が allocation metadata を heap に cache するため、片側が bucket allocation を行った後、もう片側の in-memory state が stale になる可能性がある。
8. P1: 外部 Stable Memory backend API
8.1 この変更が必要になる条件
次の構成を採用する場合のみ必要である。
一方、最初の実装を以下にするなら不要である。
したがって P1 は NICP PoC の blocker ではない。
8.2 現在の内部 Memory trait
ic-sqlite-vfsには既に以下の概念が存在する。これは外部 backend injection の基盤として十分近い。
しかし現在は
stablemodule 自体が crate private であり、public API として外部 crate から実装することを前提としていない。さらに
DbMemoryが以下に固定されている。そのため単純に
Memorytrait を re-export するだけでは不十分である。9. P1 API 設計案
既存 2.x API を壊さず、additive に拡張することを推奨する。
9.1 避ける設計
以下のように既存 alias をいきなり generic 化する変更は避ける。
理由:
Db::init(DbMemory)の既存 source compatibility を壊しやすい。DbHandleを generic 化すると public API 全体へ型 parameter が波及する。9.2 推奨: type-erased backend を追加する
新しい public abstraction を追加する。
概念例:
identity は clone された memory handle でも同一領域として判定できなければならない。
具体的な値の意味は API contract として固定しすぎず、同一 process / canister instance 内での重複登録判定に利用する。
その上で type erased handle を用意する。
新 API:
既存 API は維持する。
10. 内部 memory registry の変更案
現在は概念的に以下である。
これを private abstraction へ変更する。
registry は以下のように type erase する。
これにより以下の両方を同じ VFS backend として利用できる。
SQLite/VFS 上位層からは byte-addressed memory であることだけを要求する。
11. NICP callback backend の例
nicp-sqlite-ffi側で以下の Rust type を実装できるようにする。この設計なら
ic-sqlite-vfs自体は NICP を知らない。12. ただし P1 は後回しにするべき理由
P1 は memory layer の内部設計変更を伴うため、SQLite の NICP PoC と同時に実装すべきではない。
推奨する初期構成は以下である。
Rust wrapper から Nim へ次を公開する。
この方式なら
ic-sqlite-vfsの MemoryManager が唯一の manager になる。NICP の stable structures はこの VM API 上へ移行できる。
利点
欠点
したがって長期的に NICP 自身が manager を所有したい場合にのみ P1 を進める。
13. C ABI は ic-sqlite-vfs に追加しない
以下のような API を
ic-sqlite-vfs本体へ直接追加することは推奨しない。理由:
C ABI は以下に置く。
14. staticlib も ic-sqlite-vfs に追加しない
以下の変更も原則不要である。
理由:
ic-sqlite-vfsは Rust library dependency として維持し、最終 linking boundary を wrapper crate にする方が責務が明確だからである。最終 static archive:
のみを Nim/clang linker へ渡す。
15. ic-cdk dependency
ic-sqlite-vfsの core DB/VFS 自体は directic0.stable64_*import を利用している。Canister Candid API は
canister-apifeature によって分離されている。NICP wrapper では以下を使用しない。
つまり
ic-sqlite-vfsの reference canister API を final Wasm に export する必要はない。将来的な改善として、
ic-cdkdependency 自体を optional にしてcanister-apiにだけ紐付けられるかは検討可能だが、NICP integration の必須変更ではない。候補:
ただし core API で candid/serde type を使用している箇所がないことを確認した上で行う。
これは binary size と dependency surface を減らす改善であり、P2 とする。
16. Lifecycle API は変更しない
ic-sqlite-vfsREADME の典型利用では以下を要求する。しかし lifecycle export は利用 framework の責務である。
Rust Canister:
Nim Canister:
したがって
ic-sqlite-vfsへ lifecycle hook を追加しない。17. Async / inter-canister API も変更しない
現在の同期 closure API は維持すべきである。
SQLite transaction 中の
awaitを禁止する設計は Internet Computer の execution model と整合する。Nim binding 側でも同じ制約を表現する。
この block を async procedure にしない。
ic-sqlite-vfs側へ async API を追加する必要はない。18. 推奨する変更セット
Phase A: upstream 最小変更
対象:
ic-sqlite-vfsこの時点では memory API を変更しない。
Release impact
原則 additive change のため minor release で対応可能。
例:
19. Phase B: NICP integration PoC
対象:
nim-ic-sqliteic-sqlite-vfsを dependency とする。C ABI:
を wrapper crate に実装する。
この段階では
ic-sqlite-vfsMemoryManager が stable memory allocation を所有する。20. Phase C: Stable Memory architecture の評価
PoC 後に以下を判断する。
Option 1
この場合
ic-sqlite-vfsの追加変更は不要。Option 2
この場合 P1 の external backend API を
ic-sqlite-vfsupstream に提案する。21. P1 の upstream PR を分離する
P0 と P1 は同じ PR にしない。
推奨 PR 構成:
PR 1
内容:
PR 2
内容:
理由:
P0 は小さく明確で upstream に受け入れられやすい。
P1 は API design と compatibility discussion が必要になる。
22. P0 Acceptance Criteria
以下をすべて満たした時点で完了とする。
wasm32-wasip1 + sqlite-precompiledが build できる。wasm32-wasip1 + sqlite-bundledが build できる。wasm32-wasip1/libsqlite3.aが含まれる。23. P1 Acceptance Criteria
P1 を実施する場合は以下を満たす。
Db::init(DbMemory)API がそのまま動く。DbHandle::init(DbMemory)API がそのまま動く。24. テスト設計
24.1 Build test
24.2 staticlib consumer fixture
NICP そのものを
ic-sqlite-vfsCI dependency にする必要はない。代わりに小さな fixture crate を用意する。
これを
wasm32-wasip1staticlib として build する。これにより foreign-language consumer と同じ linkage path を upstream CI で検証できる。
25. 互換性
Stable layout
P0 では一切変更しない。
precompiled target support は binary build artifact の問題であり stable data format の問題ではない。
したがって upgrade compatibility に影響を与えないことを必須条件とする。
26. リスク
Risk 1: WASI libc symbol duplication
Nim/C/Rust を一つの Wasm へ static-link するため、Rust と clang が別々の libc/object を持ち込むと duplicate symbol が発生する可能性がある。
これは主として NICP / wrapper build layer の問題であり、
ic-sqlite-vfscore API の変更で解決すべきではない。必要に応じて Rust wrapper の build flags で external WASI libc を共有する。
Risk 2: precompiled archive の ABI mismatch
libsqlite3.aをwasm32-wasip1用として配布するなら、実際の compile target、WASI SDK、SQLite compile flags を reproducible にする必要がある。最低限以下を記録する。
Risk 3: external memory API が過剰設計になる
NICP の initial integration だけなら external backend API は不要である。
先に導入すると、upstream public API と stable memory identity semantics を不必要に複雑化する可能性がある。
したがって P1 は PoC 後に実際の ownership requirement が確定してから実施する。
27. 推奨最終方針
ic-sqlite-vfs に今すぐ入れる変更
nim-ic-sqlite に入れるもの
nicp_cdk に入れるもの
28. 最終判断
ic-sqlite-vfs本体へ大規模な NICP 対応を入れるべきではない。最も妥当な境界は以下である。
短期的には、
ic-sqlite-vfsの必須修正をwasm32-wasip1の正式 build support に限定する。Stable Memory ownership については、まず Rust 側 MemoryManager を唯一の manager として PoC と upgrade persistence を成立させる。
その後、NICP が manager ownership を持つ必要性が明確になった場合のみ、
ic-sqlite-vfsに 外部 Stable Memory backend injection を追加する。この順序にすることで、
nim-ic-sqliteの開発を先行できるという利点がある。
29. 参照した現状コード
Cargo.tomlhttps://github.com/humandebri/ic-sqlite-vfs/blob/main/Cargo.toml
build.rshttps://github.com/humandebri/ic-sqlite-vfs/blob/main/build.rs
.cargo/config.tomlhttps://github.com/humandebri/ic-sqlite-vfs/blob/main/.cargo/config.toml
scripts/build-sqlite-precompiled.shhttps://github.com/humandebri/ic-sqlite-vfs/blob/main/scripts/build-sqlite-precompiled.sh
src/lib.rshttps://github.com/humandebri/ic-sqlite-vfs/blob/main/src/lib.rs
src/db/mod.rshttps://github.com/humandebri/ic-sqlite-vfs/blob/main/src/db/mod.rs
src/stable/memory.rshttps://github.com/humandebri/ic-sqlite-vfs/blob/main/src/stable/memory.rs
src/stable/memory_manager.rshttps://github.com/humandebri/ic-sqlite-vfs/blob/main/src/stable/memory_manager.rs
src/stable/raw_memory.rshttps://github.com/humandebri/ic-sqlite-vfs/blob/main/src/stable/raw_memory.rs