PerlにおけるGraphQLライブラリのまとめ

リッチなフロントエンドを開発しているとGraphQLを使いたくなるときがあり、さらにそのバックエンドをPerlで実装したくなるときもあるでしょう。

このエントリではPerlでGraphQL開発をしたい時に使える2023/02/24現在のPerlのGraphQL周りのライブラリ事情についてまとめます。

GraphQLフレームワーク

graphql-perl

PerlCPANモジュールを使ってGraphQLバックエンドを実装したいとなった場合に現状コレ一択。

PerlCareersの支援で資金援助を受けて実装されている。

世界観としてはThis module is a port of the GraphQL reference implementation, graphql-js, to Perl 5.と言っているが、graphql-jsの忠実な移植ではなく、限りなく処理を独自実装しているのが特徴。

MooやType::TinyなどのモダンPerl的なライブラリをふんだんに使っていたり、GraphQLのパースをPegex::Parserを使い正規表現を用いて行っている。

やや実装に難がありパフォーマンスに問題が発生しがちな傾向にある。

khrt/graphql-perl

graphql-jsの忠実な移植プロダクト。 残念ながら開発が途中で止まっていそう。

stevan/p5-Graph-QL

khrt/graphql-perlと同様にgraphql-jsの忠実な移植。 開発が5年で止まって入るが、比較的実装されている。

パーサーライブラリ

GraphQL::Language::Parser

前述のgraphql-perlの一部分

Parser::GraphQL::XS

