Fiddle で C のメモリを free した時にやらかした話

備忘録。

少し前に Fiddle を使って C 関数を呼び出す Ruby プログラムを書いた時にやらかした時のメモ。

Fiddle とは

とてもざっくりというと dlopen/dlsym のラッパー。以下は C の strlenRuby から呼び出す例がドキュメントにある。

起きたこと

以下のような C ライブラリを Ruby から呼び出す必要があった。

// ライブラリが提供する構造体
struct foo_t;

int main(void) {
  // ライブラリ内で malloc & 初期化した foo_t を取得
  foo_t *foo = new_foo();

  // foo を介してライブラリの機能を使う
  work_with_foo(foo);

  // アプリケーション側の責務として取得した foo を free で開放する必要がある
  free(foo);
}

これを Ruby から Fiddle 経由で使うために(だいぶ簡易化しているが)以下のようなコードを書いた。((実際には Fiddle::Importer を使った))

require 'Fiddle'

# libfoo のラッパー
module LibFoo
  extend Fiddle::Importer
  dlload 'libfoo.so'
  extern 'void *new_foo(void)'
  extern 'void work_with_foo(void *)'
end

# free を呼び出すための libc のラッパー
module LibC
  extend Fiddle::Importer
  dlload 'libc.so.6'
  extern 'void free(void *)'
end

# foo ライブラリを利用する
foo = LibFoo.new_foo
LibFoo.work_with_foo(foo)

# free で開放
LibC.free(foo)

動きそうだが、最後の LibC.free の呼び出しで abort してしまった。

何が原因だったか

abort する Ruby プロセスを確認したところ、malloc/free が libc から libjemalloc に置き換わっていることがわかった(LD_PRELOAD を使っていた)。つまり以下のような動きになっていた。

  • LibFoo.new_foo 内部の malloc 呼び出しはプログラムがリンクしている libjemalloc の malloc が呼び出される
  • LibC.free は明示的に libc の free が呼び出される

つまり jemalloc が管理しているメモリを libc の free で解放しようとして死んでいた。

どうするべきか/どうあるべきか

実際にこの問題に遭遇した時はプログラムが libclibjemalloc のどちらを使っているかを判断して、Fiddle でオープンする共有ライブラリを使い分けるようにした。

Fiddle.free というそれっぽい関数もあるが、これは Fiddle.malloc で確保したメモリにしか使ってはならないので断念。(おそらく Ruby インタプリタ内部のメモリ使用量管理と絡んでいる?)

そもそもの話として「アプリケーションコードに free 呼び出す責務がある」というライブラリ側の仕様が微妙な気もする。「開放処理も delete_foo などでラップする」「ライブラリ内部で使うアロケータを利用者が指定できるようにする」などの仕様の方がいいかもしれない。