はじめに
Perlの備忘録として書いてます。
前回は tie による変数の拡張を扱いました。今回は、Perlがコードを解釈する「前」の段階に介入する「ソースフィルタ」について。
ソースフィルタとは
ソースフィルタは、Perlインタプリタがソースコードを読み込み、構文解析(パース)を行う前に、そのソースコードのテキストそのものを加工する仕組みです。
これを利用することで、Perlの文法を拡張したり、独自の構文を導入したりすることが可能になります。
基本的な仕組みとしては Filter::Util::Call モジュールを使いますが、より扱いやすい Filter::Simple を使うのが一般的です。
基本の仕組み:Filter::Util::Call
まずは仕組みを理解するために、Filter::Util::Call を使って、古典的な「ROT13(アルファベットを13文字ずらす暗号)」のコードを実行できるようにするフィルタを作ってみます。
package Rot13Filter;
use Filter::Util::Call;
sub import {
my ($type) = @_;
my ($ref) = [];
filter_add(bless $ref);
}
sub filter {
my ($self) = @_;
my $status;
# ソースコードを読み込んで加工する
if (($status = filter_read()) > 0) {
# ROT13 変換
$_ =~ tr/a-zA-Z/n-za-mN-ZA-M/;
}
$status;
}
1;
これを使うと、以下のような暗号化されたコードがそのまま動作するようになります。
use Rot13Filter;
# "print 'Hello world';" のROT13
cevag 'Uryyb jbeyq';
Perlが use Rot13Filter を認識すると、それ以降の行をフィルタに通してからパースを行います。
実践:Filter::Simple で簡易マクロ
Filter::Util::Call は便利ですが、文字列リテラルやコメントの中まで無差別に置換してしまうリスクがあるようです。
そこで、Perl 5.8以降で標準搭載されている Filter::Simple を使います。これを使うと、「コード部分だけ」を対象にフィルタを掛けることが簡単にできます。
例として、C言語の #define のようなマクロ置換を行うフィルタを実装してみます。
package MyMacro;
use Filter::Simple;
# FILTER_ONLY code => ... とすることで、
# 文字列やコメントを除外して置換できる
FILTER_ONLY code => sub {
# DEFINE_FUNC foo を sub foo { ... } に置換
s/DEFINE_FUNC\s+(\w+)/sub $1 { print "I am $1\n" }/g;
};
1;
使用例:
use MyMacro;
DEFINE_FUNC foo;
DEFINE_FUNC bar;
foo(); # I am foo
bar(); # I am bar
# 文字列内は置換されないので安全
print "DEFINE_FUNC baz"; # そのまま "DEFINE_FUNC baz" と出力される
ソースフィルタの注意点とデバッグ
ソースフィルタは「パース前のコード」を変えてしまうため、デバッグが難しくなります。
- エラー行のズレ: 加工によって行数が変わると、エラーメッセージの行番号が元のソースと合わなくなる
- 実態が見えない: 実際にPerlが実行しているコードが目に見えない
対策:
フィルタを通した後のコードがどうなっているか確認するには、コマンドラインで B::Deparse を使います。
perl -MO=Deparse script.pl
これにより、フィルタ処理され、パースされた後の「Perlが認識しているコード」が出力されるため、意図通りに置換されているかを確認できます。また、Filter::tee モジュールを使って変換結果をファイルに吐き出す方法もあります。
まとめ
- ソースフィルタは、パース前のソースコードを直接加工する仕組み
-
Filter::Util::Callが基本だが、通常はFilter::Simpleを使うのが安全で簡単 - デバッグが難しいため、困ったときは
B::Deparseで中身を確認する
次回は、デバッグの話でも登場したPerlのコンパイラバックエンド B モジュールについて記載します。