Featured image of post 在 Cloudflare Workers 使用 Go 編譯的 Wasm

在 Cloudflare Workers 使用 Go 編譯的 Wasm

輕鬆部署 Serverless 到雲端。

Cloudflare Workers 是一款 Severless 佈署平台,預設使用 JavaScript 作為開發語言(更新:最近也支援了原生 Rust)。在官方的文件中,雖然有各種語言轉換成 JavaScript 或編成 Wasm 的例子,但卻未有針對 Go 作出進一步說明。本文章將會介紹如何把 Go 編譯出來的 Wasm 佈署到 Cloudflare Workers 中。

編譯 Wasm

由於 Go 原生支援 Wasm,因此編譯起來十分簡單。只需執行以下指令:
GOOS=js GOARCH=wasm go build -o main.wasm main.go

另外,也可以使用 TinyGo 來編譯,使 Wasm 檔案更小。

佈署前準備

一般來說 Go 在執行 Wasm 時,需要在 JavaScript 的 context 中先運行 wasm_exec.js,但如果直接使用這個官方的檔案,無法在 Worker 的環境下直接執行,原因是因為 Worker 不支援

  • require("fs")原因
  • performance.now, process.hrtime 等時間相關的 API(原因

因此需要稍作修改才能正常使用,另外在編寫 Go 程式時也要注意避免使用會用到以上功能的 Function。

  • 針對 fs 部份,需要把 require 的程式碼刪去(第 31 至 36 行
  • 針對 performance.now 的部份,需要自己另外加入 polyfill:
globalThis.performance = {
  now: Date.now,
};

目前在 Go 的社群中,也有討論到因為需要支援不同環境的 Wasm,因此目前正在修改 wasm_exec.js,讓用戶自行定義所需的 polyfill。相關討論

佈署到 Cloudflare Workers

方法 1:使用 REST API 上傳 metadata

優點

  • 不需安裝附加的軟件便能上傳

缺點

  • 難以在本地端除錯或測試
  • 需要自行打包 JavaScript

1. 先準備好 Worker 程式碼

import "./wasm_exec_worker_polyfill.js";
import "./wasm_exec.js";

async function handleRequest(request) {
  // init go wasm instance
  const go = new globalThis.Go();
  const instance = await WebAssembly.instantiate(WASM, go.importObject);
  go.run(instance);

  // call wasm functions here
  const r = globalThis.someWasmFunction(body);

  return new Response(r, {});
}

addEventListener("fetch", (event) => {
  event.respondWith(handleRequest(event.request));
});

2. 打包 JavaScript

這邊介紹的是使用 Parcel 2 進行打包,另外也可以視乎個人習慣使用 Webpack, gulp 等工具。

package.jsonscript 加入 parcel build --no-source-maps ./main.js
執行後在 ./dist 將會出現一個打包好的 main.js

3. 準備 metadata

以下 metadata.json 設定將會將你的 .wasm 在 JavaScript 中 bind 成 WASM 這個變數。

{
  "body_part": "script",
  "bindings": [
    {
      "type": "wasm_module",
      "name": "WASM",
      "part": "wasm"
    }
  ]
}

3. 上傳

確保已準備好 metadata.json, ./dist/main.jsmain.wasm
及設定環境變數 SCRIPT_NAME, CF_ACCOUNT_ID, CF_API_TOKEN

curl -X PUT "https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/workers/scripts/$SCRIPT_NAME" \
    -H "Authorization: Bearer $CF_API_TOKEN" \
    -F "[email protected];type=application/json" \
    -F "script=@dist/main.js;type=application/javascript" \
    -F "[email protected];type=application/wasm"

完成後便能在 Cloudflare Dashboard 的 Workers 版面看到剛上傳的 script。

參考:Let’s build a Cloudflare Worker with WebAssembly and Haskell

方法 2:使用 Wrangler

目前在 Cloudflare Worker 官方文件未有 Go 使用 Wrangler 佈署的例子。

優點

  • 方便在本地端除錯及測試
  • 毋需自行打包 JavaScript

缺點

  • Wrangler 需額外安裝
  • 使用 ES Module 格式佈署後,在 Dashboard 暫未支援 Quick edit

安裝 Wrangler

npm i @cloudflare/wrangler -g

1. 準備 Worker 程式碼 (ES Module)

在 Wrangler 要上傳 Wasm 需要 modules format 才能支援 CompiledWasm,因此這邊用了 ES Modules 語法,與方法 1 的語法稍有分別。

import "./wasm_exec_worker_polyfill.js";
import "./wasm_exec.js";
import WASM from "./main.wasm";

export default {
  async fetch(request, env, ctx) {
    // init go wasm instance
    const go = new globalThis.Go();
    const instance = await WebAssembly.instantiate(WASM, go.importObject);
    go.run(instance);

    // call wasm functions here
    const r = globalThis.someWasmFunction(body);

    return new Response(r, {});
  },
};

2. 設定 Wrangler

參考:官方文件

name = ""
type = "javascript"
account_id = ""
zone_id = ""
workers_dev = true

[build.upload]
format = "modules"
main = "./main.mjs"

[[build.upload.rules]]
globs = ["**/*.wasm"]
type  = "CompiledWasm"

2. 除錯及佈署

Wrangler 本身已經提供了本地除錯及佈署的功能,因此在設定好 wrangler.toml 後,便可以立即執行。

除錯:wrangler dev
佈署:wrangler publish

倪任爾 Ngai Yam Yi / CC BY-NC-SA 4.0