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引数は渡さないのが良さそう。

プログラミング言語Raku(旧:Perl6)のオンリーカンファレンスが開催されるらしい

インターネット見ていたら発見してしまったのですが、ついにプログラミング言語Raku(旧 Perl6)のオンリーカンファレンスが開催されるようです。

conf.raku.org

今までのRakuはPerlのカンファレンスであるYAPCやPerlConでPerl5とセットで話される機会がほとんどでしたが、ついに初めて単独カンファレンスとなります。 今のところはトークはLTしか応募されていないようですので、みなさん応募しましょう!!(??)

Online, August 7, 2021

とのことです。 以前のThe Perl and Raku Conference 2020 cloudの際はEDTでスケジュールされていたので、日本からリアルタイムでみる場合は夜になるかと思います。


参加チケットは無料ですが、個人で寄付をすることが可能です。

面白いのはTシャツを発送することができないので、Tシャツの素材が配布されており、各自でプリントアウトしてくれ!!!という新スタイルで運用しています。僕もTシャツほしい。

f:id:anatofuz:20210516191406p:plain

ちなみに The Perl and Raku Conference 2021 cloudもやるっぽい。

git commitしたら別のブランチに自動でcherry-pickするPerlスクリプトかいた

今は開発環境でM1 macを使っているのだけれど、色々あってプルリクを送りたいブランチと自分の手元のブランチが異なっているケースがある。ようはM1専用のcommitを積んでいるが、これをプルリクに含めたくないケース。

最近はgit慣れてないのもあって、commitする度にcheckoutしてcherry-pickしていたのだけど、めんどうなのでスクリプトを書いた。 自分が今作業しているブランチをhoge_m1という命名規則にして、プルリクを出すブランチをhogeにするみたいな運用。suffixで分類する。

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

my $git_branches = `git branch`;

die "$git_branches\n" if ($git_branches =~ /fatal: not/);

my $current_git_branch = `git branch --show-current`;
chomp $current_git_branch;

my $target_branch;
if ($current_git_branch =~ /(.*)_m1$/) {
    $target_branch = $1;
    die "not found $target_branch branch" unless ($git_branches =~ /$target_branch/);
}

system("git","commit");

system("git", "checkout", $target_branch);
system("git", "cherry-pick", $current_git_branch);

system("git", "checkout", $target_branch);

オチ

git rebase勉強した方が早かった気がする

サブルーチンのエイリアスを型グロブで作る

知らなかったのでメモ。

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

sub hoge {
  print "hello\n";
}

*foo = \&hoge;

foo();

この例題を実行すると元気にhelloと表示される。 これは型グロブを使ってhogeサブルーチンのエイリアス的にfooを登録している。 代入しているのはサブルーチンリファレンスなので参照している実態は同じとなる。

また代入先も変数そのものではなくて型グロブに対してなので、呼び出しが$foo->()ではない。

世間で使われているものだとSmart::Argsに使われていた。

metacpan.org

よくみたら公式ドキュメントにそれっぽい記述があった。 これができるのも面白いけどuse strict環境で普通に実行できたのがびっくり。。。。