天然パーマです。

Cloudflare Workers🔥でもPerl🐫でも動くPerlを書く

このエントリーは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_INFOQUERY_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 のオブジェクトをそのまま書いて、どっち書いてるのか分からなくなるのが面白かったです。