From a715bde79fa4f88fbecb948477d970b0954f5f6b Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Mon, 2 Mar 2026 21:21:18 +0900 Subject: [PATCH 01/11] Update project --- Cargo.lock | 47 ++++++++++--------- uzumibi-on-cloudflare-spike/.gitignore | 4 +- uzumibi-on-cloudflare-spike/package.json | 2 +- .../wasm-app/Cargo.toml | 8 ++-- uzumibi-on-cloudflare-spike/wrangler.jsonc | 2 +- 5 files changed, 33 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f1fb9aa..8b5ab13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -963,9 +963,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" -version = "0.3.90" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dc6f6450b3f6d4ed5b16327f38fed626d375a886159ca555bd7822c0c3a5a6" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -1069,9 +1069,9 @@ dependencies = [ [[package]] name = "mruby-compiler2-sys" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e57b6bea7a439d4a48c5e2d680104b99855404caa0e24a30febe5bbee176f25" +checksum = "e521a8e2c0b4d22ee0755f54c68029f0763b83128f08d148755eb3c0901bbeea" dependencies = [ "bindgen", "cc", @@ -1081,9 +1081,9 @@ dependencies = [ [[package]] name = "mrubyedge" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6637cd859a862100009f116830b1b1f6e932633d574c24480ba57983e208567" +checksum = "8196b89b47357c9d189abfdcb6024b0842817d089742cb127f5a223c11592a2b" dependencies = [ "plain", "rand_core 0.10.0", @@ -1093,9 +1093,9 @@ dependencies = [ [[package]] name = "mrubyedge-serde-json" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51a1b5516aee35d6f4271fa86e77aa9a0a20fd3b7a3a5ee0acaaab7364adf62" +checksum = "7ae563f43f788c3fdc7553de60f74780afca2501ebbbb75663661a28be703c93" dependencies = [ "mrubyedge", "serde", @@ -1153,9 +1153,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -1949,7 +1949,8 @@ version = "0.1.0" dependencies = [ "mruby-compiler2-sys", "mrubyedge", - "uzumibi-gem 0.5.0", + "uzumibi-art-router 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "uzumibi-gem 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -2043,9 +2044,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60722a937f594b7fde9adb894d7c092fc1bb6612897c46368d18e7a20208eff2" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -2056,9 +2057,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac8c6395094b6b91c4af293f4c79371c163f9a6f56184d2c9a85f5a95f3950" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2066,9 +2067,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3fabce6159dc20728033842636887e4877688ae94382766e00b180abac9d60" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -2079,9 +2080,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.113" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0e091bdb824da87dc01d967388880d017a0a9bc4f3bdc0d86ee9f9336e3bb5" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -2433,18 +2434,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", diff --git a/uzumibi-on-cloudflare-spike/.gitignore b/uzumibi-on-cloudflare-spike/.gitignore index 1967f73..9ee5478 100644 --- a/uzumibi-on-cloudflare-spike/.gitignore +++ b/uzumibi-on-cloudflare-spike/.gitignore @@ -1,2 +1,4 @@ node_modules/* -/target +wasm-app/target +src/*.wasm +*.mrb \ No newline at end of file diff --git a/uzumibi-on-cloudflare-spike/package.json b/uzumibi-on-cloudflare-spike/package.json index d099a0d..fc3a82a 100644 --- a/uzumibi-on-cloudflare-spike/package.json +++ b/uzumibi-on-cloudflare-spike/package.json @@ -1,5 +1,5 @@ { - "name": "uzumibi-on-cloudflare-spike-wip", + "name": "uzumibi-on-cloudflare-spike", "version": "0.0.0", "private": true, "scripts": { diff --git a/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml b/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml index 86362ca..407a6de 100644 --- a/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml +++ b/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml @@ -7,11 +7,11 @@ edition = "2024" crate-type = ["cdylib", "rlib"] [dependencies] -mrubyedge = { version = ">= 1.1.0", features = [ +mrubyedge = { version = ">= 1.1", features = [ "no-wasi", - "mruby-random", ], default-features = false } -uzumibi-gem = { path = "../../uzumibi-gem" } +uzumibi-gem = ">= 0.5.0" +uzumibi-art-router = ">= 0.3.1" [build-dependencies] -mruby-compiler2-sys = ">= 0.2.2" +mruby-compiler2-sys = ">= 0.3.0" diff --git a/uzumibi-on-cloudflare-spike/wrangler.jsonc b/uzumibi-on-cloudflare-spike/wrangler.jsonc index f599584..c06b321 100644 --- a/uzumibi-on-cloudflare-spike/wrangler.jsonc +++ b/uzumibi-on-cloudflare-spike/wrangler.jsonc @@ -4,7 +4,7 @@ */ { "$schema": "node_modules/wrangler/config-schema.json", - "name": "uzumibi-on-cloudflare-spike-wip", + "name": "uzumibi-on-cloudflare-spike", "main": "src/index.js", "compatibility_date": "2025-12-30", "observability": { From 79a4a4da4f4b1597f8d8018eee76168c8d21cc57 Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Wed, 4 Mar 2026 22:42:11 +0900 Subject: [PATCH 02/11] First walkthrough --- uzumibi-on-cloudflare-spike/package.json | 8 +- uzumibi-on-cloudflare-spike/pnpm-lock.yaml | 31 +++ uzumibi-on-cloudflare-spike/src/index.js | 153 ++++++++--- .../src/index.vanilla.js | 182 +++++++++++++ .../wasm-app/.cargo/config.toml | 12 + .../wasm-app/Cargo.toml | 4 + .../wasm-app/src/lib.rs | 256 +++++++++++++++++- uzumibi-on-cloudflare-spike/wrangler.jsonc | 56 ++-- 8 files changed, 642 insertions(+), 60 deletions(-) create mode 100644 uzumibi-on-cloudflare-spike/src/index.vanilla.js create mode 100644 uzumibi-on-cloudflare-spike/wasm-app/.cargo/config.toml diff --git a/uzumibi-on-cloudflare-spike/package.json b/uzumibi-on-cloudflare-spike/package.json index fc3a82a..3a541e3 100644 --- a/uzumibi-on-cloudflare-spike/package.json +++ b/uzumibi-on-cloudflare-spike/package.json @@ -3,13 +3,17 @@ "version": "0.0.0", "private": true, "scripts": { - "deploy": "wrangler deploy", - "dev": "cargo build --package uzumibi-on-cloudflare-spike --target wasm32-unknown-unknown --release && cp -v -f ../target/wasm32-unknown-unknown/release/uzumibi_on_cloudflare_spike.wasm src/ && wrangler dev", + "deploy": "npm run build:wasm:asyncify && wrangler deploy", + "dev": "npm run build:wasm:asyncify && wrangler dev", + "dev:vanilla": "npm run build:wasm:vanilla && wrangler dev --main src/index.vanilla.js", + "build:wasm:vanilla": "cargo build --package uzumibi-on-cloudflare-spike --target wasm32-unknown-unknown --release && cp -v -f ../target/wasm32-unknown-unknown/release/uzumibi_on_cloudflare_spike.wasm src/", + "build:wasm:asyncify": "cargo build --package uzumibi-on-cloudflare-spike --target wasm32-unknown-unknown --release --features enable-external && wasm-opt --enable-bulk-memory-opt --enable-nontrapping-float-to-int --asyncify -O2 ../target/wasm32-unknown-unknown/release/uzumibi_on_cloudflare_spike.wasm -o ./src/uzumibi_on_cloudflare_spike.wasm", "start": "wrangler dev", "test": "vitest" }, "devDependencies": { "@cloudflare/vitest-pool-workers": "^0.8.19", + "asyncify-wasm": "^1.2.0", "vitest": "~3.2.0", "wrangler": "^4.54.0" } diff --git a/uzumibi-on-cloudflare-spike/pnpm-lock.yaml b/uzumibi-on-cloudflare-spike/pnpm-lock.yaml index d0717bc..f53e06b 100644 --- a/uzumibi-on-cloudflare-spike/pnpm-lock.yaml +++ b/uzumibi-on-cloudflare-spike/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@cloudflare/vitest-pool-workers': specifier: ^0.8.19 version: 0.8.71(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@3.2.4) + asyncify-wasm: + specifier: ^1.2.0 + version: 1.2.1 vitest: specifier: ~3.2.0 version: 3.2.4 @@ -608,67 +611,79 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -740,56 +755,67 @@ packages: resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.54.0': resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.54.0': resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.54.0': resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.54.0': resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.54.0': resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.54.0': resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.54.0': resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.54.0': resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.54.0': resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.54.0': resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openharmony-arm64@4.54.0': resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} @@ -874,6 +900,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + asyncify-wasm@1.2.1: + resolution: {integrity: sha512-ZS7tZ8H8EVbUxAZnkKHvMt9UkYoALue2jwrVl7cnLByjq+1MRrbq7rL5TS+EHQduwkfXD/cNZNa+I0ZyLEBBRQ==} + birpc@0.2.14: resolution: {integrity: sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA==} @@ -1798,6 +1827,8 @@ snapshots: assertion-error@2.0.1: {} + asyncify-wasm@1.2.1: {} + birpc@0.2.14: {} blake3-wasm@2.1.5: {} diff --git a/uzumibi-on-cloudflare-spike/src/index.js b/uzumibi-on-cloudflare-spike/src/index.js index e61e534..406f3dd 100644 --- a/uzumibi-on-cloudflare-spike/src/index.js +++ b/uzumibi-on-cloudflare-spike/src/index.js @@ -1,46 +1,139 @@ +import { DurableObject } from "cloudflare:workers"; +import { instantiate } from "asyncify-wasm"; import mod from "./uzumibi_on_cloudflare_spike.wasm"; -const importObject = { - env: { - debug_console_log: (ptr, size) => { - const memory = exports.memory; - let str = ""; - const buffer = new Uint8Array(memory.buffer); - for (let i = ptr; i < ptr + size; i++) { - str += String.fromCharCode(buffer[i]); - } - console.log(`[debug]: ${str}`); - return 0; - }, - }, -}; -const instance = await WebAssembly.instantiate(mod, importObject); -const exports = instance.exports; +const wasmModule = mod; + +/** + * Durable Object for Uzumibi::KV storage + */ +export class UzumibiKVObject extends DurableObject { + async get(key) { + const value = await this.ctx.storage.get(key); + return value ?? null; + } + + async set(key, value) { + await this.ctx.storage.put(key, value); + } +} export default { async fetch(request, env, ctx) { - const reqResult = exports.uzumibi_initialize_request(65536); + const path = new URL(request.url).pathname; + if (path === "/favicon.ico") { + return new Response(null, { status: 404 }); + } + + const query = new URL(request.url).searchParams; + + // Durable Object stub (if binding exists) + const doStub = env.UZUMIBI_KV_DATA + ? env.UZUMIBI_KV_DATA.getByName("default") + : null; + + const decoder = new TextDecoder(); + const encoder = new TextEncoder(); + + const importObject = { + env: { + debug_console_log: (ptr, size) => { + const memory = exports.memory; + const buffer = new Uint8Array(memory.buffer, ptr, size); + console.log(`[debug]: ${decoder.decode(buffer)}`); + return 0; + }, + + // Fetch.fetch(url, method, body) -> response body string + uzumibi_cf_fetch: async ( + urlPtr, urlSize, + methodPtr, methodSize, + bodyPtr, bodySize, + resultPtr, resultMaxSize + ) => { + const memory = exports.memory; + const url = decoder.decode(new Uint8Array(memory.buffer, urlPtr, urlSize)); + const method = decoder.decode(new Uint8Array(memory.buffer, methodPtr, methodSize)); + const body = bodySize > 0 + ? decoder.decode(new Uint8Array(memory.buffer, bodyPtr, bodySize)) + : null; + + const fetchOptions = { method }; + if (body && method !== "GET" && method !== "HEAD") { + fetchOptions.body = body; + } + + const response = await fetch(url, fetchOptions); + const responseText = await response.text(); + const responseBytes = encoder.encode(responseText); + const length = Math.min(responseBytes.length, resultMaxSize); + const resultBuffer = new Uint8Array(memory.buffer, resultPtr, resultMaxSize); + resultBuffer.set(responseBytes.slice(0, length)); + return length; + }, + + // KV.get(key) -> value string (via Durable Object) + uzumibi_cf_durable_object_get: async (keyPtr, keySize, resultPtr, resultMaxSize) => { + if (!doStub) return -1; + const memory = exports.memory; + const key = decoder.decode(new Uint8Array(memory.buffer, keyPtr, keySize)); + + const value = await doStub.get(key); + if (value === null) { + return -1; + } + const valueBytes = encoder.encode(value); + const length = Math.min(valueBytes.length, resultMaxSize); + const resultBuffer = new Uint8Array(memory.buffer, resultPtr, resultMaxSize); + resultBuffer.set(valueBytes.slice(0, length)); + return length; + }, + + // KV.set(key, value) (via Durable Object) + uzumibi_cf_durable_object_set: async (keyPtr, keySize, valuePtr, valueSize) => { + if (!doStub) return -1; + const memory = exports.memory; + const key = decoder.decode(new Uint8Array(memory.buffer, keyPtr, keySize)); + const value = decoder.decode(new Uint8Array(memory.buffer, valuePtr, valueSize)); + + await doStub.set(key, value); + return 0; + }, + + // Queue.send(queue_name, message) + uzumibi_cf_queue_send: async (queueNamePtr, queueNameSize, messagePtr, messageSize) => { + const memory = exports.memory; + const queueName = decoder.decode(new Uint8Array(memory.buffer, queueNamePtr, queueNameSize)); + const message = decoder.decode(new Uint8Array(memory.buffer, messagePtr, messageSize)); + + const queue = env[queueName]; + if (!queue) { + console.error(`Queue binding '${queueName}' not found`); + return -1; + } + await queue.send(message); + return 0; + }, + }, + }; + + const instance = await instantiate(wasmModule, importObject); + const exports = instance.exports; + + const reqResult = await exports.uzumibi_initialize_request(65536); const reqOffset = Number(reqResult & 0xFFFFFFFFn); if (reqOffset === 0) { const errOffset = Number((reqResult >> 32n) & 0xFFFFFFFFn); - const decoder = new TextDecoder(); - let errStr = ""; const buffer = new Uint8Array(exports.memory.buffer, errOffset); + let errStr = ""; for (let i = 0; buffer[i] !== 0; i++) { errStr += String.fromCharCode(buffer[i]); } throw new Error(`Failed to initialize request: ${errStr}`); } const requestBuffer = new Uint8Array(exports.memory.buffer, reqOffset, 65536); - const path = new URL(request.url).pathname; - if (path === "/favicon.ico") { - return new Response(null, { status: 404 }); - } - - const query = new URL(request.url).searchParams; let pos = 0; - const encoder = new TextEncoder(); const dataView = new DataView(exports.memory.buffer, reqOffset); const method = encoder.encode(request.method); @@ -70,7 +163,6 @@ export default { // Headers const headers = []; request.headers.forEach((value, key) => { - // 一般的なヘッダーのみ含める(必要に応じて調整) if (key.toLowerCase() !== 'cf-connecting-ip' && key.toLowerCase() !== 'cf-ray' && !key.toLowerCase().startsWith('x-')) { @@ -116,13 +208,12 @@ export default { throw new Error("Request data exceeds allocated buffer size"); } - const resResult = exports.uzumibi_start_request(); + const resResult = await exports.uzumibi_start_request(); const resOffset = Number(resResult & 0xFFFFFFFFn); if (resOffset === 0) { const errOffset = Number((resResult >> 32n) & 0xFFFFFFFFn); - const decoder = new TextDecoder(); - let errStr = ""; const buffer = new Uint8Array(exports.memory.buffer, errOffset); + let errStr = ""; for (let i = 0; buffer[i] !== 0; i++) { errStr += String.fromCharCode(buffer[i]); } @@ -130,10 +221,8 @@ export default { } // Unpack response - const decoder = new TextDecoder(); const resDataView = new DataView(exports.memory.buffer, resOffset); - let resPos = 0; // Status code (u16 little-endian) diff --git a/uzumibi-on-cloudflare-spike/src/index.vanilla.js b/uzumibi-on-cloudflare-spike/src/index.vanilla.js new file mode 100644 index 0000000..e61e534 --- /dev/null +++ b/uzumibi-on-cloudflare-spike/src/index.vanilla.js @@ -0,0 +1,182 @@ +import mod from "./uzumibi_on_cloudflare_spike.wasm"; + +const importObject = { + env: { + debug_console_log: (ptr, size) => { + const memory = exports.memory; + let str = ""; + const buffer = new Uint8Array(memory.buffer); + for (let i = ptr; i < ptr + size; i++) { + str += String.fromCharCode(buffer[i]); + } + console.log(`[debug]: ${str}`); + return 0; + }, + }, +}; +const instance = await WebAssembly.instantiate(mod, importObject); +const exports = instance.exports; + +export default { + async fetch(request, env, ctx) { + const reqResult = exports.uzumibi_initialize_request(65536); + const reqOffset = Number(reqResult & 0xFFFFFFFFn); + if (reqOffset === 0) { + const errOffset = Number((reqResult >> 32n) & 0xFFFFFFFFn); + const decoder = new TextDecoder(); + let errStr = ""; + const buffer = new Uint8Array(exports.memory.buffer, errOffset); + for (let i = 0; buffer[i] !== 0; i++) { + errStr += String.fromCharCode(buffer[i]); + } + throw new Error(`Failed to initialize request: ${errStr}`); + } + const requestBuffer = new Uint8Array(exports.memory.buffer, reqOffset, 65536); + const path = new URL(request.url).pathname; + if (path === "/favicon.ico") { + return new Response(null, { status: 404 }); + } + + const query = new URL(request.url).searchParams; + + let pos = 0; + const encoder = new TextEncoder(); + const dataView = new DataView(exports.memory.buffer, reqOffset); + + const method = encoder.encode(request.method); + requestBuffer.fill(0, pos, pos + 6); + requestBuffer.set(method.slice(0, 6), pos); + pos += 6; + + // Path size (u16 little-endian) + const pathBytes = encoder.encode(path); + dataView.setUint16(pos, pathBytes.length, true); + pos += 2; + + // Path + requestBuffer.set(pathBytes, pos); + pos += pathBytes.length; + + // Query string size (u16 little-endian) + const queryString = query.toString(); + const queryBytes = encoder.encode(queryString); + dataView.setUint16(pos, queryBytes.length, true); + pos += 2; + + // Query string + requestBuffer.set(queryBytes, pos); + pos += queryBytes.length; + + // Headers + const headers = []; + request.headers.forEach((value, key) => { + // 一般的なヘッダーのみ含める(必要に応じて調整) + if (key.toLowerCase() !== 'cf-connecting-ip' && + key.toLowerCase() !== 'cf-ray' && + !key.toLowerCase().startsWith('x-')) { + headers.push({ key, value }); + } + }); + + // Headers count (u16 little-endian) + dataView.setUint16(pos, headers.length, true); + pos += 2; + + // Each header + for (const header of headers) { + // Header key size (u16 little-endian) + const keyBytes = encoder.encode(header.key); + dataView.setUint16(pos, keyBytes.length, true); + pos += 2; + + // Header key + requestBuffer.set(keyBytes, pos); + pos += keyBytes.length; + + // Header value size (u16 little-endian) + const valueBytes = encoder.encode(header.value); + dataView.setUint16(pos, valueBytes.length, true); + pos += 2; + + // Header value + requestBuffer.set(valueBytes, pos); + pos += valueBytes.length; + } + + // Request body size (u32 little-endian) + const bodyBytes = request.body ? new Uint8Array(await request.arrayBuffer()) : new Uint8Array(0); + dataView.setUint32(pos, bodyBytes.length, true); + pos += 4; + + // Request body + requestBuffer.set(bodyBytes, pos); + pos += bodyBytes.length; + + if (pos > 65536) { + throw new Error("Request data exceeds allocated buffer size"); + } + + const resResult = exports.uzumibi_start_request(); + const resOffset = Number(resResult & 0xFFFFFFFFn); + if (resOffset === 0) { + const errOffset = Number((resResult >> 32n) & 0xFFFFFFFFn); + const decoder = new TextDecoder(); + let errStr = ""; + const buffer = new Uint8Array(exports.memory.buffer, errOffset); + for (let i = 0; buffer[i] !== 0; i++) { + errStr += String.fromCharCode(buffer[i]); + } + throw new Error(`Failed to start request: ${errStr}`); + } + + // Unpack response + const decoder = new TextDecoder(); + const resDataView = new DataView(exports.memory.buffer, resOffset); + + + let resPos = 0; + + // Status code (u16 little-endian) + const statusCode = resDataView.getUint16(resPos, true); + resPos += 2; + + // Headers count (u16 little-endian) + const headersCount = resDataView.getUint16(resPos, true); + resPos += 2; + + // Parse headers + const responseHeaders = new Headers(); + for (let i = 0; i < headersCount; i++) { + // Header key size (u16 little-endian) + const keySize = resDataView.getUint16(resPos, true); + resPos += 2; + + // Header key + const keyBytes = new Uint8Array(exports.memory.buffer, resOffset + resPos, keySize); + const key = decoder.decode(keyBytes); + resPos += keySize; + + // Header value size (u16 little-endian) + const valueSize = resDataView.getUint16(resPos, true); + resPos += 2; + + // Header value + const valueBytes = new Uint8Array(exports.memory.buffer, resOffset + resPos, valueSize); + const value = decoder.decode(valueBytes); + resPos += valueSize; + + console.log(`[Response Header] ${key}: ${value}`); + responseHeaders.set(key, value); + } + + // Body size (u32 little-endian) + const bodySize = resDataView.getUint32(resPos, true); + resPos += 4; + + // Body + const bodyBuffer = new Uint8Array(exports.memory.buffer, resOffset + resPos, bodySize); + const responseText = decoder.decode(bodyBuffer); + + return new Response(responseText, { status: statusCode, headers: responseHeaders }); + } +}; diff --git a/uzumibi-on-cloudflare-spike/wasm-app/.cargo/config.toml b/uzumibi-on-cloudflare-spike/wasm-app/.cargo/config.toml new file mode 100644 index 0000000..cef1057 --- /dev/null +++ b/uzumibi-on-cloudflare-spike/wasm-app/.cargo/config.toml @@ -0,0 +1,12 @@ +[target.wasm32-unknown-unknown] +rustflags = [ + "-C", + "target-feature=+bulk-memory,+mutable-globals", + "-C", + "link-arg=--import-memory", + "-C", + "link-arg=--export-memory", +] + +[build] +target = "wasm32-unknown-unknown" diff --git a/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml b/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml index 407a6de..221a984 100644 --- a/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml +++ b/uzumibi-on-cloudflare-spike/wasm-app/Cargo.toml @@ -15,3 +15,7 @@ uzumibi-art-router = ">= 0.3.1" [build-dependencies] mruby-compiler2-sys = ">= 0.3.0" + +[features] +default = ["enable-external"] +enable-external = [] diff --git a/uzumibi-on-cloudflare-spike/wasm-app/src/lib.rs b/uzumibi-on-cloudflare-spike/wasm-app/src/lib.rs index 5714d38..90c75f1 100644 --- a/uzumibi-on-cloudflare-spike/wasm-app/src/lib.rs +++ b/uzumibi-on-cloudflare-spike/wasm-app/src/lib.rs @@ -7,7 +7,7 @@ use std::{mem::MaybeUninit, rc::Rc}; use mrubyedge::{ rite::rite, yamrb::{ - helpers::{mrb_define_cmethod, mrb_funcall}, + helpers::{mrb_define_class_cmethod, mrb_define_cmethod, mrb_funcall}, value::{RObject, RValue}, vm::VM, }, @@ -34,12 +34,134 @@ unsafe extern "C" { unsafe fn debug_console_log(ptr: *const u8, len: usize); } +#[cfg(feature = "enable-external")] +unsafe extern "C" { + unsafe fn uzumibi_cf_fetch( + url_ptr: *const u8, + url_size: usize, + method_ptr: *const u8, + method_size: usize, + body_ptr: *const u8, + body_size: usize, + result_ptr: *mut u8, + result_max_size: usize, + ) -> i32; + unsafe fn uzumibi_cf_durable_object_get( + key_ptr: *const u8, + key_size: usize, + result_ptr: *mut u8, + result_max_size: usize, + ) -> i32; + unsafe fn uzumibi_cf_durable_object_set( + key_ptr: *const u8, + key_size: usize, + value_ptr: *const u8, + value_size: usize, + ) -> i32; + unsafe fn uzumibi_cf_queue_send( + queue_name_ptr: *const u8, + queue_name_size: usize, + message_ptr: *const u8, + message_size: usize, + ) -> i32; +} + fn debug_console_log_internal(message: &str) { unsafe { debug_console_log(message.as_ptr(), message.len()); } } +// ---- External API wrappers (only when enable-external feature is active) ---- + +#[cfg(feature = "enable-external")] +fn cf_fetch(url: &str, method: &str, body: &str) -> Result { + const BUFFER_SIZE: usize = 65536; + let mut buffer = vec![0u8; BUFFER_SIZE]; + + unsafe { + let result = uzumibi_cf_fetch( + url.as_ptr(), + url.len(), + method.as_ptr(), + method.len(), + body.as_ptr(), + body.len(), + buffer.as_mut_ptr(), + BUFFER_SIZE, + ); + match result { + len if len >= 0 => { + let len = len as usize; + String::from_utf8(buffer[..len].to_vec()) + .map_err(|e| format!("Failed to decode UTF-8: {}", e)) + } + _ => Err(format!("Fetch failed with return code: {}", result)), + } + } +} + +#[cfg(feature = "enable-external")] +fn cf_durable_object_get(key: &str) -> Result, String> { + const BUFFER_SIZE: usize = 65536; + let mut buffer = vec![0u8; BUFFER_SIZE]; + + unsafe { + let result = uzumibi_cf_durable_object_get( + key.as_ptr(), + key.len(), + buffer.as_mut_ptr(), + BUFFER_SIZE, + ); + match result { + -1 => Ok(None), + len if len >= 0 => { + let len = len as usize; + let value = String::from_utf8(buffer[..len].to_vec()) + .map_err(|e| format!("Failed to decode UTF-8: {}", e))?; + Ok(Some(value)) + } + _ => Err(format!( + "Unexpected return value from durable_object_get: {}", + result + )), + } + } +} + +#[cfg(feature = "enable-external")] +fn cf_durable_object_set(key: &str, value: &str) -> Result<(), String> { + unsafe { + let result = + uzumibi_cf_durable_object_set(key.as_ptr(), key.len(), value.as_ptr(), value.len()); + match result { + 0 => Ok(()), + _ => Err(format!("Failed to set value: return code {}", result)), + } + } +} + +#[cfg(feature = "enable-external")] +fn cf_queue_send(queue_name: &str, message: &str) -> Result<(), String> { + unsafe { + let result = uzumibi_cf_queue_send( + queue_name.as_ptr(), + queue_name.len(), + message.as_ptr(), + message.len(), + ); + match result { + 0 => Ok(()), + _ => Err(format!( + "Failed to send queue message: return code {}", + result + )), + } + } +} + +// ---- mruby gem method implementations ---- + fn uzumibi_kernel_debug_console_log( vm: &mut VM, args: &[Rc], @@ -53,6 +175,105 @@ fn uzumibi_kernel_debug_console_log( Ok(RObject::nil().to_refcount_assigned()) } +/// Fetch.fetch(url, method="GET", body="") +#[cfg(feature = "enable-external")] +fn uzumibi_fetch_class_fetch( + vm: &mut VM, + args: &[Rc], +) -> Result, mrubyedge::Error> { + let url_obj = &args[0]; + let url = mrb_funcall(vm, url_obj.clone().into(), "to_s", &[])?; + let url: String = url.as_ref().try_into()?; + + let method = if args.len() > 1 { + let m = mrb_funcall(vm, args[1].clone().into(), "to_s", &[])?; + let m: String = m.as_ref().try_into()?; + m + } else { + "GET".to_string() + }; + + let body = if args.len() > 2 { + let b = mrb_funcall(vm, args[2].clone().into(), "to_s", &[])?; + let b: String = b.as_ref().try_into()?; + b + } else { + String::new() + }; + + match cf_fetch(&url, &method, &body) { + Ok(response) => Ok(RObject::string(response).to_refcount_assigned()), + Err(e) => Err(mrubyedge::Error::RuntimeError(format!( + "Fetch failed: {}", + e + ))), + } +} + +/// KV.get(key) +#[cfg(feature = "enable-external")] +fn uzumibi_kv_class_get( + vm: &mut VM, + args: &[Rc], +) -> Result, mrubyedge::Error> { + let key_obj = &args[0]; + let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?; + let key: String = key.as_ref().try_into()?; + + match cf_durable_object_get(&key) { + Ok(Some(value)) => Ok(RObject::string(value).to_refcount_assigned()), + Ok(None) => Ok(RObject::nil().to_refcount_assigned()), + Err(e) => Err(mrubyedge::Error::RuntimeError(format!( + "Failed to access storage value: {}", + e + ))), + } +} + +/// KV.set(key, value) +#[cfg(feature = "enable-external")] +fn uzumibi_kv_class_set( + vm: &mut VM, + args: &[Rc], +) -> Result, mrubyedge::Error> { + let key_obj = &args[0]; + let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?; + let key: String = key.as_ref().try_into()?; + + let value_obj = &args[1]; + let value = mrb_funcall(vm, value_obj.clone().into(), "to_s", &[])?; + let value: String = value.as_ref().try_into()?; + + cf_durable_object_set(&key, &value).map_err(|e| { + mrubyedge::Error::RuntimeError(format!("Failed to set storage value: {}", e)) + })?; + + Ok(RObject::boolean(true).to_refcount_assigned()) +} + +/// Queue.send(queue_name, message) +#[cfg(feature = "enable-external")] +fn uzumibi_queue_class_send( + vm: &mut VM, + args: &[Rc], +) -> Result, mrubyedge::Error> { + let queue_name_obj = &args[0]; + let queue_name = mrb_funcall(vm, queue_name_obj.clone().into(), "to_s", &[])?; + let queue_name: String = queue_name.as_ref().try_into()?; + + let message_obj = &args[1]; + let message = mrb_funcall(vm, message_obj.clone().into(), "to_s", &[])?; + let message: String = message.as_ref().try_into()?; + + cf_queue_send(&queue_name, &message).map_err(|e| { + mrubyedge::Error::RuntimeError(format!("Failed to send queue message: {}", e)) + })?; + + Ok(RObject::boolean(true).to_refcount_assigned()) +} + +// ---- VM initialization ---- + fn init_vm() -> Result { let mut rite = rite::load(MRB) .map_err(|e| mrubyedge::Error::RuntimeError(format!("Failed to load mruby: {:?}", e)))?; @@ -66,6 +287,39 @@ fn init_vm() -> Result { Box::new(uzumibi_kernel_debug_console_log), ); + #[cfg(feature = "enable-external")] + { + let uzumibi_module = vm.get_module_by_name("Uzumibi"); + + // Uzumibi::Fetch.fetch(url, method="GET", body="") + let fetch_class = vm.define_class("Fetch", None, Some(uzumibi_module.clone())); + mrb_define_class_cmethod( + &mut vm, + fetch_class, + "fetch", + Box::new(uzumibi_fetch_class_fetch), + ); + + // Uzumibi::KV.get(key) / Uzumibi::KV.set(key, value) + let kv_class = vm.define_class("KV", None, Some(uzumibi_module.clone())); + mrb_define_class_cmethod( + &mut vm, + kv_class.clone(), + "get", + Box::new(uzumibi_kv_class_get), + ); + mrb_define_class_cmethod(&mut vm, kv_class, "set", Box::new(uzumibi_kv_class_set)); + + // Uzumibi::Queue.send(queue_name, message) + let queue_class = vm.define_class("Queue", None, Some(uzumibi_module)); + mrb_define_class_cmethod( + &mut vm, + queue_class, + "send", + Box::new(uzumibi_queue_class_send), + ); + } + vm.run() .map_err(|e| mrubyedge::Error::RuntimeError(format!("Failed to init VM: {:?}", e)))?; diff --git a/uzumibi-on-cloudflare-spike/wrangler.jsonc b/uzumibi-on-cloudflare-spike/wrangler.jsonc index c06b321..02708ec 100644 --- a/uzumibi-on-cloudflare-spike/wrangler.jsonc +++ b/uzumibi-on-cloudflare-spike/wrangler.jsonc @@ -9,33 +9,39 @@ "compatibility_date": "2025-12-30", "observability": { "enabled": true - } - /** - * Smart Placement - * https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement - */ - // "placement": { "mode": "smart" } - /** - * Bindings - * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including - * databases, object storage, AI inference, real-time communication and more. - * https://developers.cloudflare.com/workers/runtime-apis/bindings/ - */ - /** - * Environment Variables - * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables - * Note: Use secrets to store sensitive data. - * https://developers.cloudflare.com/workers/configuration/secrets/ - */ - // "vars": { "MY_VARIABLE": "production_value" } + }, /** - * Static Assets - * https://developers.cloudflare.com/workers/static-assets/binding/ + * Durable Objects + * Used for Uzumibi::KV.get/set + * https://developers.cloudflare.com/durable-objects/ */ - // "assets": { "directory": "./public/", "binding": "ASSETS" } + "durable_objects": { + "bindings": [ + { + "name": "UZUMIBI_KV_DATA", + "class_name": "UzumibiKVObject" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": [ + "UzumibiKVObject" + ] + } + ], /** - * Service Bindings (communicate between multiple Workers) - * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings + * Queues + * Used for Uzumibi::Queue.send + * https://developers.cloudflare.com/queues/ */ - // "services": [ { "binding": "MY_SERVICE", "service": "my-service" } ] + "queues": { + "producers": [ + { + "binding": "MY_QUEUE", + "queue": "my-queue-name" + } + ] + } } \ No newline at end of file From a80403f072c965466b0468b67d75c07894175e40 Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Wed, 4 Mar 2026 22:47:20 +0900 Subject: [PATCH 03/11] Sample endpoint to use fetch --- uzumibi-on-cloudflare-spike/lib/app.rb | 11 +++++++++++ uzumibi-on-cloudflare-spike/wrangler.jsonc | 18 +++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/uzumibi-on-cloudflare-spike/lib/app.rb b/uzumibi-on-cloudflare-spike/lib/app.rb index 4ae7b70..0ff837f 100644 --- a/uzumibi-on-cloudflare-spike/lib/app.rb +++ b/uzumibi-on-cloudflare-spike/lib/app.rb @@ -24,6 +24,17 @@ class App < Uzumibi::Router res end + get "/yesno" do |req, res| + body = Uzumibi::Fetch.fetch("https://yesno.wtf/api", "GET", "") + + res.status_code = 200 + res.headers = { + "Content-Type" => "application/json", + } + res.body = body + res + end + get "/healthz" do |req, res| res.status_code = 200 res.headers = { diff --git a/uzumibi-on-cloudflare-spike/wrangler.jsonc b/uzumibi-on-cloudflare-spike/wrangler.jsonc index 02708ec..0dab6d5 100644 --- a/uzumibi-on-cloudflare-spike/wrangler.jsonc +++ b/uzumibi-on-cloudflare-spike/wrangler.jsonc @@ -30,18 +30,18 @@ "UzumibiKVObject" ] } - ], + ] /** * Queues * Used for Uzumibi::Queue.send * https://developers.cloudflare.com/queues/ */ - "queues": { - "producers": [ - { - "binding": "MY_QUEUE", - "queue": "my-queue-name" - } - ] - } + // "queues": { + // "producers": [ + // { + // "binding": "MY_QUEUE", + // "queue": "my-queue-name" + // } + // ] + // } } \ No newline at end of file From afad529bee5e486ebf219502ac3a42871586e91b Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Wed, 4 Mar 2026 23:55:30 +0900 Subject: [PATCH 04/11] Handle fetch response metadata --- uzumibi-on-cloudflare-spike/lib/app.rb | 6 +- uzumibi-on-cloudflare-spike/src/index.js | 54 ++++++++-- .../wasm-app/src/lib.rs | 98 +++++++++++++++++-- 3 files changed, 140 insertions(+), 18 deletions(-) diff --git a/uzumibi-on-cloudflare-spike/lib/app.rb b/uzumibi-on-cloudflare-spike/lib/app.rb index 0ff837f..1d55526 100644 --- a/uzumibi-on-cloudflare-spike/lib/app.rb +++ b/uzumibi-on-cloudflare-spike/lib/app.rb @@ -25,13 +25,15 @@ class App < Uzumibi::Router end get "/yesno" do |req, res| - body = Uzumibi::Fetch.fetch("https://yesno.wtf/api", "GET", "") + remote_res = Uzumibi::Fetch.fetch("https://yesno.wtf/api", "GET", "") + debug_console("[Uzumibi] Fetched from yesno.wtf: #{remote_res.status_code}") + debug_console("[Uzumibi] headers: #{remote_res.headers.inspect}") res.status_code = 200 res.headers = { "Content-Type" => "application/json", } - res.body = body + res.body = remote_res.body res end diff --git a/uzumibi-on-cloudflare-spike/src/index.js b/uzumibi-on-cloudflare-spike/src/index.js index 406f3dd..8f3920f 100644 --- a/uzumibi-on-cloudflare-spike/src/index.js +++ b/uzumibi-on-cloudflare-spike/src/index.js @@ -44,7 +44,8 @@ export default { return 0; }, - // Fetch.fetch(url, method, body) -> response body string + // Fetch.fetch(url, method, body) -> packed Uzumibi::Response + // Format: u16 status | u16 headers_count | (u16 key_size, key, u16 value_size, value)... | u32 body_size | body uzumibi_cf_fetch: async ( urlPtr, urlSize, methodPtr, methodSize, @@ -64,12 +65,53 @@ export default { } const response = await fetch(url, fetchOptions); - const responseText = await response.text(); - const responseBytes = encoder.encode(responseText); - const length = Math.min(responseBytes.length, resultMaxSize); + const responseBody = await response.text(); + + // Collect response headers + const respHeaders = []; + response.headers.forEach((value, key) => { + respHeaders.push({ key, value }); + }); + + // Pack into binary format matching Uzumibi::Response#to_shared_memory + const resultView = new DataView(memory.buffer, resultPtr, resultMaxSize); const resultBuffer = new Uint8Array(memory.buffer, resultPtr, resultMaxSize); - resultBuffer.set(responseBytes.slice(0, length)); - return length; + let pos = 0; + + // Status code (u16 LE) + resultView.setUint16(pos, response.status, true); + pos += 2; + + // Headers count (u16 LE) + resultView.setUint16(pos, respHeaders.length, true); + pos += 2; + + // Each header + for (const header of respHeaders) { + const keyBytes = encoder.encode(header.key); + resultView.setUint16(pos, keyBytes.length, true); + pos += 2; + resultBuffer.set(keyBytes, pos); + pos += keyBytes.length; + + const valueBytes = encoder.encode(header.value); + resultView.setUint16(pos, valueBytes.length, true); + pos += 2; + resultBuffer.set(valueBytes, pos); + pos += valueBytes.length; + } + + // Body size (u32 LE) + const bodyBytes = encoder.encode(responseBody); + resultView.setUint32(pos, bodyBytes.length, true); + pos += 4; + + // Body + const bodyLen = Math.min(bodyBytes.length, resultMaxSize - pos); + resultBuffer.set(bodyBytes.slice(0, bodyLen), pos); + pos += bodyLen; + + return pos; }, // KV.get(key) -> value string (via Durable Object) diff --git a/uzumibi-on-cloudflare-spike/wasm-app/src/lib.rs b/uzumibi-on-cloudflare-spike/wasm-app/src/lib.rs index 90c75f1..de76de9 100644 --- a/uzumibi-on-cloudflare-spike/wasm-app/src/lib.rs +++ b/uzumibi-on-cloudflare-spike/wasm-app/src/lib.rs @@ -8,6 +8,7 @@ use mrubyedge::{ rite::rite, yamrb::{ helpers::{mrb_define_class_cmethod, mrb_define_cmethod, mrb_funcall}, + prelude::hash::{mrb_hash_new, mrb_hash_set_index}, value::{RObject, RValue}, vm::VM, }, @@ -74,8 +75,14 @@ fn debug_console_log_internal(message: &str) { // ---- External API wrappers (only when enable-external feature is active) ---- +/// Packed response format (same as Uzumibi::Response#to_shared_memory): +/// u16 LE status_code +/// u16 LE headers_count +/// (u16 LE key_size, key bytes, u16 LE value_size, value bytes) * headers_count +/// u32 LE body_size +/// body bytes #[cfg(feature = "enable-external")] -fn cf_fetch(url: &str, method: &str, body: &str) -> Result { +fn cf_fetch(url: &str, method: &str, body: &str) -> Result, String> { const BUFFER_SIZE: usize = 65536; let mut buffer = vec![0u8; BUFFER_SIZE]; @@ -93,8 +100,7 @@ fn cf_fetch(url: &str, method: &str, body: &str) -> Result { match result { len if len >= 0 => { let len = len as usize; - String::from_utf8(buffer[..len].to_vec()) - .map_err(|e| format!("Failed to decode UTF-8: {}", e)) + Ok(buffer[..len].to_vec()) } _ => Err(format!("Fetch failed with return code: {}", result)), } @@ -175,7 +181,7 @@ fn uzumibi_kernel_debug_console_log( Ok(RObject::nil().to_refcount_assigned()) } -/// Fetch.fetch(url, method="GET", body="") +/// Fetch.fetch(url, method="GET", body="") -> Uzumibi::Response #[cfg(feature = "enable-external")] fn uzumibi_fetch_class_fetch( vm: &mut VM, @@ -201,13 +207,85 @@ fn uzumibi_fetch_class_fetch( String::new() }; - match cf_fetch(&url, &method, &body) { - Ok(response) => Ok(RObject::string(response).to_refcount_assigned()), - Err(e) => Err(mrubyedge::Error::RuntimeError(format!( - "Fetch failed: {}", - e - ))), + let packed = cf_fetch(&url, &method, &body) + .map_err(|e| mrubyedge::Error::RuntimeError(format!("Fetch failed: {}", e)))?; + + // Unpack the packed response into Uzumibi::Response + unpack_response_to_robject(vm, &packed) +} + +/// Unpack packed binary response into Uzumibi::Response mruby object +#[cfg(feature = "enable-external")] +fn unpack_response_to_robject(vm: &mut VM, buf: &[u8]) -> Result, mrubyedge::Error> { + let mut offset = 0; + + // Status code (u16 LE) + let status_code = u16::from_le_bytes([buf[offset], buf[offset + 1]]); + offset += 2; + + // Headers count (u16 LE) + let headers_count = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize; + offset += 2; + + // Parse headers + let headers_hash = mrb_hash_new(vm, &[])?; + for _ in 0..headers_count { + let key_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize; + offset += 2; + let key = String::from_utf8_lossy(&buf[offset..offset + key_size]).to_string(); + offset += key_size; + + let value_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize; + offset += 2; + let value = String::from_utf8_lossy(&buf[offset..offset + value_size]).to_string(); + offset += value_size; + + mrb_hash_set_index( + headers_hash.clone(), + RObject::string(key).to_refcount_assigned(), + RObject::string(value).to_refcount_assigned(), + )?; } + + // Body size (u32 LE) + let body_size = u32::from_le_bytes([ + buf[offset], + buf[offset + 1], + buf[offset + 2], + buf[offset + 3], + ]) as usize; + offset += 4; + + // Body + let body = String::from_utf8_lossy(&buf[offset..offset + body_size]).to_string(); + + // Create Uzumibi::Response instance + let uzumibi = vm + .get_const_by_name("Uzumibi") + .ok_or_else(|| mrubyedge::Error::RuntimeError("Uzumibi module not found".to_string()))?; + let uzumibi_module = match &uzumibi.as_ref().value { + RValue::Module(m) => m.clone(), + _ => { + return Err(mrubyedge::Error::RuntimeError( + "Uzumibi must be a module".to_string(), + )); + } + }; + let response_class = uzumibi_module + .get_const_by_name("Response") + .ok_or_else(|| { + mrubyedge::Error::RuntimeError("Uzumibi::Response class not found".to_string()) + })?; + let response = mrb_funcall(vm, Some(response_class), "new", &[])?; + + response.set_ivar( + "@status_code", + RObject::integer(status_code as i64).to_refcount_assigned(), + ); + response.set_ivar("@headers", headers_hash); + response.set_ivar("@body", RObject::string(body).to_refcount_assigned()); + + Ok(response) } /// KV.get(key) From 2a9598a54735f377f7cd3267f60956a14bec4adb Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Thu, 5 Mar 2026 00:00:25 +0900 Subject: [PATCH 05/11] Small fix --- uzumibi-on-cloudflare-spike/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uzumibi-on-cloudflare-spike/package.json b/uzumibi-on-cloudflare-spike/package.json index 3a541e3..ce766cd 100644 --- a/uzumibi-on-cloudflare-spike/package.json +++ b/uzumibi-on-cloudflare-spike/package.json @@ -6,7 +6,7 @@ "deploy": "npm run build:wasm:asyncify && wrangler deploy", "dev": "npm run build:wasm:asyncify && wrangler dev", "dev:vanilla": "npm run build:wasm:vanilla && wrangler dev --main src/index.vanilla.js", - "build:wasm:vanilla": "cargo build --package uzumibi-on-cloudflare-spike --target wasm32-unknown-unknown --release && cp -v -f ../target/wasm32-unknown-unknown/release/uzumibi_on_cloudflare_spike.wasm src/", + "build:wasm:vanilla": "cargo build --package uzumibi-on-cloudflare-spike --target wasm32-unknown-unknown --release --no-default-features && cp -v -f ../target/wasm32-unknown-unknown/release/uzumibi_on_cloudflare_spike.wasm src/", "build:wasm:asyncify": "cargo build --package uzumibi-on-cloudflare-spike --target wasm32-unknown-unknown --release --features enable-external && wasm-opt --enable-bulk-memory-opt --enable-nontrapping-float-to-int --asyncify -O2 ../target/wasm32-unknown-unknown/release/uzumibi_on_cloudflare_spike.wasm -o ./src/uzumibi_on_cloudflare_spike.wasm", "start": "wrangler dev", "test": "vitest" From d896d1bf8e7a83016e39457e358465d1f8661c20 Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Thu, 5 Mar 2026 00:23:19 +0900 Subject: [PATCH 06/11] Support cloudflare services --- uzumibi-cli/src/main.rs | 103 +++- .../__features__/enable-external/package.json | 18 + .../__features__/enable-external/src/index.js | 313 ++++++++++++ .../wasm-app/.cargo/config.toml | 12 + .../enable-external/wasm-app/Cargo.toml_ | 21 + .../enable-external/wasm-app/src/lib.rs | 469 ++++++++++++++++++ .../enable-external/wrangler.jsonc | 47 ++ 7 files changed, 978 insertions(+), 5 deletions(-) create mode 100644 uzumibi-cli/templates/cloudflare/__features__/enable-external/package.json create mode 100644 uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js create mode 100644 uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/.cargo/config.toml create mode 100644 uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/Cargo.toml_ create mode 100644 uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/src/lib.rs create mode 100644 uzumibi-cli/templates/cloudflare/__features__/enable-external/wrangler.jsonc diff --git a/uzumibi-cli/src/main.rs b/uzumibi-cli/src/main.rs index 4d36417..d9d7827 100644 --- a/uzumibi-cli/src/main.rs +++ b/uzumibi-cli/src/main.rs @@ -1,6 +1,7 @@ use clap::{Parser, Subcommand}; use dialoguer::Select; use include_dir::{Dir, include_dir}; +use std::collections::HashSet; use std::fs; use std::io::Write; use std::path::Path; @@ -35,6 +36,10 @@ enum Commands { /// Overwrite existing files without prompting #[arg(short, long, default_value_t = false)] force: bool, + + /// Comma-separated list of features to enable (e.g. "enable-external") + #[arg(long, value_delimiter = ',')] + features: Vec, }, } @@ -47,9 +52,10 @@ fn main() -> Result<(), Box> { project_name, dest_dir, force, + features, } => { let dest = dest_dir.as_deref().unwrap_or(&project_name); - create_project(&template, &project_name, dest, force)?; + create_project(&template, &project_name, dest, force, &features)?; } } @@ -68,6 +74,7 @@ fn create_project( project_name: &str, dest_dir: &str, force: bool, + features: &[String], ) -> Result<(), Box> { // Check if template exists let template_dir = TEMPLATES.get_dir(template).ok_or_else(|| { @@ -83,7 +90,10 @@ fn create_project( println!("Creating project '{}'...", project_name); - // Copy template files recursively + // Collect feature overlay paths to know which files to skip from base + let feature_files = collect_feature_overlay_files(template, features); + + // Copy base template files (skip files that will be overridden by feature overlays) copy_dir_recursive( template_dir, target_path, @@ -91,18 +101,65 @@ fn create_project( dest_dir, Path::new(""), force, + &feature_files, )?; + // Apply feature overlays + for feature in features { + let feature_path = format!("{}/__features__/{}", template, feature); + if let Some(feature_dir) = TEMPLATES.get_dir(&feature_path) { + copy_dir_recursive( + feature_dir, + target_path, + project_name, + dest_dir, + Path::new(""), + force, + &HashSet::new(), + )?; + } + } + println!( "\n✓ Successfully created project from template '{}'", template ); println!(" Run 'cd {}' to get started!", dest_dir); - print_project_next_steps(template, project_name); + print_project_next_steps(template, project_name, features); Ok(()) } +/// Collect all file paths from feature overlay directories that will replace base files. +fn collect_feature_overlay_files(template: &str, features: &[String]) -> HashSet { + let mut paths = HashSet::new(); + for feature in features { + let feature_path = format!("{}/__features__/{}", template, feature); + if let Some(feature_dir) = TEMPLATES.get_dir(&feature_path) { + collect_files_recursive(feature_dir, Path::new(""), &mut paths); + } + } + paths +} + +fn collect_files_recursive(dir: &Dir, relative_path: &Path, paths: &mut HashSet) { + for file in dir.files() { + let file_name = file.path().file_name().unwrap().to_str().unwrap(); + let actual_file_name = if file_name == "Cargo.toml_" { + "Cargo.toml" + } else { + file_name + }; + let path = relative_path.join(actual_file_name); + paths.insert(path.to_string_lossy().to_string()); + } + for sub_dir in dir.dirs() { + let dir_name = sub_dir.path().file_name().unwrap(); + let new_relative = relative_path.join(dir_name); + collect_files_recursive(sub_dir, &new_relative, paths); + } +} + fn copy_dir_recursive( source: &Dir, target: &Path, @@ -110,6 +167,7 @@ fn copy_dir_recursive( dest_dir: &str, relative_path: &Path, force: bool, + skip_files: &HashSet, ) -> Result<(), Box> { // Copy all files in current directory for file in source.files() { @@ -123,9 +181,15 @@ fn copy_dir_recursive( file_name_str }; - let target_file = target.join(actual_file_name); let display_path = relative_path.join(actual_file_name); + // Skip files that will be provided by a feature overlay + if skip_files.contains(&display_path.to_string_lossy().to_string()) { + continue; + } + + let target_file = target.join(actual_file_name); + let content = file.contents(); let content_str = std::str::from_utf8(content); @@ -172,6 +236,13 @@ fn copy_dir_recursive( // Recursively copy subdirectories for dir in source.dirs() { let dir_name = dir.path().file_name().unwrap(); + let dir_name_str = dir_name.to_str().unwrap(); + + // Skip __features__ directory (handled separately) + if dir_name_str == "__features__" { + continue; + } + let target_subdir = target.join(dir_name); let new_relative_path = relative_path.join(dir_name); @@ -183,6 +254,7 @@ fn copy_dir_recursive( dest_dir, &new_relative_path, force, + skip_files, )?; } @@ -272,7 +344,9 @@ fn substitute_project_name(content: &str, project_name: &str) -> String { .replace("$$PROJECT_NAME_UNDERSCORE$$", &project_name_underscore) } -fn print_project_next_steps(template: &str, project_name: &str) { +fn print_project_next_steps(template: &str, project_name: &str, features: &[String]) { + let has_enable_external = features.iter().any(|f| f == "enable-external"); + println!("\nNext steps:"); match template { "cloudflare" => { @@ -284,6 +358,11 @@ fn print_project_next_steps(template: &str, project_name: &str) { println!(" \x1b[36mrustup target add wasm32-unknown-unknown\x1b[0m"); println!(" • Node.js tools:"); println!(" \x1b[36mnpm install -g pnpm wrangler\x1b[0m"); + if has_enable_external { + println!(" • wasm-opt (Binaryen, required for asyncify):"); + println!(" \x1b[36mbrew install binaryen\x1b[0m"); + println!(" Or visit: https://github.com/WebAssembly/binaryen/releases"); + } println!(); println!(" 1. Install dependencies:"); println!(" \x1b[36mpnpm install\x1b[0m"); @@ -291,6 +370,20 @@ fn print_project_next_steps(template: &str, project_name: &str) { println!(" \x1b[36mpnpm run dev\x1b[0m"); println!(" 3. Deploy to Cloudflare:"); println!(" \x1b[36mpnpm run deploy\x1b[0m"); + if has_enable_external { + println!(); + println!(" \x1b[33mNote:\x1b[0m This project uses enable-external feature."); + println!(" The following Uzumibi APIs are available in Ruby:"); + println!( + " • \x1b[36mUzumibi::Fetch.fetch(url, method, body)\x1b[0m → Uzumibi::Response" + ); + println!( + " • \x1b[36mUzumibi::KV.get(key)\x1b[0m / \x1b[36mUzumibi::KV.set(key, value)\x1b[0m → Durable Object storage" + ); + println!( + " • \x1b[36mUzumibi::Queue.send(queue_name, message)\x1b[0m → Cloudflare Queue" + ); + } } "cloudrun" => { println!(" 0. Install required tools and setup account:"); diff --git a/uzumibi-cli/templates/cloudflare/__features__/enable-external/package.json b/uzumibi-cli/templates/cloudflare/__features__/enable-external/package.json new file mode 100644 index 0000000..ff48e72 --- /dev/null +++ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/package.json @@ -0,0 +1,18 @@ +{ + "name": "$$PROJECT_NAME$$", + "version": "0.0.0", + "private": true, + "scripts": { + "deploy": "npm run build:wasm:asyncify && wrangler deploy", + "dev": "npm run build:wasm:asyncify && wrangler dev", + "build:wasm:asyncify": "cargo build --package $$PROJECT_NAME$$ --target wasm32-unknown-unknown --release --features enable-external && wasm-opt --enable-bulk-memory-opt --enable-nontrapping-float-to-int --asyncify -O2 target/wasm32-unknown-unknown/release/$$PROJECT_NAME_UNDERSCORE$$.wasm -o ./src/$$PROJECT_NAME_UNDERSCORE$$.wasm", + "start": "wrangler dev", + "test": "vitest" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.8.19", + "asyncify-wasm": "^1.2.0", + "vitest": "~3.2.0", + "wrangler": "^4.54.0" + } +} \ No newline at end of file diff --git a/uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js b/uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js new file mode 100644 index 0000000..558cb9f --- /dev/null +++ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/src/index.js @@ -0,0 +1,313 @@ +import { DurableObject } from "cloudflare:workers"; +import { instantiate } from "asyncify-wasm"; +import mod from "./$$PROJECT_NAME_UNDERSCORE$$.wasm"; + +const wasmModule = mod; + +/** + * Durable Object for Uzumibi::KV storage + */ +export class UzumibiKVObject extends DurableObject { + async get(key) { + const value = await this.ctx.storage.get(key); + return value ?? null; + } + + async set(key, value) { + await this.ctx.storage.put(key, value); + } +} + +export default { + async fetch(request, env, ctx) { + const path = new URL(request.url).pathname; + if (path === "/favicon.ico") { + return new Response(null, { status: 404 }); + } + + const query = new URL(request.url).searchParams; + + // Durable Object stub (if binding exists) + const doStub = env.UZUMIBI_KV_DATA + ? env.UZUMIBI_KV_DATA.getByName("default") + : null; + + const decoder = new TextDecoder(); + const encoder = new TextEncoder(); + + const importObject = { + env: { + debug_console_log: (ptr, size) => { + const memory = exports.memory; + const buffer = new Uint8Array(memory.buffer, ptr, size); + console.log(`[debug]: ${decoder.decode(buffer)}`); + return 0; + }, + + // Fetch.fetch(url, method, body) -> packed Uzumibi::Response + // Format: u16 status | u16 headers_count | (u16 key_size, key, u16 value_size, value)... | u32 body_size | body + uzumibi_cf_fetch: async ( + urlPtr, urlSize, + methodPtr, methodSize, + bodyPtr, bodySize, + resultPtr, resultMaxSize + ) => { + const memory = exports.memory; + const url = decoder.decode(new Uint8Array(memory.buffer, urlPtr, urlSize)); + const method = decoder.decode(new Uint8Array(memory.buffer, methodPtr, methodSize)); + const body = bodySize > 0 + ? decoder.decode(new Uint8Array(memory.buffer, bodyPtr, bodySize)) + : null; + + const fetchOptions = { method }; + if (body && method !== "GET" && method !== "HEAD") { + fetchOptions.body = body; + } + + const response = await fetch(url, fetchOptions); + const responseBody = await response.text(); + + // Collect response headers + const respHeaders = []; + response.headers.forEach((value, key) => { + respHeaders.push({ key, value }); + }); + + // Pack into binary format matching Uzumibi::Response#to_shared_memory + const resultView = new DataView(memory.buffer, resultPtr, resultMaxSize); + const resultBuffer = new Uint8Array(memory.buffer, resultPtr, resultMaxSize); + let pos = 0; + + // Status code (u16 LE) + resultView.setUint16(pos, response.status, true); + pos += 2; + + // Headers count (u16 LE) + resultView.setUint16(pos, respHeaders.length, true); + pos += 2; + + // Each header + for (const header of respHeaders) { + const keyBytes = encoder.encode(header.key); + resultView.setUint16(pos, keyBytes.length, true); + pos += 2; + resultBuffer.set(keyBytes, pos); + pos += keyBytes.length; + + const valueBytes = encoder.encode(header.value); + resultView.setUint16(pos, valueBytes.length, true); + pos += 2; + resultBuffer.set(valueBytes, pos); + pos += valueBytes.length; + } + + // Body size (u32 LE) + const bodyBytes = encoder.encode(responseBody); + resultView.setUint32(pos, bodyBytes.length, true); + pos += 4; + + // Body + const bodyLen = Math.min(bodyBytes.length, resultMaxSize - pos); + resultBuffer.set(bodyBytes.slice(0, bodyLen), pos); + pos += bodyLen; + + return pos; + }, + + // KV.get(key) -> value string (via Durable Object) + uzumibi_cf_durable_object_get: async (keyPtr, keySize, resultPtr, resultMaxSize) => { + if (!doStub) return -1; + const memory = exports.memory; + const key = decoder.decode(new Uint8Array(memory.buffer, keyPtr, keySize)); + + const value = await doStub.get(key); + if (value === null) { + return -1; + } + const valueBytes = encoder.encode(value); + const length = Math.min(valueBytes.length, resultMaxSize); + const resultBuffer = new Uint8Array(memory.buffer, resultPtr, resultMaxSize); + resultBuffer.set(valueBytes.slice(0, length)); + return length; + }, + + // KV.set(key, value) (via Durable Object) + uzumibi_cf_durable_object_set: async (keyPtr, keySize, valuePtr, valueSize) => { + if (!doStub) return -1; + const memory = exports.memory; + const key = decoder.decode(new Uint8Array(memory.buffer, keyPtr, keySize)); + const value = decoder.decode(new Uint8Array(memory.buffer, valuePtr, valueSize)); + + await doStub.set(key, value); + return 0; + }, + + // Queue.send(queue_name, message) + uzumibi_cf_queue_send: async (queueNamePtr, queueNameSize, messagePtr, messageSize) => { + const memory = exports.memory; + const queueName = decoder.decode(new Uint8Array(memory.buffer, queueNamePtr, queueNameSize)); + const message = decoder.decode(new Uint8Array(memory.buffer, messagePtr, messageSize)); + + const queue = env[queueName]; + if (!queue) { + console.error(`Queue binding '${queueName}' not found`); + return -1; + } + await queue.send(message); + return 0; + }, + }, + }; + + const instance = await instantiate(wasmModule, importObject); + const exports = instance.exports; + + const reqResult = await exports.uzumibi_initialize_request(65536); + const reqOffset = Number(reqResult & 0xFFFFFFFFn); + if (reqOffset === 0) { + const errOffset = Number((reqResult >> 32n) & 0xFFFFFFFFn); + const buffer = new Uint8Array(exports.memory.buffer, errOffset); + let errStr = ""; + for (let i = 0; buffer[i] !== 0; i++) { + errStr += String.fromCharCode(buffer[i]); + } + throw new Error(`Failed to initialize request: ${errStr}`); + } + const requestBuffer = new Uint8Array(exports.memory.buffer, reqOffset, 65536); + + let pos = 0; + const dataView = new DataView(exports.memory.buffer, reqOffset); + + const method = encoder.encode(request.method); + requestBuffer.fill(0, pos, pos + 6); + requestBuffer.set(method.slice(0, 6), pos); + pos += 6; + + // Path size (u16 little-endian) + const pathBytes = encoder.encode(path); + dataView.setUint16(pos, pathBytes.length, true); + pos += 2; + + // Path + requestBuffer.set(pathBytes, pos); + pos += pathBytes.length; + + // Query string size (u16 little-endian) + const queryString = query.toString(); + const queryBytes = encoder.encode(queryString); + dataView.setUint16(pos, queryBytes.length, true); + pos += 2; + + // Query string + requestBuffer.set(queryBytes, pos); + pos += queryBytes.length; + + // Headers + const headers = []; + request.headers.forEach((value, key) => { + if (key.toLowerCase() !== 'cf-connecting-ip' && + key.toLowerCase() !== 'cf-ray' && + !key.toLowerCase().startsWith('x-')) { + headers.push({ key, value }); + } + }); + + // Headers count (u16 little-endian) + dataView.setUint16(pos, headers.length, true); + pos += 2; + + // Each header + for (const header of headers) { + // Header key size (u16 little-endian) + const keyBytes = encoder.encode(header.key); + dataView.setUint16(pos, keyBytes.length, true); + pos += 2; + + // Header key + requestBuffer.set(keyBytes, pos); + pos += keyBytes.length; + + // Header value size (u16 little-endian) + const valueBytes = encoder.encode(header.value); + dataView.setUint16(pos, valueBytes.length, true); + pos += 2; + + // Header value + requestBuffer.set(valueBytes, pos); + pos += valueBytes.length; + } + + // Request body size (u32 little-endian) + const bodyBytes = request.body ? new Uint8Array(await request.arrayBuffer()) : new Uint8Array(0); + dataView.setUint32(pos, bodyBytes.length, true); + pos += 4; + + // Request body + requestBuffer.set(bodyBytes, pos); + pos += bodyBytes.length; + + if (pos > 65536) { + throw new Error("Request data exceeds allocated buffer size"); + } + + const resResult = await exports.uzumibi_start_request(); + const resOffset = Number(resResult & 0xFFFFFFFFn); + if (resOffset === 0) { + const errOffset = Number((resResult >> 32n) & 0xFFFFFFFFn); + const buffer = new Uint8Array(exports.memory.buffer, errOffset); + let errStr = ""; + for (let i = 0; buffer[i] !== 0; i++) { + errStr += String.fromCharCode(buffer[i]); + } + throw new Error(`Failed to start request: ${errStr}`); + } + + // Unpack response + const resDataView = new DataView(exports.memory.buffer, resOffset); + + let resPos = 0; + + // Status code (u16 little-endian) + const statusCode = resDataView.getUint16(resPos, true); + resPos += 2; + + // Headers count (u16 little-endian) + const headersCount = resDataView.getUint16(resPos, true); + resPos += 2; + + // Parse headers + const responseHeaders = new Headers(); + for (let i = 0; i < headersCount; i++) { + // Header key size (u16 little-endian) + const keySize = resDataView.getUint16(resPos, true); + resPos += 2; + + // Header key + const keyBytes = new Uint8Array(exports.memory.buffer, resOffset + resPos, keySize); + const key = decoder.decode(keyBytes); + resPos += keySize; + + // Header value size (u16 little-endian) + const valueSize = resDataView.getUint16(resPos, true); + resPos += 2; + + // Header value + const valueBytes = new Uint8Array(exports.memory.buffer, resOffset + resPos, valueSize); + const value = decoder.decode(valueBytes); + resPos += valueSize; + + console.log(`[Response Header] ${key}: ${value}`); + responseHeaders.set(key, value); + } + + // Body size (u32 little-endian) + const bodySize = resDataView.getUint32(resPos, true); + resPos += 4; + + // Body + const bodyBuffer = new Uint8Array(exports.memory.buffer, resOffset + resPos, bodySize); + const responseText = decoder.decode(bodyBuffer); + + return new Response(responseText, { status: statusCode, headers: responseHeaders }); + } +}; diff --git a/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/.cargo/config.toml b/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/.cargo/config.toml new file mode 100644 index 0000000..cef1057 --- /dev/null +++ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/.cargo/config.toml @@ -0,0 +1,12 @@ +[target.wasm32-unknown-unknown] +rustflags = [ + "-C", + "target-feature=+bulk-memory,+mutable-globals", + "-C", + "link-arg=--import-memory", + "-C", + "link-arg=--export-memory", +] + +[build] +target = "wasm32-unknown-unknown" diff --git a/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/Cargo.toml_ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/Cargo.toml_ new file mode 100644 index 0000000..9bce1c4 --- /dev/null +++ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/Cargo.toml_ @@ -0,0 +1,21 @@ +[package] +name = "$$PROJECT_NAME$$" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +mrubyedge = { version = ">= 1.1", features = [ + "no-wasi", +], default-features = false } +uzumibi-gem = ">= 0.5.0" +uzumibi-art-router = ">= 0.3.1" + +[build-dependencies] +mruby-compiler2-sys = ">= 0.3.0" + +[features] +default = ["enable-external"] +enable-external = [] diff --git a/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/src/lib.rs b/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/src/lib.rs new file mode 100644 index 0000000..de76de9 --- /dev/null +++ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/wasm-app/src/lib.rs @@ -0,0 +1,469 @@ +#![allow(static_mut_refs)] +extern crate mrubyedge; +extern crate uzumibi_gem; + +use std::{mem::MaybeUninit, rc::Rc}; + +use mrubyedge::{ + rite::rite, + yamrb::{ + helpers::{mrb_define_class_cmethod, mrb_define_cmethod, mrb_funcall}, + prelude::hash::{mrb_hash_new, mrb_hash_set_index}, + value::{RObject, RValue}, + vm::VM, + }, +}; + +static MRB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/app.mrb")); + +static mut MRUBY_VM: MaybeUninit = MaybeUninit::uninit(); +static mut MRUBY_VM_LOADED: bool = false; + +static mut ERROR_BUF: [u8; 4096] = [0; 4096]; + +fn set_error_to_buf(message: impl AsRef) -> *const u8 { + unsafe { + let bytes = message.as_ref().as_bytes(); + let len = bytes.len().min(ERROR_BUF.len() - 1); + ERROR_BUF[..len].copy_from_slice(&bytes[..len]); + ERROR_BUF[len] = 0; + ERROR_BUF.as_ptr() + } +} + +unsafe extern "C" { + unsafe fn debug_console_log(ptr: *const u8, len: usize); +} + +#[cfg(feature = "enable-external")] +unsafe extern "C" { + unsafe fn uzumibi_cf_fetch( + url_ptr: *const u8, + url_size: usize, + method_ptr: *const u8, + method_size: usize, + body_ptr: *const u8, + body_size: usize, + result_ptr: *mut u8, + result_max_size: usize, + ) -> i32; + unsafe fn uzumibi_cf_durable_object_get( + key_ptr: *const u8, + key_size: usize, + result_ptr: *mut u8, + result_max_size: usize, + ) -> i32; + unsafe fn uzumibi_cf_durable_object_set( + key_ptr: *const u8, + key_size: usize, + value_ptr: *const u8, + value_size: usize, + ) -> i32; + unsafe fn uzumibi_cf_queue_send( + queue_name_ptr: *const u8, + queue_name_size: usize, + message_ptr: *const u8, + message_size: usize, + ) -> i32; +} + +fn debug_console_log_internal(message: &str) { + unsafe { + debug_console_log(message.as_ptr(), message.len()); + } +} + +// ---- External API wrappers (only when enable-external feature is active) ---- + +/// Packed response format (same as Uzumibi::Response#to_shared_memory): +/// u16 LE status_code +/// u16 LE headers_count +/// (u16 LE key_size, key bytes, u16 LE value_size, value bytes) * headers_count +/// u32 LE body_size +/// body bytes +#[cfg(feature = "enable-external")] +fn cf_fetch(url: &str, method: &str, body: &str) -> Result, String> { + const BUFFER_SIZE: usize = 65536; + let mut buffer = vec![0u8; BUFFER_SIZE]; + + unsafe { + let result = uzumibi_cf_fetch( + url.as_ptr(), + url.len(), + method.as_ptr(), + method.len(), + body.as_ptr(), + body.len(), + buffer.as_mut_ptr(), + BUFFER_SIZE, + ); + match result { + len if len >= 0 => { + let len = len as usize; + Ok(buffer[..len].to_vec()) + } + _ => Err(format!("Fetch failed with return code: {}", result)), + } + } +} + +#[cfg(feature = "enable-external")] +fn cf_durable_object_get(key: &str) -> Result, String> { + const BUFFER_SIZE: usize = 65536; + let mut buffer = vec![0u8; BUFFER_SIZE]; + + unsafe { + let result = uzumibi_cf_durable_object_get( + key.as_ptr(), + key.len(), + buffer.as_mut_ptr(), + BUFFER_SIZE, + ); + match result { + -1 => Ok(None), + len if len >= 0 => { + let len = len as usize; + let value = String::from_utf8(buffer[..len].to_vec()) + .map_err(|e| format!("Failed to decode UTF-8: {}", e))?; + Ok(Some(value)) + } + _ => Err(format!( + "Unexpected return value from durable_object_get: {}", + result + )), + } + } +} + +#[cfg(feature = "enable-external")] +fn cf_durable_object_set(key: &str, value: &str) -> Result<(), String> { + unsafe { + let result = + uzumibi_cf_durable_object_set(key.as_ptr(), key.len(), value.as_ptr(), value.len()); + match result { + 0 => Ok(()), + _ => Err(format!("Failed to set value: return code {}", result)), + } + } +} + +#[cfg(feature = "enable-external")] +fn cf_queue_send(queue_name: &str, message: &str) -> Result<(), String> { + unsafe { + let result = uzumibi_cf_queue_send( + queue_name.as_ptr(), + queue_name.len(), + message.as_ptr(), + message.len(), + ); + match result { + 0 => Ok(()), + _ => Err(format!( + "Failed to send queue message: return code {}", + result + )), + } + } +} + +// ---- mruby gem method implementations ---- + +fn uzumibi_kernel_debug_console_log( + vm: &mut VM, + args: &[Rc], +) -> Result, mrubyedge::Error> { + let msg_obj = &args[0]; + let msg = mrb_funcall(vm, msg_obj.clone().into(), "to_s", &[])?; + let msg: String = msg.as_ref().try_into()?; + unsafe { + debug_console_log(msg.as_ptr(), msg.len()); + } + Ok(RObject::nil().to_refcount_assigned()) +} + +/// Fetch.fetch(url, method="GET", body="") -> Uzumibi::Response +#[cfg(feature = "enable-external")] +fn uzumibi_fetch_class_fetch( + vm: &mut VM, + args: &[Rc], +) -> Result, mrubyedge::Error> { + let url_obj = &args[0]; + let url = mrb_funcall(vm, url_obj.clone().into(), "to_s", &[])?; + let url: String = url.as_ref().try_into()?; + + let method = if args.len() > 1 { + let m = mrb_funcall(vm, args[1].clone().into(), "to_s", &[])?; + let m: String = m.as_ref().try_into()?; + m + } else { + "GET".to_string() + }; + + let body = if args.len() > 2 { + let b = mrb_funcall(vm, args[2].clone().into(), "to_s", &[])?; + let b: String = b.as_ref().try_into()?; + b + } else { + String::new() + }; + + let packed = cf_fetch(&url, &method, &body) + .map_err(|e| mrubyedge::Error::RuntimeError(format!("Fetch failed: {}", e)))?; + + // Unpack the packed response into Uzumibi::Response + unpack_response_to_robject(vm, &packed) +} + +/// Unpack packed binary response into Uzumibi::Response mruby object +#[cfg(feature = "enable-external")] +fn unpack_response_to_robject(vm: &mut VM, buf: &[u8]) -> Result, mrubyedge::Error> { + let mut offset = 0; + + // Status code (u16 LE) + let status_code = u16::from_le_bytes([buf[offset], buf[offset + 1]]); + offset += 2; + + // Headers count (u16 LE) + let headers_count = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize; + offset += 2; + + // Parse headers + let headers_hash = mrb_hash_new(vm, &[])?; + for _ in 0..headers_count { + let key_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize; + offset += 2; + let key = String::from_utf8_lossy(&buf[offset..offset + key_size]).to_string(); + offset += key_size; + + let value_size = u16::from_le_bytes([buf[offset], buf[offset + 1]]) as usize; + offset += 2; + let value = String::from_utf8_lossy(&buf[offset..offset + value_size]).to_string(); + offset += value_size; + + mrb_hash_set_index( + headers_hash.clone(), + RObject::string(key).to_refcount_assigned(), + RObject::string(value).to_refcount_assigned(), + )?; + } + + // Body size (u32 LE) + let body_size = u32::from_le_bytes([ + buf[offset], + buf[offset + 1], + buf[offset + 2], + buf[offset + 3], + ]) as usize; + offset += 4; + + // Body + let body = String::from_utf8_lossy(&buf[offset..offset + body_size]).to_string(); + + // Create Uzumibi::Response instance + let uzumibi = vm + .get_const_by_name("Uzumibi") + .ok_or_else(|| mrubyedge::Error::RuntimeError("Uzumibi module not found".to_string()))?; + let uzumibi_module = match &uzumibi.as_ref().value { + RValue::Module(m) => m.clone(), + _ => { + return Err(mrubyedge::Error::RuntimeError( + "Uzumibi must be a module".to_string(), + )); + } + }; + let response_class = uzumibi_module + .get_const_by_name("Response") + .ok_or_else(|| { + mrubyedge::Error::RuntimeError("Uzumibi::Response class not found".to_string()) + })?; + let response = mrb_funcall(vm, Some(response_class), "new", &[])?; + + response.set_ivar( + "@status_code", + RObject::integer(status_code as i64).to_refcount_assigned(), + ); + response.set_ivar("@headers", headers_hash); + response.set_ivar("@body", RObject::string(body).to_refcount_assigned()); + + Ok(response) +} + +/// KV.get(key) +#[cfg(feature = "enable-external")] +fn uzumibi_kv_class_get( + vm: &mut VM, + args: &[Rc], +) -> Result, mrubyedge::Error> { + let key_obj = &args[0]; + let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?; + let key: String = key.as_ref().try_into()?; + + match cf_durable_object_get(&key) { + Ok(Some(value)) => Ok(RObject::string(value).to_refcount_assigned()), + Ok(None) => Ok(RObject::nil().to_refcount_assigned()), + Err(e) => Err(mrubyedge::Error::RuntimeError(format!( + "Failed to access storage value: {}", + e + ))), + } +} + +/// KV.set(key, value) +#[cfg(feature = "enable-external")] +fn uzumibi_kv_class_set( + vm: &mut VM, + args: &[Rc], +) -> Result, mrubyedge::Error> { + let key_obj = &args[0]; + let key = mrb_funcall(vm, key_obj.clone().into(), "to_s", &[])?; + let key: String = key.as_ref().try_into()?; + + let value_obj = &args[1]; + let value = mrb_funcall(vm, value_obj.clone().into(), "to_s", &[])?; + let value: String = value.as_ref().try_into()?; + + cf_durable_object_set(&key, &value).map_err(|e| { + mrubyedge::Error::RuntimeError(format!("Failed to set storage value: {}", e)) + })?; + + Ok(RObject::boolean(true).to_refcount_assigned()) +} + +/// Queue.send(queue_name, message) +#[cfg(feature = "enable-external")] +fn uzumibi_queue_class_send( + vm: &mut VM, + args: &[Rc], +) -> Result, mrubyedge::Error> { + let queue_name_obj = &args[0]; + let queue_name = mrb_funcall(vm, queue_name_obj.clone().into(), "to_s", &[])?; + let queue_name: String = queue_name.as_ref().try_into()?; + + let message_obj = &args[1]; + let message = mrb_funcall(vm, message_obj.clone().into(), "to_s", &[])?; + let message: String = message.as_ref().try_into()?; + + cf_queue_send(&queue_name, &message).map_err(|e| { + mrubyedge::Error::RuntimeError(format!("Failed to send queue message: {}", e)) + })?; + + Ok(RObject::boolean(true).to_refcount_assigned()) +} + +// ---- VM initialization ---- + +fn init_vm() -> Result { + let mut rite = rite::load(MRB) + .map_err(|e| mrubyedge::Error::RuntimeError(format!("Failed to load mruby: {:?}", e)))?; + let mut vm = VM::open(&mut rite); + uzumibi_gem::init::init_uzumibi(&mut vm); + let object = vm.object_class.clone(); + mrb_define_cmethod( + &mut vm, + object, + "debug_console", + Box::new(uzumibi_kernel_debug_console_log), + ); + + #[cfg(feature = "enable-external")] + { + let uzumibi_module = vm.get_module_by_name("Uzumibi"); + + // Uzumibi::Fetch.fetch(url, method="GET", body="") + let fetch_class = vm.define_class("Fetch", None, Some(uzumibi_module.clone())); + mrb_define_class_cmethod( + &mut vm, + fetch_class, + "fetch", + Box::new(uzumibi_fetch_class_fetch), + ); + + // Uzumibi::KV.get(key) / Uzumibi::KV.set(key, value) + let kv_class = vm.define_class("KV", None, Some(uzumibi_module.clone())); + mrb_define_class_cmethod( + &mut vm, + kv_class.clone(), + "get", + Box::new(uzumibi_kv_class_get), + ); + mrb_define_class_cmethod(&mut vm, kv_class, "set", Box::new(uzumibi_kv_class_set)); + + // Uzumibi::Queue.send(queue_name, message) + let queue_class = vm.define_class("Queue", None, Some(uzumibi_module)); + mrb_define_class_cmethod( + &mut vm, + queue_class, + "send", + Box::new(uzumibi_queue_class_send), + ); + } + + vm.run() + .map_err(|e| mrubyedge::Error::RuntimeError(format!("Failed to init VM: {:?}", e)))?; + + Ok(vm) +} + +fn assume_init_vm() -> Result<&'static mut VM, mrubyedge::Error> { + unsafe { + if !MRUBY_VM_LOADED { + MRUBY_VM = MaybeUninit::new(init_vm()?); + MRUBY_VM_LOADED = true; + } + Ok(MRUBY_VM.assume_init_mut()) + } +} + +fn do_uzumibi_initialize_request(size: i32) -> Result<*mut u8, mrubyedge::Error> { + let vm = assume_init_vm()?; + let size = RObject::integer(size as i64).to_refcount_assigned(); + let app = vm + .globals + .get("$APP") + .ok_or_else(|| mrubyedge::Error::RuntimeError("$APP is not defined".to_string()))?; + let ret = mrb_funcall(vm, app.clone().into(), "initialize_request", &[size])?; + ret.as_ref().try_into() +} + +fn do_uzumibi_start_request() -> Result<*mut u8, mrubyedge::Error> { + debug_console_log_internal("uzumibi_start_request called"); + let vm = assume_init_vm()?; + let app = vm + .globals + .get("$APP") + .ok_or_else(|| mrubyedge::Error::RuntimeError("$APP is not defined".to_string()))?; + let ret = mrb_funcall( + vm, + app.clone().into(), + "start_request_and_return_shared_memory", + &[], + )?; + match &ret.as_ref().value { + RValue::SharedMemory(sm) => Ok(sm.borrow_mut().leak()), + _ => Err(mrubyedge::Error::RuntimeError( + "Returned value is not SharedMemory".to_string(), + )), + } +} + +#[unsafe(export_name = "uzumibi_initialize_request")] +unsafe extern "C" fn uzumibi_initialize_request(size: i32) -> u64 { + match do_uzumibi_initialize_request(size) { + Ok(ptr) => (ptr as u32) as u64, + Err(e) => { + let err_buf = set_error_to_buf(format!("Error in initialize_request: {}", e)); + ((err_buf as u32) as u64) << 32 + } + } +} + +#[unsafe(export_name = "uzumibi_start_request")] +unsafe extern "C" fn uzumibi_start_request() -> u64 { + match do_uzumibi_start_request() { + Ok(ptr) => (ptr as u32) as u64, + Err(e) => { + let err_buf = set_error_to_buf(format!("Error in start_request: {}", e)); + ((err_buf as u32) as u64) << 32 + } + } +} diff --git a/uzumibi-cli/templates/cloudflare/__features__/enable-external/wrangler.jsonc b/uzumibi-cli/templates/cloudflare/__features__/enable-external/wrangler.jsonc new file mode 100644 index 0000000..bc7cadc --- /dev/null +++ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/wrangler.jsonc @@ -0,0 +1,47 @@ +/** + * For more details on how to configure Wrangler, refer to: + * https://developers.cloudflare.com/workers/wrangler/configuration/ + */ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "$$PROJECT_NAME$$", + "main": "src/index.js", + "compatibility_date": "2025-12-30", + "observability": { + "enabled": true + }, + /** + * Durable Objects + * Used for Uzumibi::KV.get/set + * https://developers.cloudflare.com/durable-objects/ + */ + "durable_objects": { + "bindings": [ + { + "name": "UZUMIBI_KV_DATA", + "class_name": "UzumibiKVObject" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": [ + "UzumibiKVObject" + ] + } + ] + /** + * Queues + * Used for Uzumibi::Queue.send + * https://developers.cloudflare.com/queues/ + */ + // "queues": { + // "producers": [ + // { + // "binding": "MY_QUEUE", + // "queue": "my-queue-name" + // } + // ] + // } +} \ No newline at end of file From 7437376c2e52f6ad6ae852aadbb5614084bbcad5 Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Thu, 5 Mar 2026 00:24:07 +0900 Subject: [PATCH 07/11] Bump to RC --- Cargo.lock | 2 +- uzumibi-cli/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8b5ab13..0e92b85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1915,7 +1915,7 @@ dependencies = [ [[package]] name = "uzumibi-cli" -version = "0.5.1" +version = "0.6.0-rc1" dependencies = [ "clap", "dialoguer", diff --git a/uzumibi-cli/Cargo.toml b/uzumibi-cli/Cargo.toml index e391118..736eb7d 100644 --- a/uzumibi-cli/Cargo.toml +++ b/uzumibi-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uzumibi-cli" -version = "0.5.1" +version = "0.6.0-rc1" edition = "2024" authors = ["Uchio Kondo "] description = "Uzumibi CLI tool to generate serverless mruby/edge apps" From 9d826c2e2775ba73dda3fbd38e20c1949b7d48f1 Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Thu, 5 Mar 2026 00:27:58 +0900 Subject: [PATCH 08/11] Fix opt opt --- .../cloudflare/__features__/enable-external/package.json | 2 +- uzumibi-on-cloudflare-spike/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uzumibi-cli/templates/cloudflare/__features__/enable-external/package.json b/uzumibi-cli/templates/cloudflare/__features__/enable-external/package.json index ff48e72..c04421a 100644 --- a/uzumibi-cli/templates/cloudflare/__features__/enable-external/package.json +++ b/uzumibi-cli/templates/cloudflare/__features__/enable-external/package.json @@ -5,7 +5,7 @@ "scripts": { "deploy": "npm run build:wasm:asyncify && wrangler deploy", "dev": "npm run build:wasm:asyncify && wrangler dev", - "build:wasm:asyncify": "cargo build --package $$PROJECT_NAME$$ --target wasm32-unknown-unknown --release --features enable-external && wasm-opt --enable-bulk-memory-opt --enable-nontrapping-float-to-int --asyncify -O2 target/wasm32-unknown-unknown/release/$$PROJECT_NAME_UNDERSCORE$$.wasm -o ./src/$$PROJECT_NAME_UNDERSCORE$$.wasm", + "build:wasm:asyncify": "cargo build --package $$PROJECT_NAME$$ --target wasm32-unknown-unknown --release --features enable-external && wasm-opt --enable-bulk-memory --enable-nontrapping-float-to-int --asyncify -O2 target/wasm32-unknown-unknown/release/$$PROJECT_NAME_UNDERSCORE$$.wasm -o ./src/$$PROJECT_NAME_UNDERSCORE$$.wasm", "start": "wrangler dev", "test": "vitest" }, diff --git a/uzumibi-on-cloudflare-spike/package.json b/uzumibi-on-cloudflare-spike/package.json index ce766cd..09189fe 100644 --- a/uzumibi-on-cloudflare-spike/package.json +++ b/uzumibi-on-cloudflare-spike/package.json @@ -7,7 +7,7 @@ "dev": "npm run build:wasm:asyncify && wrangler dev", "dev:vanilla": "npm run build:wasm:vanilla && wrangler dev --main src/index.vanilla.js", "build:wasm:vanilla": "cargo build --package uzumibi-on-cloudflare-spike --target wasm32-unknown-unknown --release --no-default-features && cp -v -f ../target/wasm32-unknown-unknown/release/uzumibi_on_cloudflare_spike.wasm src/", - "build:wasm:asyncify": "cargo build --package uzumibi-on-cloudflare-spike --target wasm32-unknown-unknown --release --features enable-external && wasm-opt --enable-bulk-memory-opt --enable-nontrapping-float-to-int --asyncify -O2 ../target/wasm32-unknown-unknown/release/uzumibi_on_cloudflare_spike.wasm -o ./src/uzumibi_on_cloudflare_spike.wasm", + "build:wasm:asyncify": "cargo build --package uzumibi-on-cloudflare-spike --target wasm32-unknown-unknown --release --features enable-external && wasm-opt --enable-bulk-memory --enable-nontrapping-float-to-int --asyncify -O2 ../target/wasm32-unknown-unknown/release/uzumibi_on_cloudflare_spike.wasm -o ./src/uzumibi_on_cloudflare_spike.wasm", "start": "wrangler dev", "test": "vitest" }, From 86ff9b88f2690738eb94d62ef9068325877ac683 Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Thu, 5 Mar 2026 00:58:47 +0900 Subject: [PATCH 09/11] Fix some testcase, add external runn tests --- .github/workflows/runn-cli-tests.yml | 92 ++++++++++++++++------- uzumibi-cli/tests/runn/help.yml | 2 +- uzumibi-cli/tests/runn/new_cloudflare.yml | 3 +- 3 files changed, 66 insertions(+), 31 deletions(-) diff --git a/.github/workflows/runn-cli-tests.yml b/.github/workflows/runn-cli-tests.yml index ecb82db..14511f6 100644 --- a/.github/workflows/runn-cli-tests.yml +++ b/.github/workflows/runn-cli-tests.yml @@ -1,23 +1,14 @@ -name: CLI Integration Tests (runn) +name: Runn CLI Tests on: push: - branches: - - master - paths: - - '**' + branches: [main] pull_request: - paths: - - 'uzumibi-cli/**' - - '.github/workflows/runn-cli-tests.yml' - workflow_dispatch: - -env: - CARGO_TERM_COLOR: always + branches: [main] jobs: basic-tests: - name: Basic CLI tests (help, error_cases) + name: Basic CLI tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -32,9 +23,9 @@ jobs: ~/.cargo/registry ~/.cargo/git target - key: ${{ runner.os }}-cargo-cli-${{ hashFiles('**/Cargo.lock') }} + key: ${{ runner.os }}-cargo-basic-${{ hashFiles('**/Cargo.lock') }} restore-keys: | - ${{ runner.os }}-cargo-cli- + ${{ runner.os }}-cargo-basic- - name: Install runn uses: k1LoW/gh-setup@v1 @@ -47,15 +38,13 @@ jobs: env: RUNN_SCOPES: "read:parent,run:exec" UZUMIBI_TEST_BINARY: ${{ github.workspace }}/target/release/uzumibi - UZUMIBI_TEST_TMPDIR: ${{ runner.temp }} run: runn run help.yml --verbose - - name: Run error_cases tests + - name: Run error case tests working-directory: uzumibi-cli/tests/runn env: RUNN_SCOPES: "read:parent,run:exec" UZUMIBI_TEST_BINARY: ${{ github.workspace }}/target/release/uzumibi - UZUMIBI_TEST_TMPDIR: ${{ runner.temp }} run: runn run error_cases.yml --verbose cloudflare-test: @@ -66,8 +55,9 @@ jobs: - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown + + - name: Install wasm32 target + run: rustup target add wasm32-unknown-unknown - name: Cache cargo registry and build uses: actions/cache@v4 @@ -80,28 +70,71 @@ jobs: restore-keys: | ${{ runner.os }}-cargo-cloudflare- - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Install runn + uses: k1LoW/gh-setup@v1 + with: + repo: k1LoW/runn + bin-match: runn + + - name: Run cloudflare tests + working-directory: uzumibi-cli/tests/runn + env: + RUNN_SCOPES: "read:parent,run:exec" + UZUMIBI_TEST_BINARY: ${{ github.workspace }}/target/release/uzumibi + UZUMIBI_TEST_TMPDIR: ${{ runner.temp }} + run: runn run new_cloudflare.yml --verbose + + cloudflare-enable-external-test: + name: Cloudflare template test (enable-external) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install wasm32 target + run: rustup target add wasm32-unknown-unknown + + - name: Cache cargo registry and build + uses: actions/cache@v4 with: - node-version: '22' + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-cloudflare-ext-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-cloudflare-ext- - name: Install pnpm uses: pnpm/action-setup@v4 with: version: 10 + - name: Install wasm-opt + run: | + curl -L https://github.com/WebAssembly/binaryen/releases/download/version_120/binaryen-version_120-x86_64-linux.tar.gz | tar xz + sudo cp binaryen-version_120/bin/wasm-opt /usr/local/bin/ + - name: Install runn uses: k1LoW/gh-setup@v1 with: repo: k1LoW/runn bin-match: runn - - name: Run cloudflare tests + - name: Run cloudflare tests with enable-external working-directory: uzumibi-cli/tests/runn env: RUNN_SCOPES: "read:parent,run:exec" UZUMIBI_TEST_BINARY: ${{ github.workspace }}/target/release/uzumibi UZUMIBI_TEST_TMPDIR: ${{ runner.temp }} + UZUMIBI_NEW_EXTRA_ARGS: "--features enable-external" run: runn run new_cloudflare.yml --verbose cloudrun-test: @@ -123,7 +156,7 @@ jobs: key: ${{ runner.os }}-cargo-cloudrun-${{ hashFiles('**/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo-cloudrun- - + - name: Install runn uses: k1LoW/gh-setup@v1 with: @@ -139,15 +172,16 @@ jobs: run: runn run new_cloudrun.yml --verbose webworker-test: - name: WebWorker template test + name: Web Worker template test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Rust toolchain uses: dtolnay/rust-toolchain@stable - with: - targets: wasm32-unknown-unknown + + - name: Install wasm32 target + run: rustup target add wasm32-unknown-unknown - name: Cache cargo registry and build uses: actions/cache@v4 @@ -172,4 +206,4 @@ jobs: RUNN_SCOPES: "read:parent,run:exec" UZUMIBI_TEST_BINARY: ${{ github.workspace }}/target/release/uzumibi UZUMIBI_TEST_TMPDIR: ${{ runner.temp }} - run: runn run new_webworker.yml --verbose + run: runn run new_webworker.yml --verbose \ No newline at end of file diff --git a/uzumibi-cli/tests/runn/help.yml b/uzumibi-cli/tests/runn/help.yml index 2b5965e..4d8c9e8 100644 --- a/uzumibi-cli/tests/runn/help.yml +++ b/uzumibi-cli/tests/runn/help.yml @@ -1,7 +1,7 @@ desc: Test uzumibi CLI help command vars: binary: ${UZUMIBI_TEST_BINARY:-../target/release/uzumibi} - version: 0.5.1 + version: 0.6.0-rc1 steps: build: desc: Build release binary diff --git a/uzumibi-cli/tests/runn/new_cloudflare.yml b/uzumibi-cli/tests/runn/new_cloudflare.yml index c22765a..794e637 100644 --- a/uzumibi-cli/tests/runn/new_cloudflare.yml +++ b/uzumibi-cli/tests/runn/new_cloudflare.yml @@ -4,6 +4,7 @@ runners: vars: binary: ${UZUMIBI_TEST_BINARY:-../target/release/uzumibi} tmpdir: ${UZUMIBI_TEST_TMPDIR:-../tmp} + extra_args: ${UZUMIBI_NEW_EXTRA_ARGS:-} project_name: test-cloudflare-project template: cloudflare steps: @@ -28,7 +29,7 @@ steps: create_project: desc: Create new cloudflare project exec: - command: cd {{ vars.tmpdir }} && {{ vars.binary }} new -t {{ vars.template }} {{ vars.project_name }} + command: cd {{ vars.tmpdir }} && {{ vars.binary }} new -t {{ vars.template }} {{ vars.extra_args }} {{ vars.project_name }} test: | current.exit_code == 0 && current.stdout contains 'Successfully created project' From 40b3c03b6bd7e1f623d4ae31353ed32e7fda6349 Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Thu, 5 Mar 2026 01:00:46 +0900 Subject: [PATCH 10/11] Fix metadata in CI --- .github/workflows/runn-cli-tests.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/runn-cli-tests.yml b/.github/workflows/runn-cli-tests.yml index 14511f6..4968555 100644 --- a/.github/workflows/runn-cli-tests.yml +++ b/.github/workflows/runn-cli-tests.yml @@ -1,10 +1,19 @@ -name: Runn CLI Tests +name: CLI Integration Tests (runn) on: push: - branches: [main] + branches: + - master + paths: + - '**' pull_request: - branches: [main] + paths: + - 'uzumibi-cli/**' + - '.github/workflows/runn-cli-tests.yml' + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always jobs: basic-tests: From 07623d3807ab52a37b3abc4aa5ccbbd8d6c3180f Mon Sep 17 00:00:00 2001 From: Uchio Kondo Date: Thu, 5 Mar 2026 01:02:39 +0900 Subject: [PATCH 11/11] Lint --- .github/workflows/runn-cli-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/runn-cli-tests.yml b/.github/workflows/runn-cli-tests.yml index 4968555..1b9226f 100644 --- a/.github/workflows/runn-cli-tests.yml +++ b/.github/workflows/runn-cli-tests.yml @@ -215,4 +215,4 @@ jobs: RUNN_SCOPES: "read:parent,run:exec" UZUMIBI_TEST_BINARY: ${{ github.workspace }}/target/release/uzumibi UZUMIBI_TEST_TMPDIR: ${{ runner.temp }} - run: runn run new_webworker.yml --verbose \ No newline at end of file + run: runn run new_webworker.yml --verbose