XSでメモリリークを起こさずプログラムを書く方法
XSでメモリリークを起こさずプログラムを書く方法を解説します。
Perlのメモリ管理はリファレンスカウント方式
まず基礎知識としてPerlのメモリ管理はリファレンスカウント方式によって、行われているということを、知っておいてください。リファレンスカウント方式では、リファレンスカウントが0になった時点で、メモリの解放が行われます。つまり、Perlにおいてメモリ解放を行うということは、リファレンスカウントを0にするという操作を行うことと等しいです。
リファレンスカウント方式についてもう少し説明しておきます。Perlの変数においては、最初に変数を宣言したときに、その変数のリファレンスカウントは1になります。また、変数への参照が作られると、リファレンスカウントが1増やされます。
{ # $strのリファレンスカウントは1になる。 my $str = 'Hello'; # $strのリファレンスカウントは2になる。$str_refのリファレンスカウントは1になる my $str_ref = \$str; }
またPerlではスコープを抜けると、自動的に変数が解放されます。これは、なぜかというと、変数は、自動的に、モータルと呼ばれる状態になっているからです。モータルという概念は非常に重要です。モータルとは、「スコープを抜けたときに、リファレンスカウントが自動的に1減らされる状態」という意味です。
上記のコードでスコープから抜けたときに何が起こるかを記述します。
{ # $strのリファレンスカウントは1になる。 my $str = 'Hello'; # $strのリファレンスカウントは2になる。$str_refのリファレンスカウントは1になる my $str_ref = \$str; } # $str_refのリファレンスカウントが1減らされて0になります。 # $str_refのリファレンスカウントが0になったので、$str_refは解放されます。 # $str_refが解放されたので、$strのリファレンスカウントは2から1になっています。 # $strのリファレンスカウントが1減らされて0になります。 # $strのリファレンスカウント0になったので、$strは解放されます。 # ('Hello'は$strの内部に含まれているのこれも解放されます)
このような経緯をたどって、メモリは解放されます。
XSにおけるメモリ管理の基礎
次にこれを踏まえてXSにおけるメモリ管理の基礎について解説します。
Perlの変数
最初にPerlの変数について簡単に解説します。
Perlの変数について解説しておきます。まず内部的には、スカラ変数は「SV*型」で表現されます。配列は「AV*型」、ハッシュは「HV*型」で表現されます。まずこのみっつを覚えましょう。リファレンスはもちろん「SV*型」に代入できます。そして、内部的には、「AV*型」と「HV*型」は、「SV*型」から派生しているということを、覚えておきましょう。これは、アップキャスト、ダウンキャストができるという意味です。
スカラ変数の作成は次の関数で行います。XSにおいては、文字列、浮動小数点、整数で、作成する関数が異なるということを覚えておきましょう。
SV* sv_str = newSvPV("Hello", 0); SV* sv_num = newSvNV(1.2); SV* sv_num_int = newSvIV(4);
配列とハッシュの生成は次の関数で行います。
AV* av_nums = newAV(); HV* hv_scores = newHV();
リファレンスの生成は次の関数で行います。
SV* sv_str_ref = newRV_inc(sv_str);
他のnewRVという関数もありますが、リファレンスを生成するときは、newRV_incで、リファレンスカウントを1増やすことが原則です。
作成した変数はすべてモータルにする
次にメモリ管理に進みます。Perlのメモリ管理の鉄則は、新しく作成するPerlの変数は、すべてモータルにするということです。モータルにすることによって、スコープを抜けた変数のリファレンスカウントは1減らされ、自動的にメモリ解放されます。
新しく作成するPerlの変数はすべてモータルにする。
モータルにするにはsv_2mortal関数を使用します。引数には「SV*型」を受け取り、戻り値はモータルにされた「SV*型」です。
sv_2mortal(SV* sv_var)
次のように使用します。
SV* sv_str = sv_2mortal(newSvPV("Hello", 0));
sv_2mortalに「AV*型」「HV*型」などを渡すには、「SV*型」にアップキャストして、さらに受け取るときに「AV*型」「HV*型」にダウンキャストする必要があります。
AV* sv_nums = (AV*)sv_2mortal((SV*)newAV()); HV* hv_nums = (AV*)sv_2mortal((SV*)newHV());
リファレンスを作成する場合もsv_2mortalを使います。
SV* sv_str_ref = sv_2mortal(newRV_inc(sv_str));
このように変数をモータルにしておくと、Perlのスコープが終わった時点で、リファレンスカウントが1減らされ自動的に解放されます。C言語のスコープではなくってPerlのスコープが終わった時点なので、区別しましょう。以下のようなXSの関数は、内部的には、関数全体がPerlのスコープで囲われています。
SV* foo(...) PPCODE: { /* Perlのスコープの開始 */ SV* sv_str = sv_2mortal(newSvPV("Hello", 0)); XSRETURN(0); /* Perlのスコープの終わり */ }
以下のように戻り値として返したときは、Perlのコードに戻った時点で、リファレンスカウントが1増やされ、モータルになっているのでリファレンスカウントが1減らされるので、結果としてリファレンスカウントは変化しません。
SV* foo(...) PPCODE: { /* Perlのスコープの開始 */ SV* sv_str = sv_2mortal(newSvPV("Hello", 0)); /* スタックに積んで、戻り値として返却 */ XPUSHs(sv_str); XSRETURN(1); /* Perlのスコープの終わり */ }
配列とハッシュにデータを格納する場合
上記の鉄則を守りながらコーディングをしていくと、配列とハッシュにデータを格納した場合にメモリ解放がうまくいきません。なぜなら、配列とハッシュは、それ自体が破棄されるときに、内部に含まれる「SV*型」のデータのリファレンスカウントを1下げてしまうためです。
これを回避するために、配列とハッシュにデータを格納する場合は、リファレンスカウントを手動で1増やしてあげる必要があります。配列の場合は、av_push,av_storeを使う場合、ハッシュの場合はhv_storeを使う場合がこれに該当します。リファレンスカウントを増やすにはSvREFCNT_inc関数を使用します。
/* 配列に格納する場合 */ SV* sv_num = sv_2mortal(newSvIV(3)); AV* av_nums = (AV*)sv_2mortal((SV*)newAV()); av_push(av_nums, SvREFCNT_inc(sv_num)); /* ハッシュに格納する場合 */ SV* sv_score_math = sv_2mortal(newSViv(60)); HV* hv_scores = (HV*)sv_2mortal((SV*)newHV()); hv_store(hv_scores, "math", strlen("math"), SvREFCNT_inc(sv_score_math), 0);
Cの構造体に、Perlのデータを格納する場合
Cの構造体にPerlのデータを格納する場合は、自分でメモリ管理を行う必要があります。この話の前提として、C言語の構造体をPerlのオブジェクトとして扱う方法を見ていただくとよいと思います。
C言語の構造体に、「SV*型」を保存したい場合を考えましょう。sv_nameというメンバを持つPeopleという構造体を宣言してみました。
struct People { SV* sv_name; };
この場合は、代入するときにリファレンスカウントをSvREFCNT_incを使って増やします。そうしなければ、Perlのスコープを抜けた瞬間にリファレンスカウントは1下げられ、勝手に解放されてしまうからです。
SV* foo(...) PPCODE: { /* 省略 */ /* 構造体の作成(ポインタとして作成) */ People* people = (People*)malloc(sizeof(People)); SV* sv_name = sv_2mortal(newSvPV("kimoto", 0)); people->name = SvREFCNT_inc(sv_name); /* 省略 */ }
そして、デストラクタの中で、リファレンスカウントをSvREFCNT_decを使ってひとつ下げます。
void DESTORY(...) PPCODE: { // オブジェクトを取得 SV* people_obj = ST(0); // デリファレンス SV* people_sv = SvROK(people_obj) ? SvRV(people_obj) : people_obj; // SV*型をsize_t型に変換 size_t people_iv = SvIV(people_sv); // size_t型をポインタに変換 People* people = INT2PTR(People*, people_iv); // sv_nameを解放 SvREFCNT_dec(people->sv_name); // People*を解放 free(people); XSRETURN(0); }
このようにC言語の世界の構造体(クラスも同じ)のメンバにデータを保存したい場合は、手動でリファレンスカウントの増加と減少を行う必要があります。
まとめ
まとめると、要点は3つ。
- 新しくPerlの変数を作成した場合はsv_2mortalを使って変数をモータルな状態にする。
- 配列とハッシュに値を格納するときは、SvREFCNT_incを使って手動でリファレンスカウントを増やす。
- 構造体のメンバに保存する場合は、格納するときにSvREFCNT_incでリファレンスカウントを増やし、デストラクタで、SvREFCNT_decを使って、リファレンスカウントを1減らす。
これで、たいていの場合には対処できると思います。
参考
この解説ででてきたXSで利用する関数については以下の記事で詳しく解説しています。