3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Crystalで出力した実行ファイルが常にlibpcre2にリンクされてしまう問題を修正した話

Last updated at Posted at 2024-08-21

はじめに

Crystal言語はRubyに似た静的型付けのプログラミング言語です。Rubyほどの柔軟性はありませんが、ほとんどRubyのような見た目のコードが、CやRustに匹敵する速度で動作するのが痛快です。

Crystalが常にlibpcre2をリンクする問題

Crystalは大企業のバックアップがない、小さなコミュニティによって運営されている言語であり、いくつかの外部ライブラリに依存しています。例えば、ガベージコレクションはlibgc-devに依存しており、ファイル出力などはlibevent-devに依存しています。

Crystalは正規表現にlibpcre2を利用しています。

しかし、正規表現を使用しないコードをコンパイルしても、libpcre2にリンクされてしまうという問題がありました。

hw.cr
puts "hello world"
ldd hw
# 正規表現を使っていないのにlibpcre2にリンクされてしまう

調査してみた

本来は、gdbやlldbをもっと活用すべきだったと思います。

でも今回はLLVM-IRを出力して調査しました。実際にLLVM-IRをコンパイルすることで、ビルドフラグにライブラリが必要かどうかを確認できます。

hw1.cr
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_getterbeginで始まっているのが問題であることがわかったので、修正するプルリクエストを送りました。このプルリクエストはマージされ、1.14から適用される予定です。今後は、Crystalで作成した任意のプログラムがlibpcre2にリンクされることはなくなるはずです。

まとめ

Crystalは小さなコミュニティが支える言語であり、現在も日々改良が続けられています。ソースコードもCrystalで書かれているため、RubyやPythonのような文法に慣れている人は、それなりに簡単にバグを特定できるでしょう。今回Crystalに意味のあるプルリクエストを送ったのは2回目だったので、とても嬉しくなり、この記録記事を書きました。

Crystal言語をどうぞよろしくお願いします。

この記事は以上です。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?