はじめに
Crystal言語はRubyに似た静的型付けのプログラミング言語です。Rubyほどの柔軟性はありませんが、ほとんどRubyのような見た目のコードが、CやRustに匹敵する速度で動作するのが痛快です。
Crystalが常にlibpcre2をリンクする問題
Crystalは大企業のバックアップがない、小さなコミュニティによって運営されている言語であり、いくつかの外部ライブラリに依存しています。例えば、ガベージコレクションはlibgc-devに依存しており、ファイル出力などはlibevent-devに依存しています。
Crystalは正規表現にlibpcre2を利用しています。
しかし、正規表現を使用しないコードをコンパイルしても、libpcre2にリンクされてしまうという問題がありました。
puts "hello world"
ldd hw
# 正規表現を使っていないのにlibpcre2にリンクされてしまう
調査してみた
本来は、gdbやlldbをもっと活用すべきだったと思います。
でも今回はLLVM-IRを出力して調査しました。実際にLLVM-IRをコンパイルすることで、ビルドフラグにライブラリが必要かどうかを確認できます。
puts "hello world"
このコードをコンパイルして、含まれている関数を確認します。
crystal build --release hw1.cr
nm hw1
特に指定がない限り、Crystalは以下のライブラリをロードします。
https://github.com/crystal-lang/crystal/blob/master/src/prelude.cr
nm hw1 | grep pcre2
grepで検索すると、
U pcre2_config_8
この関数が呼び出されていることがわかります。次に、中間表現LLVM-IRを出力します。
crystal build --emit llvm-ir hw1.cr
hw1.ll
は次のリンクフラグでコンパイルできます。このコンパイルには-lpcre2-8
が必要です。
clang hw1.ll -lm -lz -levent -lgc -lpcre2-8
hw1.ll
をテキストエディタで開き、pcre2
を検索すると次の宣言が見つかります。
declare i32 @pcre2_config_8(i32, ptr) local_unnamed_addr
ここで使用されています。
%41 = call i32 @pcre2_config_8(i32 11, ptr %40), !dbg !17427
次の行を以下のように変更してみます。
%41 = add i32 0, 17
これで、pcre2なしでコンパイルできるようになります。
clang hw1.ll -lm -lz -levent -lgc
pcre2へのリンクがないことを確認します。
ldd a.out
実行するとエラーが発生します。
Unhandled exception: Invalid libpcre2 version (RuntimeError)
from /usr/local/share/crystal/src/regex/pcre2.cr:18:33 in '~Regex::PCRE2::version_number:init'
問題は標準ライブラリのRegex::PCRE2
のこの部分にあります。
module Regex::PCRE2
@re : LibPCRE2::Code*
@jit : Bool
def self.version : String
String.new(24) do |pointer|
size = LibPCRE2.config(LibPCRE2::CONFIG_VERSION, pointer) ## %41 HERE!!
{size - 1, size - 1}
end
end
class_getter version_number : {Int32, Int32} = begin
version = self.version
dot = version.index('.') || raise RuntimeError.new("Invalid libpcre2 version") ## THE ERROR!!
space = version.index(' ', dot) || raise RuntimeError.new("Invalid libpcre2 version")
# PCRE2 versions can contain -RC{N} which would make `.to_i` fail unless strict is set to false
{version.byte_slice(0, dot).to_i, version.byte_slice(dot + 1, space - dot - 1).to_i(strict: false)}
end
この行の呼び出しが%41
と一致します。
LibPCRE2.config(LibPCRE2::CONFIG_VERSION, pointer)
この行が実際に行っているのは、libpcre2のバージョンの取得です。そこで、バージョンを直接指定すると、
version = "10.42 2022-12-11" # self.version
pcre2
がリンクされなくなります。
crystal build hw1.cr
ldd hw1
さらに調査を進め、class_getter
がbegin
で始まっているのが問題であることがわかったので、修正するプルリクエストを送りました。このプルリクエストはマージされ、1.14から適用される予定です。今後は、Crystalで作成した任意のプログラムがlibpcre2にリンクされることはなくなるはずです。
まとめ
Crystalは小さなコミュニティが支える言語であり、現在も日々改良が続けられています。ソースコードもCrystalで書かれているため、RubyやPythonのような文法に慣れている人は、それなりに簡単にバグを特定できるでしょう。今回Crystalに意味のあるプルリクエストを送ったのは2回目だったので、とても嬉しくなり、この記録記事を書きました。
Crystal言語をどうぞよろしくお願いします。
この記事は以上です。