1137 文字
6 分
Cloudflare WorkersでGleamを動かす

Gleamという静的型付け・関数型言語がある.

gleam-lang
/
gleam
Waiting for api.github.com...
00K
0K
0K
Waiting...

Erlang VMやJavaScriptランタイムで動くようコンパイルされるのが特徴.

今回は,「JavaScriptにトランスパイルできる」というところに注目し,Cloudflare WorkersでGleamを動かしてみた.

環境構築#

依存関係にgleam_javascriptを追加する.

また,ローカルでの動作確認・デプロイ用にwranglerをインストールする.

gleam new workers_gleam
cd workers_gleam
gleam add gleam_javascript
pnpm init
pnpm add -D wrangler

gleam.toml を編集.targetにJavaScriptを指定した.

name = "workers_gleam"
version = "1.0.0"
target = "javascript"

[dependencies]
gleam_stdlib = ">=0.34.0 and < 2.0.0"
gleam_javascript = ">=0.12.0 and < 1.0.0"

[dev-dependencies]
gleeunit = ">= 1.0.0 and < 2.0.0"

また,wrangler.tomlを以下のように記述する.

name = "workers-gleam"
main = "build/dev/javascript/workers_gleam/index.js"
compatibility_date = "2023-09-22"

[build]
command = "gleam build"

GleamとJavaScriptをFFIで繋ぐ#

GleamはJavaScriptやErlangと相互に運用できるため,FFIの仕組みが備わっている.

ここでは,JavaScriptで定義されているResponseRequestといったオブジェクトをGleamでも使えるようにしていく.

まずはsrc/ffi.jsにresponse, method, url, bodyを追加する.

export function response(status, headersList, body) {
  let headers = new Headers()
  for (let [k, v] of headersList) headers.append(k, v)
  return new Response(body, { status, headers })
}

export function method(request) {
  return request.method
}

export function url(request) {
  return request.url
}

export function body(request) {
  return request.body
}

src/cloudflare/workers.gleamでffi.jsで書いた関数とGleamの関数を結びつける.

pub type Request

pub type Response

pub type Environment

pub type ExecutionContext

@external(javascript, "../ffi.js", "response")
pub fn response(
  status: Int,
  headers: List(#(String, String)),
  body: String,
) -> Response

@external(javascript, "../ffi.js", "method")
pub fn method(req: Request) -> String

@external(javascript, "../ffi.js", "url")
pub fn url(req: Request) -> String

@external(javascript, "../ffi.js", "body")
pub fn body(req: Request) -> String

これでGleam側でresponsemethodを実行したときには,JavaScript側の関数を呼び出すようになった.

ハンドラーを作る#

これらの型や関数を使ってハンドラーを書く.

handle_requestはJavaScriptのPromise<Response>を返さなければならないため,返り値をPromise(Response)としている.

また,promise.resolveに渡すことでresponsePromiseでラップしている.

import cloudflare/workers.{type Request, type Response}
import gleam/javascript/promise.{type Promise}

pub fn handle_request(
  _request: Request,
) -> Promise(Response) {
  workers.response(200, [], "Cloudflare Workers上でGleamが動いてるよ")
  |> promise.resolve
}

エントリポイントを作る#

最後にCf Workers用のエントリポイントを作る.src/index.jsの中に書いていく.

import { handle_request } from "./workers_gleam.mjs"

export default {
	async fetch(request) {
		return handle_request(request)
	}
}

一行目でimportしているworkers_gleam.mjsはビルド時にsrc/workers_gleam.gleamから生成されるJavaScriptモジュールで,Gleamで定義したhandle_requestがそのままJavaScriptの関数としてexportされている.

import * as $promise from "../gleam_javascript/gleam/javascript/promise.mjs";
import * as $workers from "./cloudflare/workers.mjs";
import { toList } from "./gleam.mjs";

export function handle_request(_) {
  let _pipe = $workers.response(200, toList([]), "Cloudflare Workers上でGleamが動いてるよ");
  return $promise.resolve(_pipe);
}

Envを使う#

Cf Workersではfetch関数の第二引数にenvを受け取る.Cf Workersに設定した環境変数がKey-Value形式で格納されたオブジェクトになっている.

.dev.varsに環境変数を追加する.

SECRET_KEY=5U93R_53CR37_K3Y

この値をGleamから取り出して使う.

ffi.jsに新たにread_environment(env, key)関数を用意するが,この関数はGleamのResult型を返すようにする.

値が存在すればOk,無ければErrorを返すようにしておけば,Gleamからは安全にEnvの値にアクセスすることが出来るためである.

JavaScriptからGleamの機能にアクセスするには,./gleam.mjsをインポートする.gleam.mjsもビルド時に生成され,Gleamの一部機能をJavaScriptから使えるようになっている.

import { Ok, Error as Err } from "./gleam.mjs"

export function read_environment(env, key) {
	const value = env[key]
	return value ? new Ok(value) : new Err(undefined)
}

src/cloudflare/workers.gleamread_environmentを追加する.

@external(javascript, "../ffi.js", "read_environment")
pub fn read_environment(env: Environment, key: String) -> Result(String, Nil)

ハンドラーで呼び出す.

import cloudflare/workers.{type Environment, type Request, type Response}
import gleam/io
import gleam/javascript/promise.{type Promise}

pub fn handle_request(_request: Request, env: Environment) -> Promise(Response) {
  let _ =
    workers.read_environment(env, "SECRET_KEY")
    |> io.debug

  workers.response(200, [], "Cloudflare Workers上でGleamが動いてるよ")
  |> promise.resolve
}

エントリポイントもEnvを受け取るように変更する.

import { handle_request } from "./workers_gleam.mjs"

export default {
  async fetch(request, env) {
    return handle_request(request, env)
  }
}

実行すると,ログに設定した環境変数が表示される.

image.png


FFIコードを書かなければならないものの,GleamをCf Workersで動かせることが分かった.

KVやR2などのBindingsが使えないかどうか,今後も調査を進めていきたい.

この記事を書いている最中に,Deno DeployやCloudflare Workersでも動かせるGlenというGleam製フレームワークを発見した.

実際にCf Workersに載せるなら,自力でFFIを頑張るのではなく,このようなフレームワークを使うのが良いだろう.

MystPi
/
glen
Waiting for api.github.com...
00K
0K
0K
Waiting...

参考#

lpil
/
gleam-cloudflare-worker
Waiting for api.github.com...
00K
0K
0K
Waiting...
GitHubで編集を提案