このエントリーはPerl Advent Calendar 2021の 16 日目の記事です。
モチベーション
Cloudflare Workers が面白くて、よくいじっているのですが
これは JavaScript で書くものなんですよね。
やっぱり Perl で書きたい!!
ということで、Cloudflare Workers のスクリプトを Perl で書いてみました。
PSGI に対応するアプリにしたので、 plackup
でも動きます。
つまり… Cloudlare Workers でも Perl でも動く Perlを書いたことになります!
紹介します。
Cloudflare Workers の書き方おさらい
Cloudflare Workers の書き方をおさらいしましょう。 Service Workers の API にならった書き方をすることになります。
const handleRequest = (request) => {
const url = new URL(request.url);
const message = url.searchParams.get("message");
const data = { message: message };
return new Response(JSON.stringify(data), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
};
addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});
公式のサンプル
ネタバラシみたいになりますが、実は Cloudflare が公式に Perl で Cloudflare Workers で動かすサンプルを公開しています。
でもこれはあくまでサンプル程度のもの。今回はこれを参考にもう少し踏み込んだアプリを作ります。
Perlito
確認ですが、Cloudflare Workers は Perl を実行できません。よって、Perl のスクリプトを JavaScript に変換して、それをwrangler
なりminiflare
といった、仮想環境や本番で動かすことになります。
公式のサンプルではPerlitoってのを使っています。Perl5 から JavaScript に変換してくれるやつで、他にも Java にしたり、Perl5 から Perl6(Raku)の相互変換できます。ブラウザで Perl から JavaScript のコードに変換するデモもあり、面白いです。
こいつを使うと、インラインで JS を書けます。
JS::inline('addEventListener("fetch", event => { p5cget("main", "listener")([event]) })');
すると、p5cget
で指定した関数listener
が呼ばれます。
sub listener {
my ($event) = @_;
my $msg = "Perl Worker hello world";
my $res = Response->new($msg, { status => 200 });
$event->respondWith($res);
}
これだけで動くのです!あ、正確には動く JavaScript を生成できるのです。
Response
に注目してください。
Perl スクリプト内ではResponse
を定義していていません。
さらに、引数から受け取った$event
にはrespondWith
メソッドが生えています。
なんと、Cloudflare Workers が提供している JavaScript の「Response」オブジェクトなどが Bareword でも Perl ライクに使えちゃうんです。
ということは、JavaScript の URL オブジェクトを使う Perl スクリプトはこう書けちゃいます。 見間違わないでください。Perl の URI モジュールじゃないんです。
my $url = URL->new($req->url);
my $path = $url->pathname;
もう Perl なんだか JavaScript なんだかよくわかりません!
実際に、my
を書くところをconst
と書きそうになったり、プリントデバグしたいのにconsole.log
と書いてしまったりしました。
PSGI アプリ
Perl で Cloudflare Workers を書けるようになりました。 ただし、Cloudflare Workers のサンプルを Perl に移植するだけではつまらないので、まず Perl の PSGI アプリを書いてそれを上記のような JS ライクなコードから利用できるようしましょう。
PSGI について軽くおさらい。PSGI はスペックです。PATH_INFO
やQUERY_STRING
の入ったハッシュ$env
をリクエストとして受け取ります。レスポンスは以下のようなステータスコードとヘッダーとコンテンツの入った配列を返します。
[ 200, [ 'Content-Type' => 'text/plain' ], ["Hello, It's PSGI!"] ];
こいつを実装すれば、PSGI のツールキットであるplackup
などでサーバーとして立ち上げることが出来ます。
今回は
- GET / => テキストを返す
- GET /hello?name=yusukebe => クエリ(name)を解釈してテキストを返す
- GET /api?message=hello => JSON を返す
を実装してみました。
まずはルーター部分。素朴に書きます。
sub route {
return [
[ '/' => root ],
[ '/hello' => hello ],
[ '/api' => api ],
];
}
次にコントローラー。hello
はこうなります。
sub hello {
my ($param) = @_;
my $name = $param->{name} || 'Someone';
return [ 200, [ 'Content-Type' => 'text/plain; charset=UTF-8' ], ["Hello! $name!!"] ];
}
これは純粋な Perl コードですね!安心感があります。
$env
を受け取って、ルーティングをdispatch
すれば完成です。
sub app {
return sub {
my $env = shift;
my $path_info = $env->{PATH_INFO};
my $query_string = $env->{QUERY_STRING};
return dispatch($path_info, $query_string);
}
}
app()
で返される無名関数を返すスクリプトを書けば、plackup
で実行できます。
アプリ部分をapp
という名前空間でindex.pl
という名前のファイルで書いていたとして、
require 'index.pl';
app::app();
というapp.psgi
を用意します。そしてplackup
を実行!
$ plackup app.psgi
立ち上がりました!ブラウザで確認してみましょう!
動いてますね!plackup
だけではなく PSGI に対応するサーバーならなんでも動きます。
これこそ、純粋な PSGI アプリです!
Cloudflare Workers で PSGI アプリを利用する
ではこの PSGI アプリを Cloudflare Workers でも動くようにしましょう。 以下の流れです。
addEventListener
が発火- 渡ってくる FetchEvent オブジェクトから Request オブジェクトを取得
- Request から URL を取得
- URL を使って、PSGI に渡す
$env
を作る - PSGI アプリを実行
- 返ってきた配列を分解
- Response オブジェクトを作成
$event.respondWith
に渡す
具体的なコードを引用すると…
sub listener {
my ($event) = @_;
my $req = $event->request;
my $resp = handleRequest($req);
$event->respondWith($resp);
}
が最初に呼ばれて、handleRequest
します。
sub handleRequest {
my ($req) = @_;
# URL is JavaScript Object
my $url = URL->new($req->url);
my $query_string = $url->search;
my $env = { PATH_INFO => $url->pathname, QUERY_STRING => $query_string };
# Dispatch PSGI app
my $psgi = app->($env);
PATH_INFO
など必要な要素だけを入れた$env
を作って、PSGI アプリに渡します。$psgi
にレスポンスの配列が入ります。
こいつを分解して、Cloudflare Workers にわたす「Response」にします。
例えば $psgi->[0]
にはステータスコードが入っています。
my $msg = $psgi->[2][0];
my $status = $psgi->[0];
# Headers is JavaScript Object
my $headers = Headers->new();
my $key;
for my $v ( @{ $psgi->[1] } ) {
...
$headers->append($key, $v);
...
}
# Response is JavaScript Object
my $res = Response->new(
$msg,
{
status => 200,
headers => $headers
}
);
return $res;
どうでしょう!入力(FetchEvent)も出力(Response)も JavaScript 相当のものです。 これを Perlito を使ってコンパイルした JavaScript が Cloudflare Workers で動くようになります!
ビルドして、wrangler
で立ち上げてみましょう。
立ち上がりました!8787
ポートにブラウザでアクセスすると、先程のplackup
で立ち上げた5001
ポートのアプリと全く同じ挙動をします!やりました!我々は Cloudflare Workers でも Perl でも動くアプリを作ったのです!
公開
公開してみましょう!
$ wrangler publish
たったこれだけで全世界に PSGI アプリを Cloudflare Workers で公開したのです! 以下は私がデプロイしたホストです。期待する挙動をします。
完成品
完成品はこちらになります。
実際のところ、例えば
JS::inline('addEventListener("fetch", ...
がスクリプト内にあるとエラーになるので、ビルドの際に無理やりecho
して JS に追加する、とかやってます。
とはいえ、動きます。
$ npm run build
や、wrangler を立ち上げる
$ npm run dev
や
$ npm run publish
などをpackage.json
に書いておきました。
当然ながら、plackup
があれば
$ packup app.psgi
も出来ます。もしよかったら試してみてください。たぶん動くかもしれません。
まとめ
以上、Cloudflare Workers を Perl で書いて、 主たるロジックを PSGI アプリにして、Perl でも実行できるようにしました。 Perl の中に JS のオブジェクトをそのまま書いて、どっち書いてるのか分からなくなるのが面白かったです。