LLRTでHonoを動かす
LLRT (Low Latency Runtime)はAWS Labsの人たちによって公開されたOSSで、
「v8やJSCよりミニマムなJavaScriptエンジン付けてLambdaにデプロイしたらめっちゃ速くなるんじゃない?」というようなコンセプトを持つ新しいJavaScriptコードのランタイムです
QuickJS[1]というES2023準拠のJavaScriptエンジンとそのRustバインディングのrquickjsを使って全体的に構築されています(LLRTはES2020と明記されていますが)
ECMAScript(ES)にはないNode.jsの標準API群が一部Rustを使って書かれています
JavaScriptから呼び出せるモジュールを「LLRTからロードできるネイティブなESMモジュールを追加する」で示したとうり自作できるので、Web Standard APIs互換なウェブフレームワーク Honoぐらいならもう動くのではと思ったので試しました
インストール
お手軽なインストール方法はGitHub Releasesからお使いのシステム用のバイナリをダウンロードしてくることです
cargo buildで自分でビルドしてもかまいません
app.fire()不発
とりあえずCloudflare Workersみたいにして動いてくれという想いでapp.fire()
してみます
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Hono on LLRT!'))
app.fire()
import { Hono } from 'hono'
はLLRTではネイティブに動かないので単一ファイルにバンドル化する必要があります
Using node_modules (dependencies) with LLRTにあるというりesbuildを使ってみます
esbuild hono-llrt.js --outfile=bundle.mjs --platform=browser --target=es2020 --format=esm --bundle
llrt bundle.mjs
ReferenceError: 'addEventListener' is not defined
at <anonymous> (bundle.mjs:786:7)
at <anonymous> (bundle.mjs:1558:5)
addEventListener
はブラウザが持つグローバル関数なのでLLRTにはありません
LLRTはNode.js互換APIを用意しているということで、https://hono.dev/getting-started/nodejs を参考にNode.js Adapterを使ってみます
@hono/node-serverが動作しない理由
以下でビルドしました
esbuild hono-llrt.js --outfile=bundle.mjs --platform=node --target=es2020 --format=esm --bundle
--platform=node
になっています
llrt bundle.mjs
ReferenceError: Error resolving module 'http' from 'bundle.mjs'
Compatibility matrixを確認するとhttpモジュールはまだ完成していません
ただよく見るとnet:serverモジュールがあります。なのでAdapterになるHTTPサーバーのレイヤーを書けばいいのかと結論付けました
new Response()
がない
HTTPサーバーを順調にこんな感じに書けました
import { createServer } from 'net'
import { Hono } from 'hono'
const PORT = 3000
const app = new Hono()
app.get('/', (c) => c.text('Hello Hono on LLRT!'))
const server = createServer((socket) => {
socket.on('error', (error) => {
console.error('Socket error:', error)
socket.end()
})
socket.on('data', async (data) => {
try {
const requestString = data.toString()
const requestLines = requestString.split('\r\n')
const requestLine = requestLines[0].split(' ')
const headers = {}
for (let i = 1; i < requestLines.length; i++) {
const line = requestLines[i]
if (line) {
const [key, ...valueParts] = line.split(': ')
headers[key] = valueParts.join(': ')
}
}
const method = requestLine[0]
const path = requestLine[1]
const protocol = requestLine[2]
const url = `http://localhost:${PORT}${path}`
const request = new Request(url, {
method,
path,
protocol,
headers,
})
console.log('Received request:', request)
const response = await app.fetch(request)
console.log('Converted response:', response)
const body = await response.text()
let responseHeaders = ''
for (const [key, value] of Object.entries(response.headers)) {
responseHeaders += `${key}: ${value}\r\n`
}
const httpResponse = `HTTP/1.1 ${response.status} ${response.statusText}\r\n${responseHeaders}\r\n${body}`
socket.write(httpResponse)
socket.end()
} catch (error) {
console.error('Error handling request:', error)
socket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n')
socket.end()
}
})
})
server.listen(PORT, () => {
console.log(`Server listening on http://localhost:${PORT}/`)
})
ただ起動してブラウザからアクセスするとReferenceError: 'Response' is not defined
とエラーが発生してしまいます
llrt bundle.mjs
Server listening on http://localhost:3000/
Received request: {
keepalive: true,
url: 'http://localhost:3000/',
headers: { Symbol.iterator: undefined },
method: 'GET',
body: undefined
}
ReferenceError: 'Response' is not defined
at <anonymous> (bundle.mjs:180:22) at <anonymous> (bundle.mjs:1546:23) at dispatch (bundle.mjs:926:15) at <anonymous> (bundle.mjs:771:19) at <anonymous> (bundle.mjs:1576:34)
Error handling request: ReferenceError: 'Response' is not defined
ソースコードを確認する限りグローバル空間にResponseは定義されています
純粋な呼び出しコードをevalしてみると再現するのでLLRTのレイヤーの問題かと思います
llrt -e 'new Request("http://localhost:3000/") && console.log("OK")'
OK
llrt -e 'new Response() && console.log("OK")'
ReferenceError: 'Response' is not defined
at <eval> (eval_script:1:5)
これはもうちょっと調べてIssueで報告したいところですが[2]、LLRT本体ソースコードをいくら改変してみても解決しなかったのでとりあえず方針を変更して実行時に代替オブジェクトを差し込むことにしました
/**
* TODO: globalThis.Responseを消す
* Responseオブジェクトは本来ランタイムでサポートされている
* LLRT 0.1.6-beta時点で`new Response`がグローバルスコープで参照できない問題へのワークアラウンド
*/
globalThis.Response = class {
constructor(body, status = 200, statusText = 'OK', headers = {}) {
this.body = body
this.status = status
this.statusText = statusText
this.headers = headers
}
async text() {
return this.body
}
}
これで完成です。ブラウザにレスポンスが表示されました
しかし問題も……
1回目のリクエストがすぐ応答が来るのですが、2回目を連続でアクセスすると数秒から数十秒かかります
ソケットの処理が適切にflushされてないような挙動……
LLRTの用途はLambdaで起動してすぐ終了とのことなのでここも一旦気にしないことにしました
Tips: JavaScript部分のデバッグ
LLRTバイナリはJavaScriptのデバッグ実行に対応していなくエラーメッセージもフレンドリーなものを吐いてくれないので実行中に死んでたりします
なのでHonoのソースコード内にconsole.log()を入れてプリントデバッグをしてしのぎました(つらい)
git clone https://github.com/honojs/hono.git
cd hono
yarn
- import { Hono } from 'hono'
+ import { Hono } from './dist'
直接実行されているのはHonoのTypeScriptのソースコードではなくてJavaScriptに変換した後のコードであることに注意が必要です
全体のソースコード
-
QuickJS: QEMU、FFmpegの同作者というのもまたすごい ↩︎
-
Responseのコンストラクタが定義されていなかったようです https://github.com/awslabs/llrt/issues/168 ↩︎
Discussion