graphql公式のリポジトリにあるC++で実装されたGraphQLパーサー( https://github.com/graphql/libgraphqlparser )のXSバインダ。

ただしParser::GraphQL::XSにはlibgraphqlparserをインストールするみたいなおもてなしが無いことから、このモジュールをインストールする際は自前でlibgraphqlparserをインストールする必要がある。

DataLoader

GraphQLでクエリに対するSQLをいい感じにまとめる時に使われる機能であるDataLoaderの参考実装。 Mojoliciousが動いていることが前提となっているため、Mojoliciousを使っていないwebアプリケーションには直接組み込めない


というわけで基本graphql-perlを使えば良いのだけど、graphql-jsの素朴な移植もほしいですね。。。

Perlでスマートに配列からランダムで特定個数の値を取得する

TL;DR

use List::Util qw(sample);

my @array = (1..100);
# 3個ランダムでとりだす

my @random_pickup = sample 3, @array;

人間生きていると配列の中身からランダムにn個取得したいときがあります。Perl書いているときもありますね。

よくある方法は次のような感じでしょうか

my @array = (1..100);
my @random_array =  shuffle @array ;
my @random_pickup  = splice @random_array, 0, 3;

List::Utilのshuffleを使い配列をランダムにばらしたあとに、標準関数のspliceを使い特定の数の値を取り出してきます。 これだと@random_pickupには3つのランダムな数が入る訳ですね。

ただしこの方法だと必ず全件をshuffleしないといけないため、巨大な配列を対象にする場合はそこそこコストが掛かります。 他には結局@random_pickupが必要な場合は、中間変数の@random_arrayが不必要ですね。 (最も、工夫すれば中間変数を作らずにかけますが...)

そこでオススメなのがList::Utilのsample関数です。1.54から同梱されているので、比較的最近のList::Utilになら入っています。 ちなみにList::MoreUtilsにも入っています。

書き方は簡単で以下の感じです。

my @items = sample $count, @values; # sample {取得したい数}、 {リスト}

$count@valuesの配列長より大きい場合は、shuffleと同じ挙動になります。

近年のList::UtilはXS(C言語実装)も同梱されており、sampleも見てみるとC言語レベルで特定個数のランダム取得をしてくれています。 このため、コスト的にもshuffleしてからspliceなどで取得するより最適化されておりオススメです。 metacpan.org

2022年、CPAN(Perlの)モジュールのメンテナを引き継ぐ活動を始めた件

これははてなエンジニアアドベントカレンダー2022 42日目の記事です。 昨日は id:k-murakami0609 さんの 過去に所属してたチームに転生したら導入したいもの でした。

はてなのノベルチームで日常的に使っている便利グッズ最高ですね!! みなさんもノベルチームにjoinして体験してください!!!

さて今回は2022年にぼちぼち始めたCPANモジュールのメンテナを引き継ぐ活動についてお話しようかなと思います。

CPANモジュール

CPANモジュールとはご存知プログラミング言語Perlのモジュールシステムのことです。

Perlインタプリタに付随しているコアモジュールも含めて、PerlではCPANと呼ばれるアーカイブにモジュールがアップロードされ、cpanmcpmなどのツールを通してインストールし利用する世界観になっています。

TeXのモジュールアーカイブのCTANに影響されて作成されたと言われていることからも分かる通り、CPANは1995年から存在しており、歴史的にも古いシステムになっています。

そんなCPANには大小様々なモジュールがアップロードされています。特に有名どころではAcme名前空間でしょうか。 他の言語に無い特徴ですが、PerlのモジュールではAcme::と名前がつくモジュールはジョークモジュールとなっていて、様々なおもしろモジュールがアップロードされています。(どういう物があるか知りたい方はAcme大全がオススメです)

CPANモジュールの今

そんなCPANモジュールですが、Perl使用者の人口減に伴って、新規モジュール開発及びモジュールのメンテナが減少傾向にあります。

最近ではElasticsearchの公式クライアントライブラリのSearch::ElasticsearchもElasticsearch8を持ってPerlクライアントの提供を終了することを発表したりしています。

日本でもPerlを書かれていたエンジニアの方々が、別の言語にコミットすることになったり、また多忙でPerlを書く時間が取れなかったりと、様々な理由でメンテナが減少しています。 使わないモジュールのメンテナンスをし続けるのはなかなかしんどいですよね。

Perlではここ数年@INCがカレントディレクトリから削除されたことなどもあるため、継続してメンテナンスをしていくのは後方互換性が強いPerlといえども必要そうです。

とはいえ、はてな社にはまだPerlで元気に動いているPerlプロダクトがありますし、何よりid:anatofuzPerlが好きなので、できればモジュールのメンテは継続していきたいです。

他にはCPANモジュールではないですが、Perlの公式Dockerイメージの管理もごく数人で行っているので、こういったところにもコミットしたいなと思っていました。

CPANモジュールのメンテに関わる活動

そこで去年はパッチを当てたいOSSを中心にメンテナを引き継ぐ活動をしました。 そこまで件数が多いわけではなかったですが、声を書けながらメンテ権を頂いています。

溜まっていたPRの解決や、新規PRのレビュー等に参加することで、継続してメンテナンスを行っています。

github.com

直近ではTest::WWW::Stubのメンテに関わりました。

metacpan.org

モジュール以外ではdocker-perlやOpenAPIのスキーマからクライアントコードを自動生成するOpenAPI GeneratorのPerlクライアントもPRを送っています。

github.com

github.com

CIの修正なども

他にはPerlモジュールは、日本でよく使われるCPANモジュールオーケストレーションツールのMinillaが標準でTravis CI用の設定ファイルを出力していたことから、Travis CIでCIを回しているものが多かったです。

しかしTravis CIは近年では動かなかったりするので、これをGitHub actionsに移動するなどの活動もメンテ権をもらうついでにシュッと進めています。

github.com

CPANモジュールのメンテを引き継ぐ方法

CPANモジュールのメンテはGitHub等のリポジトリを引き継ぐ + CPANモジュールのcomaintに登録してもらうことで引き継ぐことができます。

gfx.hatenadiary.org

この部分はオリジナルの作者の方にやっていただく必要があるため、コミュニケーションが必要です。 権限を付与されてからは自分のCPANモジュールをメンテするように操作することが可能です。

ただし引き継がれたCPANモジュールをアップロードする際はx_authorityという権限に気をつける必要があります。

blog.64p.org

※追記 id:shoichikaji さんより最近はいらないとの情報を得ました!

※追記ここまで

新しいモジュールを作りつつメンテもしていく

新しいモジュールを作るのも楽しいですが、既存のモジュールのメンテをするのも盆栽をするようで楽しいです。 また偉大な先人のコードに自分も関わることができ、広く使われていく体験もできるので、ぜひPerlに興味がある方はCPANモジュールなどに関わっていきましょう!!!

明日のはてなエンジニアアドベントカレンダーは同じチームのid:deflis55さんです! お楽しみに!

git.ioは非推奨(read only)になってしまったので注意する必要がありそう

表題が全てシリーズです。(シリーズとは)

完全にノーマークだったのですが、GitHubが提供していた短縮URLサービスのgit.ioが4月27日を持ってread onlyに移行しています。

2022-04-27 Update: While the git.io url redirection service is read-only and use of the service is limited, we have received feedback from developers and academic researchers who have published git.io links in print documentation and research papers. In order to preserve the integrity of these historical documents, we have decided to archive the current git.io links in a new read-only service that will allow us to serve redirects for those links longer term.

As we continue our analysis, we may remove individual links that point to spammy, malicious or 404 links. Our goal is to not break links relied on for legitimate use, especially by the academic community, while preserving the security of developers on GitHub.

That said, we still encourage users to make use of one of the many URL shortening services available with greater functionality than the git.io service provided. GitHub support will not be able to update or edit redirection records served by the git.io archive service.

github.blog

もともとは4月29日に全てのリダイレクトを止める方針だったようですが、論文等ですでに利用されていることもあり、リダイレクトは止めず、新たに追加/編集等ができない読み取り専用に移行したようです。

(これはgit.ioがスパムなどの目的で使われてしまっており、そのメンテナンスコストを鑑みての判断らしいです。)

read-onlyになったのでまだ使用できるとはいえ、非推奨になってしまったので、新たなURLに移行するのが良さそうです。

Perlの人むけ情報

Perlの人向け情報としては、cpmcpmを使ってインストールする方法が、以前はgit.ioを使ってインストールする方法が推奨されており、この問題で推奨インストール先のURLが変更されているので注意が必要です。

github.com

これは作者のid:shoichikajiさんからもCHANGELOGで変更が呼びかけられています。

github.com

差分は次のような感じです。

旧版

$curl -fsSL https://git.io/cpm | perl - install -g --with-develop --with-recommends --show-build-log-on-failure

移行版

$curl -fsSL https://raw.githubusercontent.com/skaji/cpm/main/cpm | perl - install -g --with-develop --with-recommends --show-build-log-on-failure

ということで気づきベースで変更していくのが良さそうです。git.ioがこんな状況になってたの知らなかった....。

いい感じにuseするのを助けてくれるApp::perlimportsのご紹介

これはPerlアドベントカレンダー2021の12日目の記事です。

昨日はshogo82148さんで、Perl 5.35.5 の iterating over multiple values at a time を先取りでした。

今回はPerlでもいい感じのモジュールのImportをしてくれるApp::perlimportsの紹介です。

App::perlImports

このモジュールはThe Perl and Raku Conference 2021で作者本人からトークされていて、スライド及び動画が上がっています。

www.youtube.com

スライドはGitHub上にHTMLがあるので手で落とす必要があるようです...。(GitHubPagesは特に使っていない模様)

curl https://raw.githubusercontent.com/oalders/presentations/main/slides/6-perlimports/remark.html -o perlimports.html && open perlimports.html

「Where Did That Symbol Come From?」とトークタイトルが示す通り、Perlのシンボルがどこからimportされたかを明確にしたいというのが実装の動機になっているようです。合わせて動き自体はgolangでimportしているモジュールをいい感じに管理してくれるgoimportsを参考にしているようです。

このモジュールについて説明するにはまず、Perlのモジュールロードがどのように行われているかを確認する必要があります。ざっと見てみましょう。

Perlのモジュールロード

Perlはモジュールをロードする方法に主にuseを使った方法とrequireを使った方法があります。*1 歴史的経緯でrequireの方が先に出ていて、 Perl5でuseが導入されています。 requireはモジュールロード以外に外部ファイルの読み込みにも使われます。

pointoht.ti-da.net

近年はuseを使ってモジュールロードをするのが一般的です。

例えば、PerlにデフォルトでバンドルされているHTTPクライアントのHTTP::Tinyを使う場合は次のようにロードします。

use HTTP::Tiny;

my $ua = HTTP::Tiny->new();

ロードタイミング

Perl動的言語でありますが、useを使ったモジュールロードの処理は、該当のPerlコードをPerlインタプリタが読み込む、コンパイルタイムに行われます。 例えば存在しないHogeモジュールをロードしようとすると、次のようにprintが走ることはなくエラーで死にます。

#!/usr/bin/env perl

use strict;
use warnings;
print "hello";
use Hoge;
❯ perl hoge.pl
Can't locate Hoge.pm in @INC (you may need to install the Hoge module) (@INC contains: /Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/site_perl/5.32.0/darwin-2level /Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/site_perl/5.32.0 /Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0/darwin-2level /Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0) at hoge.pl line 2.
BEGIN failed--compilation aborted at hoge.pl line 2.

対して、各関数呼び出しなどの処理は実行時に解決されます。 そのためuseを忘れても、対象のメソッドを実行しない限りは処理が止まることはありません。 例えば次のコードは、HTTP::Tinyをuseしていませんが、呼び出す前にdie(Perlでのエラー終了)をして処理を止めるので、ロードエラーは発生しません。

#!/usr/bin/env perl
use strict;
use warnings;


print "hello\n";
die;

my $ua = HTTP::Tiny->new();
$ua->get('https://example.com');
perl hoge.pl
hello
Died at hoge.pl line 6.

useの処理

ではuseは何をしているのでしょうか。Perldoc.jpを見るとこう書かれています。

指定したモジュールから、現在のパッケージにさまざまな内容をインポートします; 多くは、パッケージのサブルーチン名や、変数名に別名を付けることで、 実現されています。 これは、以下は等価ですが

BEGIN { require Module; Module->import( LIST ); }

https://perldoc.jp/func/use

BEGIN { }はBEGIN ブロックと呼ばれるもので*2コンパイルタイムに実行させるという処理です。 中ではrequireを使い、モジュールを読み込んだ後に、そのモジュールにあるimportメソッドを実行しています。

つまり、前述したPerlのモジュールロードのuserequireを比較すると次のような違いがあることがわかります。

項目 use require
ロードタイミング コンパイルタイム 実行時
副作用 importを実行する 読み込みのみ

整理したところで、このimportとは一体何でしょうか。 一般的には、これはモジュールで定義しているメソッドを、呼び出し元のコードで定義したかのように使うために実行されるものです。 詳しく見てみましょう。

import

Perlでは特定のモジュールをuseすると使えるようになるメソッドがいくつか存在します。 例えばコアモジュールのJSONを扱うライブラリであるJSON::PPをuseすると、任意のPerlのハッシュをJSON文字列化できるencode_jsonが使えるようになります。

use strict;
use warnings;

use JSON::PP;

print encode_json({hoge => 'hello', isBool => \0});

encode_jsonJSON::PPで定義されているメソッドなので、通常はJSON::PP->encode_jsonのように使う必要があります。 importは特定のメソッドを、useしたタイミングで通常の関数のように呼び出せるようによしなにやってくれる処理になっています。

この処理のことをPerlでは関数のエクスポートなどと読んでいます。 近年ではExporterモジュールが行っています。 あるモジュールで、Exporterモジュールを継承、もしくはuseすると、そのモジュール内で自動的に関数のエクスポート処理をimport関数として登録してくれます。

あくまでExporterを使った場合なので、独自にimport関数を定義することも可能です。 その場合は通常の関数のように呼び出せるようによしなにやってくれる処理以外の処理をuse時に行うように差し込むことが可能となります。 例えばClass::Accessor::LiteはimportをExport以外の目的に使っており、オブジェクトのアクセサを生成する処理を行っています。

use Class::Accessor::Lite (
    new => 1,
    rw  => [ qw(foo bar) ],
    ro  => [ qw(baz) ],
    wo  => [ qw(hoge) ],
);

Perlでモジュールをuseする際に、exportしたい関数名の指定ではないような引数を渡しているモジュールは、ほぼ独自にimportを書いていると考えても良いでしょう。

use時に差し込まれるモノたち

Exporterを使っているモジュールがuseされたときに差し込むものは、モジュール側で定義されている@EXPORT配列に積んだシンボルになっています。((ここでのシンボルはPerlのメソッド(関数、サブルーチン)の他に、スカラ変数や配列も含みますが、Exporterを使う上ではメソッド以外のシンボルを使うと問題が発生しがちなので、基本はメソッドのことです ))

例えば、Hoge::Firstモジュールをuseしたときに、hogeメソッドを差し込みたい場合は、次の様に定義します。

package Hoge::First;
use strict;
use warnings;
use feature qw/say/;

use Exporter 'import';

our @EXPORT = (qw/hoge/); #サブルーチンの名前を文字列で配列の中にいれるとEXPORTされる

sub hoge {
  say 'this is first!';
}

1;

Perl@EXPORTに積まれているオブジェクトをすべてロードします。 このためuseしているモジュールが、内部で@EXPORTに何かしら詰めていた場合、予期せぬシンボルをロードする可能性があります。 また、あるPerlファイルの中で出てきているメソッドが、一体どのモジュールから提供されているか、はたまたデフォルトのメソッドであるかの判断が難しくなってしまいます。 使用しているメソッドが、どのモジュールから提供されているのが解りづらいのは、リファクタリングの結果すでにあるモジュールが提供しているメソッドを使わなくなったのに、useし続けてしまうモジュールが出たり、逆にプログラミングしている上で使えるだろうと思って書いたメソッドが、実はuseしないといけなかったなどの問題を誘発しがちです。

また、同じ名前のオブジェクトをロードしてしまうと、呼び出し順序によって使われる関数が異なってしまいます。 例えばHoge::FirstHoge::SecondモジュールでそれぞれhogeをEXPORTしてみます。

package Hoge::First;
use strict;
use warnings;
use feature qw/say/;

use Exporter 'import';

our @EXPORT = (qw/hoge/);

sub hoge {
  say 'this is first!';
}

1;
package Hoge::Second;
use strict;
use warnings;
use feature qw/say/;

use Exporter 'import';

our @EXPORT = (qw/hoge/);

sub hoge {
  say 'this is second!';
}

1;
#!/usr/bin/env perl
# main.pl
use strict;
use warnings;

use Hoge::First;
use Hoge::Second;

hoge();

実行すると、最後にロードされたSecondが出てきます。

❯ perl -Ilib hoge.pl
this is second!

@EXPORTはこのような思いがけない処理が走る可能性があるので、Exporterモジュールのコメントにも「とりあえずEXPORTするのはやめよう」といったことが書かれています。 pointoht.ti-da.net

さらにはPerlのLinterであるPerl::Criticにも@EXPORTを警告するルールが追加されています。

metacpan.org

とはいえ関数のエクスポートは便利なので、それはそれで使用したいです。 Perlでは@EXPORTの他に@EXPORT_OKがあります。これはデフォルトではすべてをロードせず、use時にエクスポートしたい関数名を書くことを強制させる機能です。

例えばこんな感じにuse時に関数名を指定します。

use JSON qw(encode_json);

実は@EXPORTで宣言している場合もuse時に関数名を指定している場合はそのメソッドしかエクスポートされません。関数名を何も指定せずに()を書いた場合は何もエクスポートされないため、宣言的に関数名を書くようにするとほぼ@EXPORT_OKと同様の振る舞いをします。

自分が書いているコードに出てくるメソッドの宣言元をはっきりさせる単純な解決方法はEXPORTをやめ、Hoge::Foo->bar()の様にフルパスでメソッドを実行することですが、テストヘルパなどEXPORTした方が利便性が高いものもあるでしょう。 decode_utf8など、EXPORTされたメソッドを使うのがイディオム的に認知されているものもあります。 エクスポートした関数を使うのと、どこから取り込んできたのかの視認性を高めるには、use時にエクスポートしたい関数名を漏れなく書くしか無いでしょう。 しかしこれを人間がやるのは面倒!!! ということで作られたのがperlimportsです。

....と、ここまで書いたところで奇跡的に裏番組のはてなエンジニア Advent Calendar 2021id:papixさんが紹介している内容と前提知識が同じだった!!そんなことってあるんだ

papix.hatenablog.com

あらためてApp::perlImports

  • どこから来たのかわからないシンボルをエクスポートしているuseを特定したい
  • @EXPORTを全部インポートするのではなく、一部だけ指定してインポートしたい

これらの課題に対して、golangのgoimportsを参考に、構文解析ライブラリPPIの力を借りて誕生したのがApp::perlimportsです。 metacpan.org

著者のOALDERSさんはLWPなどのメンテナもやられています。

使ってみる

例えば次の様なモジュールがあったとしましょう。

package MyPkg::One;

use strict;
use warnings;

use Exporter 'import';
our @EXPORT = qw(f1);

sub f1 {
  print "f1\n";
}

1;
package MyPkg::Two;

use strict;
use warnings;

use Exporter 'import';
our @EXPORT_OK = qw(f2);

sub f2 {
  print "f2\n";
}

1;
package MyPkg::Three;

use strict;
use warnings;

use Exporter 'import';
our @EXPORT_OK = qw(f3);

sub f3 {
  print "f3\n";
}

1;
package MyPkg::Four;

use strict;
use warnings;

use Exporter 'import';
our @EXPORT = qw(f4);

sub f4 {
  print "f4\n";
}

1;

これをhoge.plから以下の様に呼び出します。

use strict;
use warnings;
use MyPkg::One;
use MyPkg::Two;
use MyPkg::Three;
use MyPkg::Four;


f1();
f2();
MyPkg::Three->f3();

hoge.plの状況は次のとおりです。

  • f1はMyPkg::OneがEXPORTしているものを使っている
  • f2はMyPkg::TwoがEXPORT_OKしているが、コード上は指定を忘れている
  • f3はフルパスで呼び出している
  • f4はuseしているが特に使っていない

このコードはそのまま実行すると, f2の解釈で実行時エラーが発生します。また、どのモジュールからエクスポートされているのかパット見ではわかりません。 さらにMyPkg::Fourは必要がないf4をエクスポートしてしまっています。

❯ perl -Ilib hoge.pl
f1
Undefined subroutine &main::f2 called at hoge.pl line 10.

これをApp::perlimportsに通すと、以下のようにコードが書き換わります。

❯ perlimports --libs lib  hoge.pl
use strict;
use warnings;
use MyPkg::One qw( f1 );
use MyPkg::Two qw( f2 );
use MyPkg::Three ();
use MyPkg::Four ();


f1();
f2();
MyPkg::Three->f3();

見ると次の様な変換が行われています。

  • f1,f2がEXPORT元のモジュールが明記されるようになった
  • エクスポートした関数を使用していないThreeFourは、空配列を指定することになり、何もエクスポートしなくなっている

変換されたコードはそのまま動かす事ができます。(今回はパイプで動かしています) こうすると、実行時エラーを未然に防ぐことが可能になり、どのモジュールからエクスポートしているか、パット見でわかるようになりました。

❯ perlimports --libs lib  hoge.pl | perl -Ilib
f1
f2
f3

さらに、無駄にロードしているMyPkg::Fourを削除することも可能です。

❯ perlimports --libs lib --no-preserve-unused  hoge.pl
use strict;
use warnings;
use MyPkg::One qw( f1 );
use MyPkg::Two qw( f2 );
use MyPkg::Three ();


f1();
f2();
MyPkg::Three->f3();

こうすると、無駄なファイルをuseし続ける事もなくなり便利ですね!

インストール方法

cpanmもしくはcpmを使ってインストールしましょう

$ cpanm App::perlimports
$ cpm install -g App::perlimports
$ plenv rehash

使い方

CLIツールとして提供されているので、perltidyの様にコマンドで実行します。

$  perlimports --libs lib,local/lib/perl5 -f hoge.pl

主要なオプションを見てみます。

  • --libs
    • Perl-Iと同様。基本はlibを指定することになるでしょう
    • カンマ区切りです
    • Carton, cpmでinstallしたローカルディレクトリ上のモジュールも含めたい場合は、local/lib/perl5を加えます
  • -f
    • 変換対象のファイル名
  • --no-padding
    • perlimportsが変換したあと、モジュール名の前後に余計な空白をいれない
    • デフォルトではuse Foo qw( bar baz );の様になるが、--no-paddingを指定するとuse Foo qw(bar baz);になる
  • --inplace-edit, -i
    • デフォルトでは変換したコードは標準出力に出るが、このオプションを指定すると変換元のファイルを上書きする
  • --no-preserve-unused
    • 使っていなさそうなモジュールをuseしないようにする

注意点

自動でuseしてくれる訳ではない

下手にgoimportsを使ったことがあると誤解しやすい点で、perlimportsの(現状)の興味の対象はEXPORTしたシンボルをいい感じに特定することのみです。goimportsはimportの追加などもしていましたが、それに対応する機能はありません。モジュールのuseし忘れなどは興味の範囲外になっています。

例えばMyPkg::One->f1()の様なメソッド呼び出しをしていて、MyPkg::Oneをuseし忘れた場合、perlimportsはこれを補完しません。 また、そもそもuseしているモジュールがない状態で実行しても補完はしてくれません。

そのためモジュールのuse忘れなどのimportエラーは別系統で判断する必要があります。

❯ cat hoge.pl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/lib";

use DDP {deparse => 1, hash_max => 0};

f1();
~/workspace/test3 via 🐪 v5.34.0
❯ perlimports --libs lib -f hoge.pl
use strict;
use warnings;
use FindBin ();
use lib "$FindBin::Bin/lib";

use DDP {deparse => 1, hash_max => 0};

f1();

関数エクスポート以外のuseは工夫の余地がある

また、Class::Accessor::LiteClass::Enumemonのようなuse時にエクスポートする関数名を指定する以外に、独自にimportsを定義しているモジュールは、perlimportsはuseしたものを使っていないと見なしてしまいます。これはperiimportsの作者によって名指しでperlimportsの自動整形下に置かない様に書かれているモジュール群か、構文解析の結果自動調整を逃れたもの以外は、空配列のimportに変換されてしまいます。

❯ cat hoge.pl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/lib";

use DDP {deparse => 1, hash_max => 0};
use Class::Accessor::Lite (
    rw => [qw/ hoge/],
);

f1();


~/workspace/test3 via 🐪 v5.34.0
❯ perlimports --libs lib,local/lib/perl5 -f hoge.pl
use strict;
use warnings;
use FindBin ();
use lib "$FindBin::Bin/lib";

use DDP {deparse => 1, hash_max => 0};
use Class::Accessor::Lite ();

f1();

これを回避するには、調整したくないモジュールをuseしている行の後ろに特定のアノテーション(## no perlimports)を書くか*3コマンド実行時の引数でモジュール名を直接列挙、またはモジュール名が列挙されたファイルを渡すことで回避可能です。この指定には正規表現が使用可能です。

作者にPRを送るとignoreするモジュールに追加してくれる様なので、普段使うモジュールで管理されるとまずいものがあればPRを送ると良いでしょう。

~/workspace/test3 via 🐪 v5.34.0
❯ cat hoge.pl
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/lib";

use DDP {deparse => 1, hash_max => 0};
use Class::Accessor::Lite (
    rw => [qw/ hoge/],
);

f1();
~/workspace/test3 via 🐪 v5.34.0
❯ perlimports --libs lib,local/lib/perl5 --ignore-modules-pattern '^Class::' -f hoge.pl
use strict;
use warnings;
use FindBin ();
use lib "$FindBin::Bin/lib";

use DDP {deparse => 1, hash_max => 0};
use Class::Accessor::Lite (
    rw => [qw/ hoge/],
);

f1();

メソッドの追加などの副作用で持つモジュールをuseしている場合は、--no-preserve-unusedに気をつける

Perlのモジュールでは、useすると自分以外のモジュールのメソッドをオーバーライドやメソッドを名前空間に追加するものがあります。 例えばHTTP::Message::PSGIは、useするとHTTP::Request名前空間to_psgiがメソッドとして追加されます。

これはuseすることで効果を発揮するモジュールであるため、HTTP::Message::PSGIがEXPORTしているメソッドを直接使っていないケースや、HTTP::Message::PSG->で何かを呼び出しているコードが無い限り、perlimportsで--no-preserve-unusedオプションを付けて実行してしまうと、何も意味がないモジュールを呼び出しているとuseを削除されてしまいます。

実際はHTTP::Message::PSGIはperlimports側で明示的に整形する対象に含まないように指定されているので、この様なことは起きないのですが、自分たちで書いているライブラリに似た挙動があるものがあれば、使用する際は気をつけましょう。


明日はid:mp0liiu さんで、immutableなコレクションを作るです!

*1:動的ロードなどを考慮するとこの限りではない

*2:awkから来ていた気がする

*3:ブロック的に囲うことも可能です

https://metacpan.org/pod/perlimports#ANNOTATIONS/IGNORING-MODULES

PerlのXSをlldbでデバッグする

興味が湧いたのでPerlのC拡張であるXS言語で書かれているList::Utilを読んでいるのだけど、C言語チックなものを読むときはやはりデバッガが使いたい。 XSも結局C言語なのでgdb/lldbでデバッグできないかなと思ってググったところ、できそうなエントリを見つけたので試していく。

stackoverflow.com

XSのデバッグビルド

C言語デバッグにはCで書かれたコードにデバッグオプションを付けてビルドする必要がある。 XSも同様だと思われるので、何かしらの方法でデバッグビルドする必要がある。

XSモジュールに限らずPerlモジュールのビルドは、Makefile.PLかBuild.PLのどちらかのビルドツールがよく使われている。 Makefile.PLは名前の通りMakefileを生成してくれる君で、Build.PLはModule::Buildを前提としたシステムになっている。 詳細はモダンPerlの世界へようこそ 第23回 Module::Build:MakeMakerの後継者を目指してが詳しい。

今回は読みたいコードがList::Utilで、List::UtilはMakefile.PLを使っていたのでこちらの雰囲気を見る。

最終的にMakefileを生成しにいくのはExtUtils::MakeMakerが提供しているWriteMakefileを実行するタイミングである。 List::UtilのMakefile.PLの場合は一番最後の行でこのように実行している。

WriteMakefile(%params);

この引数%paramsにはビルドに関する様々な情報を入れる事ができる。 ExUtils::MakeMakerのドキュメントを見ると、XSの最適化の度合いを調整出来るOPTIMIZEがオプションとして存在している。 ドキュメントを見る限りCの最適化オプションをそのまま渡せるので、デバッグしたかったら-O0 -gあたりを渡せばよいだろう。

ということでList::Utilのparamsを作っている箇所の一番下にしれっとOPTIMIZEを指定する。

my %params = (
  NAME         => q[List::Util],
  ABSTRACT     => q[Common Scalar and List utility subroutines],
  AUTHOR       => q[Graham Barr <gbarr@cpan.org>],
  DEFINE       => $defines,
  DISTNAME     => q[Scalar-List-Utils],
  VERSION_FROM => 'lib/List/Util.pm',
  OPTIMIZE    => '-g -O0',

準備は整ったのでビルドしていく。まずはMakefileを生成する必要があるので、Makefile.PLを実行する。

❯ perl Makefile.PL
Checking if your kit is complete...
Looks good
Generating a Unix-style Makefile
Writing Makefile for List::Util
Writing MYMETA.yml and MYMETA.json

無事Makefileができたので素朴にmakeする

❯ make
cp lib/Scalar/Util.pm blib/lib/Scalar/Util.pm
cp lib/List/Util.pm blib/lib/List/Util.pm
cp lib/Sub/Util.pm blib/lib/Sub/Util.pm
cp neko.pl blib/lib/List/neko.pl
cp lib/List/Util/XS.pm blib/lib/List/Util/XS.pm
Running Mkbootstrap for Util ()
chmod 644 "Util.bs"
"/Users/anatofuz/.plenv/versions/5.32.0/bin/perl5.32.0" -MExtUtils::Command::MM -e 'cp_nonempty' -- Util.bs blib/arch/auto/List/Util/Util.bs 644
"/Users/anatofuz/.plenv/versions/5.32.0/bin/perl5.32.0" "/Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0/ExtUtils/xsubpp"  -typemap '/Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0/ExtUtils/typemap'  ListUtil.xs > ListUtil.xsc
mv ListUtil.xsc ListUtil.c
cc -c   -fno-common -DPERL_DARWIN -mmacosx-version-min=11.2 -fno-strict-aliasing -pipe -fstack-protector-strong -DPERL_USE_SAFE_PUTENV -g -O0   -DVERSION=\"1.56\" -DXS_VERSION=\"1.56\"  "-I/Users/anatofuz/.plenv/versions/5.32.0/lib/perl5/5.32.0/darwin-2level/CORE"  -DPERL_EXT -DUSE_PPPORT_H ListUtil.c
rm -f blib/arch/auto/List/Util/Util.bundle
cc  -mmacosx-version-min=11.2 -bundle -undefined dynamic_lookup -fstack-protector-strong  ListUtil.o  -o blib/arch/auto/List/Util/Util.bundle  \
              \

chmod 755 blib/arch/auto/List/Util/Util.bundle
Manifying 4 pod documents

無事makeが実行できた。 ちなみにmake cleanとするとMakefileごとビルドしたものを消してくれるので便利。

デバッグビルドしたXSをPerlから呼び出す

ビルドするともとのxsのコードを純粋なC言語*1に変換されたものなどが生成される。 実際にPerlインタプリタが読みこめるファイル形式のものはblib以下に書き出される。

blibディレクトリはMakeMakerがビルドしたファイルが含まれている。 List::Utilの場合は次のようなファイルが含まれている。

❯ tree blib
blib
├── arch
│   └── auto
│       └── List
│           └── Util
│               └── Util.bundle
├── bin
├── lib
│   ├── List
│   │   ├── Util
│   │   │   └── XS.pm
│   │   └── Util.pm
│   ├── Scalar
│   │   └── Util.pm
│   ├── Sub
│   │   └── Util.pm
│   └── auto
│       └── List
│           └── Util
├── man1
├── man3
│   ├── List::Util.3
│   ├── List::Util::XS.3
│   ├── Scalar::Util.3
│   └── Sub::Util.3
└── script

16 directories, 9 files

ちなみにUtil.bundleの場合はバイナリファイルだったりする。

普通のPerlのライブラリの場合はlib以下のものを@INCに入れれば実行できた。 似た感じでblib以下のものをPerlに教えてあげれば自分でビルドしたXSをPerlインタプリタから呼び出す事ができる。

blibをPerlスクリプトから呼び出すにはblibモジュールを使う方法もあるけれど、勝手にカレントのblibディレクトリ以下を読み込んでくれるExtUtils::testlibを使うのが便利。

というわけで自分でビルドしたList::Utilを呼び出すサンプルコードは次のようになる。今回はめんどくさいのでビルドしたディレクトリ上でやっている。

use strict;
use warnings;
use ExtUtils::testlib;
use List::Util qw/all/;

my $hoge = [1,2,3];

if (all { $_ } @$hoge) {
    print "hello!\n";
}

xsのデバッグ実行

さて例題も書けたのでデバッグしていこう。 一点ポイントとなるのは、このblibを実行できるのは、XSをビルドする際に利用したPerlじゃなくと実行ができない。

今回では上のログにある通り/Users/anatofuz/.plenv/versions/5.32.0/bin/perl5.32.0が実行したPerlのバイナリである。 これ以外のPerlのバイナリ、例えば/usr/bin/perlで実行すると以下のような感じでモジュールロードに失敗する。

❯ /usr/bin/perl neko.pl
Can't load '/Users/anatofuz/src/github.com/Dual-Life/Scalar-List-Utils/blib/arch/auto/List/Util/Util.bundle' for module List::Util: dlopen(/Users/anatofuz/src/github.com/Dual-Life/Scalar-List-Utils/blib/arch/auto/List/Util/Util.bundle, 0x0001): symbol '_PL_DBsub' not found, expected in flat namespace by '/Users/anatofuz/src/github.com/Dual-Life/Scalar-List-Utils/blib/arch/auto/List/Util/Util.bundle' at /System/Library/Perl/5.30/darwin-thread-multi-2level/DynaLoader.pm line 197.
 at neko.pl line 4.
Compilation failed in require at neko.pl line 4.
BEGIN failed--compilation aborted at neko.pl line 4.

ビルド時に使ったPerlだと問題なく実行できる。

❯ /Users/anatofuz/.plenv/versions/5.32.0/bin/perl5.32.0 neko.pl
hello!

ではデバッガをかけて実行してみる。今回はlldbでやる。

❯ lldb -- /Users/anatofuz/.plenv/versions/5.32.0/bin/perl neko.pl
(lldb) target create "/Users/anatofuz/.plenv/versions/5.32.0/bin/perl"
Current executable set to '/Users/anatofuz/.plenv/versions/5.32.0/bin/perl' (arm64).
(lldb) settings set -- target.run-args  "neko.pl"
(lldb)

今回デバッグしたいxsのファイルはListUtil.xsで、使っているallのコードを見てみたい。 allは実はanyのエイリアスで、any関数は702行目から本体がある

というわけでListUtil.xsの702行目にbreak pointを貼ってみる。

(lldb) b ListUtil.xs:702
Breakpoint 1: no locations (pending).
WARNING:  Unable to resolve breakpoint to any actual locations.

そして実行すると見事に止まる。

(lldb) process launch
Process 69006 launched: '/Users/anatofuz/.plenv/versions/5.32.0/bin/perl' (arm64)
1 location added to breakpoint 1
Process 69006 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x00000001004ae5a0 Util.bundle`XS_List__Util_any(cv=0x00000001010fd810) at ListUtil.xs:702:22
   699  PROTOTYPE: &@
   700  PPCODE:
   701  {
-> 702      int ret_true = !(ix & 2); /* return true at end of loop for none/all; false for any/notall */
   703      int invert   =  (ix & 1); /* invert block test for all/notall */
   704      GV *gv;
   705      HV *stash;
Target 0: (perl) stopped.

これで止まるとこっちのものなので、あとはnextしたり値をprintしたりして確かめていくと普通に読んでいける。 読み方としてはPerlインタプリタ読んでるのと同じなので、SVの中身に思いを馳せる感じになっていくと思う。

というわけでXSも実はlldbで読める!!! 読んでいくぞ!!!

*1:といってもPerlのマクロがバチバチにあたっている

perlのmapは2引数渡せる

というわけで驚きの事実です。この2行は同じhashrefを返します。

my $hoge = [map { id => $_, piyo => "bar" }, @hoge];
my $hoge = [map { {id => $_, piyo => "bar"} } @hoge];

これはmapの後ろの{はmapの開始地点の意味と、hashrefのコンストラクタとしての意味の{があります。 通常のmapの使い方だと、{をそれぞれの意味で解釈できるように2つ置く必要があります。

my $hoge = [map { {id => $_, piyo => "bar"} } @hoge];

これとは逆に{1つだとmapのhashrefのコンストラクタという意味になり、後ろにLISTが必要となります。 ドキュメントを見るとこうあります。

map BLOCK LIST
map EXPR,LIST

perldoc.jp

というわけでこうかくとhashrefがなんと手に入ります。,がポイント。

my $hoge = [map { id => $_, piyo => "bar" }, @hoge];

とはいえこの書き方はperltidyがフォーマットしてくれないのでやめたほうが良いでしょう。2引数は渡さないのが良さそう。