PerlならサンプルコードPerl入門

2010-08-09

XSによるC/C++バインディング入門

XSを使ったC/C++のバインディングについて解説します。

XSモジュール作成の流れ

Perlから呼び出すことのできるように作成したC言語(あるいはC++)の関数のことをエクステンションといいます。一般的にはXSと総称して呼ばれることが多いようですので、XSと呼ぶことにします。XSモジュールを作成してPerlから呼び出すためのモジュールの作成までの流れを書きたいと思います。次のような作業の流れになります。 

  1. XSファイルの作成
  2. XSファイルからC言語のソースファイルを生成
  3. C言語のソースファイルからダイナミックリンクライブラリを生成
  4. ダイナミックリンクライブラリを呼び出すためのPerlモジュールの作成
XSファイルの作成

最初はXSファイルの作成です。拡張子は(.xs)です。ファイルの中にはC言語のソースコードとXSUBが含まれています。XSUBはXS言語で書かれC言語の関数にとてもよく似ています。実質としてはC言語の関数を記述するものだと思ってください。

# C言語セクション(ライブラリの読み込みなど)
...

# XSUBセクション(XS言語で書かれる)
戻り値
関数名
型
処理(処理の部分はC言語)

戻り値
関数名
型
処理(処理の部分はC言語)
XSファイルからC言語のソースファイルを生成

XSファイルをxsubppと呼ばれるプログラムを使って、C言語のソースファイルに変換します。このときにtypemapと呼ばれる、C言語のPerlの型を相互変換するため定義が利用されます。typemapはExtUtils::MakeMakerモジュールに含まれています。

                        xsubpp
XSファイル + typemap ------------> C言語ソースファイル

XS言語を利用すると、多くの場合C言語とPerlの型の変換の処理を自分で書く必要がありません。xsubppがtypemapファイルを参照して、自動的に変換処理を追加してくれます。変換処理を自分で書くことやtypemapを拡張することもできます。

ダイナミックリンクライブラリの生成

C言語ソースファイルはCのコンパイラ(gccなど)を使ってコンパイルされ、ダイナミックリンクライブラリ(*.soなど)に変換されます。

                       Cコンパイラ
C言語ソースファイル  ------------------>   ダイナミックリンクライブラリ

ダイナミックリンクライブラリは動的に呼び出すことが可能なライブラリのことです。通常はこの形式でライブラリは作成されます。

ダイナミックリンクライブラリを呼び出すためのPerlモジュールの作成

最後にダイナミックリンクライブラリを呼び出すためのPerlのモジュールを作成します。XSLoaderというモジュールを使うと、ダイナミックリンクライブラリを読み込んで、Perlの関数として、ダイナミックリンクライブラリに書かれた関数を呼び出すことができるようになります。

package YourPackage;
use XSLoader;

XSLoader::load 'YourPackage', $YourPackage::VERSION;

XSLoaderは@INCに含まれる検索パスのautoというディレクトリの下にあるダイナミックリンクライブラリを読み込みます。ダイナミックリンクライブラリは、Windowsではdll、Linux等ではsoという拡張子です。

実際にXSモジュールを作成するには

上記の流れをしっかりと覚えておきましょう。XSモジュールを作成する場合はどのようなものを作成する場合でも流れは同じです。プログラマが専念する必要があるのはXSファイルの作成です。残りのほとんどの作業は自動化することができますので、簡単なXSモジュールの作成は難しいものではありません。

ただし一般的にいえばXSモジュールの作成は難しいものです。「XS言語」「typemap」「Perl API」「make」「Cコンパイラ」「リンカ」「ExtUtils::MakeMaker」などのそれほど簡単ではない周辺知識を大量に必要とするからです。ですから、まず基本きっちり押さえた上で、周辺知識を身につけることが大切です。

XSモジュールの作成

XSモジュールを作成するにはh2xsというコマンドを使用します。

# h2xsコマンド
h2xs [オプション] C言語ヘッダファイル

このコマンドは本来はC言語のヘッダファイルからXSモジュールを作成するものです。

そのためh2xs(つまり「C言語のヘッダファイル to XS」という意味)という名前がついていますが、単にXSモジュールの雛形を作成するのにも利用できます。

XSモジュールを作成するには次のオプションでh2xsを実行します。

# XSモジュールの作成
h2xs -A -n SomeModule

「-A」はオートローディングの機能を省くオプションです。XSモジュールを作成するときは通常オートローディングの機能を省きます。「-n」でXSモジュールの名前を指定します。

このコマンドを実行するとカレントディレクトリに「SomeModule」という名前のディレクトリが作成されます。

モジュールの構成

作成されたモジュールは次のような構成になっています。

Changes
SomeModule.xs
lib - SomeModule.pm
Makefile.PL
MANIFEST
ppport.h
README
t - SomeModule.t

簡単にそれぞれのファイルの役割について解説したいと思います。

  • Changes - モジュールの変更履歴を記述します。
  • SomeModule.xs - XSファイル。XS言語でXSUBを記述します。
  • lib - Perlのモジュールが格納されます。このモジュールからC言語で書かれたライブラリが呼び出されます。
  • Makefile.PL - C言語ソースファイルのコンパイルやモジュールのインストールなどのためのmakeファイルを生成するためのPerlスクリプトです。
  • MANIFEST -XSモジュールを配布するときに必要なファイルを記載します。
  • ppport.h - 古いPerlとの互換性を維持するためのC言語ヘッダファイルです。
  • README - モジュールの簡単な説明です。
  • t - テストスクリプトが格納されます。

この中で一番大切なのは「SomeModule.xs」です。最も簡単なXSを書く場合に記述する必要があるのは「SomeModule.xs」だけです。少し難しいことをするためには、Makefile.PLの書き方を覚える必要があります。lib以下に含まれるPerlモジュールのファイルに追加で記述することもあるでしょう。また配布用にパッケージ化するときには、Changes、MANIFEST、READMEを書く必要があります。またテストスクリプトを書くことも必要になります。

XSファイルの雛形

では最初にSomeModule.xsを開いてみてください。次のような雛形が生成されているはずです。

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include "ppport.h"


MODULE = SomeModule		PACKAGE = SomeModule		

XSファイルの先頭にはC言語の記述を行うことができます。「#include "EXTERN.h"」などはC言語の記述です。C言語を書いたことがない人のために少し解説をしておきます。「#include」は他のファイルをソースコードに取り込むためのものです。C言語では#から始まる命令はディレクティブと呼ばれ、プリプロセッサに対する命令になります。プリプロセッサとはソースコードをコンパイルする前に、ディレクティブの指定に従ってソースコードに変更を加えるプログラムのことです。

#include "EXTERN.h"

という記述は、EXTERN.hというC言語のヘッダファイルをソースコードの中に取り込むということを意味しています。

「.h」という拡張子を持つファイルはC言語のヘッダファイルです。ヘッダファイルというのは、C言語の関数の宣言やマクロの定義が記述されているファイルです。C言語のソースコードの中でヘッダに書かれた関数を使用したい場合にインクルードする必要があります。

XSファイルを書く場合に必要になるC言語ヘッダファイルについて簡単に解説しておきます。

  • EXTERN.h - Perlのグローバル変数を参照するために必要なヘッダファイル
  • perl.h - Perlのヘッダファイル
  • XSUB.h - XSUBのためのヘッダファイル
  • ppport.h - Perlのバージョン間のAPIの差異を吸収するためのヘッダファイル

次の記述からXS言語での記述が始まります。

MODULE = SomeModule		PACKAGE = SomeModule		

MODULEはXS言語の始まりと、定義される関数の名前空間を指定するのに使われます。これはPerlのパッケージとは異なるので注意してください。本来C言語というのは名前空間を持たない言語です。簡単な名前の関数を定義したとするならば、衝突してしまうことでしょう。XSファイル内ではXSUBと呼ばれる関数を定義することになりますが、XSUBの関数名はシンプルなものでかまいません。たとえば数値を2倍する関数であれば、twiceという名前でよいでしょう。けれども、これをC言語でそのまま利用したとするならば、名前の衝突が発生するでしょう。そこでこのtwiceという名前の関数はxsubppで処理されるときに「XS_SomeModule_twice」という名前に置き換えられます。衝突しない仕組みを与えるために必要になるのがMODULEです。

PACKAGEはPerlのパッケージ名です。つまりMODULEの記述とPACKAGEの記述でPerlとC言語の関数の間に次のような対応ができることになります。このような対応を作成して、PerlからC言語の関数を呼び出せるようにすることをブートストラップ(bootstrap)するといいます。

Perl                        C言語
SomeModule::twice    --->    XS_SomeModule_twice
XSUBの記述

ここからはXS言語を記述する部分です。XS言語を使ってXSUBを記述してみましょう。XSUBはXS言語を使った関数の記述で、C言語の関数とよく似ています。以下では受け取った整数を2倍する関数を書いています。

void
twice (...)
  PPCODE:
{
  // 引数の個数をチェック
  if (items != 1) {
    croak("Usage twice(x)");
  }
  // Perlのスカラをintに変換。「ST(0)」は第一引数
  int x = SvIV(ST(0));
  
  // 2倍を計算
  int x2 = x * 2;
  
  // intをPerlのSVに変換して、戻り値に設定。XPUSHsは、戻り値をひとつ積む。
  XPUSHs(sv_2mortal(newSViv(x2)));
  
  // 戻り値の個数を指定してreturn
  XSRETURN(1);
}

itemsという変数には引数の個数が代入されているのでチェックに利用できます。croakという関数を使って、エラーメッセージを出力してプログラムを終了することができます。

XSで書かなければいけないことは、型変換という作業につきます。引数で受けとったPerlの型をC言語の型に変換します。それから、C言語による処理を行って、C言語の型を、再びPerlの型に変換します。型変換については、後ほど詳しく解説します。

また以下の形はそのまま覚えてしまいましょう。

void
関数名(...)
  PPCODE:
{
  
}
コンパイルと実行

前回に解説したようにPerlのモジュールから利用するためには、XSファイルをxsubppで処理し、C言語ソースファイルをコンパイルして、ダイナミックリンクライブラリを作成する必要がありました。けれどもこの処理はmakeというプログラムによって自動化することができます。Makefile.PLはこの作業を自動化するmakeファイルを生成してくれます。ですから以下のコマンドを実行するだけです。

# makeファイルの生成
perl Makefile.PL

# xsubppの処理とコンパイル
make

成功しない場合はXS言語に間違いがある可能性があります。この作業が終わるとダイナミックリンクライブラリやPerlのモジュールが含まれるblibというディレクトリが生成されます。

モジュールを呼び出すスクリプトを記述してみましょう。XSファイルがあるディレクトリと同じディレクトリにtest.plというファイルを作成してください。

# test.pl
use SomeModule;
print SomeModule::twice(2);

このスクリプトは次のように実行できます。blibのモジュールを読み込むために、blibというモジュールを-Mオプションを使って読み込む必要があります。

# C言語で書かれた関数を実行
perl -Mblib test.pl

出力結果は次のようになりました。

4

これでXSの基礎を学び終えました。これをアレンジすれば、XSを応用することができます。

Perlの型とC/C++言語の型の相互変換

PerlからC/C++の関数を呼び出すということは、Perlの型をC/C++の型へ変換を行い、C/C++言語の関数を呼び出し、結果を、C/C++言語の型に戻してPerlに返すということです。つまり、覚えることの大部分は型変換を覚えることにつきます。

ここではどのような型変換にでも対応できるような情報を提供したいと思います。

SV*型からint型へ

Perlでは関数に渡される値は、ほぼスカラ型です。スカラ型はXSの世界では「SV*型」になります。C言語の関数がもしint型を要求しているなら、SV*型をint型に変換する必要があります。

SV*型の値は、IVという領域に整数型の値を所有しています。つまり、int型に変換するためには、SV*型に含まれるIVの値を取り出してあげればよいわけです。

SV*型の値に所有されているIVの値を取り出すには「SvIV(sv)」を使用します。

XSの世界では整数型はI32という型にマッピングされているので、I32型に代入します。I32型はintと互換があります。

# SV*型からint型へ
void
foo(...)
  PPCODE:
{
  SV* num_sv = ST(0);
  I32 num = SvIV(num_sv);
  
  XSRETURN(0);
}
int型からSV*型へ

次にPerlの世界へ返却するにはint型をSV*型に変換する必要があります。int型から新しくSV*型を作成するには、newSViv(iv)を使用します。

ひとつ注意してほしいのは、SV*型を新しく作成した場合は、必ずsv_2mortal(sv)を使って、値をモータルと呼ばれる状態にしてください。値をモータルにすることによって、SV*が使用されなくなったときに、自動的にメモリが解放されます。これを忘れると、メモリリークが発生します。

# int型からSV*型へ
void
foo(...)
  PPCODE:
{
  I32 num = 3;
  SV* num_sv = sv_2mortal(newSViv(num));
  XPUSHs(num_sv);
  XSRETURN(1);
}
SV*型からunsigned int型へ

SV*型の値はUVと呼ばれる符号なし整数の値を所有しています。つまり、unsigned int型に変換するためには、SV*型の値に所有されているUVの値を取り出してあげればよいわけです。

SV*型の値に所有されているUVの値を取り出すには「SvUV(sv)」を使用します。

XSの世界では符号なし整数はU32という型にマッピングされているので、U32型に代入します。U32とunsigned intは互換があります。

# SV*型からunsigned int型へ
void
foo(...)
  PPCODE:
{
  SV* num_sv = ST(0);
  U32 num = SvUV(num_sv);

  XSRETURN(0);
}
unsigned int型からSV*型へ

Perlの世界へ返却するにはunsigned int型をSV*型に変換する必要があります。unsigned int型から新しくSV*型を作成するには、newSVuv(uv)を使用します。値をモータルにするのを忘れないようにしてください。

# unsigned int型からSV*型へ
void
foo(...)
  PPCODE:
{
  U32 num = 5;
  SV* num_sv = sv_2mortal(newSVuv(num));
  XPUSHs(num_sv);
  XSRETURN(1);
}
SV*型からdouble型へ

double型の値は、SV*型のNVという値に含まれています。double型に変換するためには、SV*型に所有されているNVの値を取り出してあげればよいわけです。

SV*型の値に所有されているNVの値を取り出すには「SvNV(sv)」を使用します。

# SV*型からdouble型へ
void
foo(...)
  PPCODE:
{
  SV* num_sv = ST(0);
  double num = SvNV(num_sv);

  XSRETURN(0);
}
double型からSV*型へ

Perlの世界へ返却するにはdouble型をSV*型に変換する必要があります。double型から新しくSV*型を作成するには、newSVnv(nv)を使用します。値をモータルにするのを忘れないようにしてください。

# double型からSV*型へ
void
foo(...)
  PPCODE:
{
  double num = 3.35;
  SV* num_sv = sv_2mortal(newSVnv(num));
  XPUSHs(num_sv);
  XSRETURN(1);
}
SV*型からchar*型へ

char*型の値は、SV*型の値の中のPVという値に含まれています。char*型に変換するためには、SV*型の値に所有されているPVの値を取り出してあげればよいわけです。

SV*型の値に所有されているPVの値を取り出すには「SvPV_nolen(sv)」を使用します。

# SV*型からchar*型へ
void
foo(...)
  PPCODE:
{
  SV* str_sv = ST(0);
  char* str = SvPV_nolen(str_sv);

  XSRETURN(0);
}
char*型からSV*型へ

Perlの世界へ返却するにはchar*型をSV*型に変換する必要があります。char*型から新しくSV*型を作成するには、newSVpvn(pv, length)を使用します。

第二引数に文字列の長さを指定する必要があります。文字列の長さを知るために、strlen関数を使っています。

値をモータルにするのを忘れないようにしてください。

ちなみに、newSVpvというAPIもあるのですが、これは文字列の長さに 0 をあたえるとstrlenがよばれるという機能がついていて、バグの元になりやすいので、newSVpvnをつかうようにするのがお勧めだそうです。(参考)。

# char*型からSV*型へ
void
foo(...)
  PPCODE:
{
  char* str = "Hello";
  SV* str_sv2 = sv_2mortal(newSVpvn(str, strlen(str)));
  XPUSHs(str_sv2);
  XSRETURN(1);
}
SV*型からstd::string型へ
# SV*型からstd::string型へ
void
foo(...)
  PPCODE:
{
  SV* str_sv = ST(0);
  char* str_ch = SvPV_nolen(str_sv);
  std::string str(str_ch);

  XSRETURN(0);
}
std::string型からSV*型へ
# std::string型からSV*型へ
void
foo(...)
  PPCODE:
{
  char* str = "Hello";
  const char* str_ch = str.c_str();
  SV* str_sv = sv_mortal(newSVpvn(str_ch, strlen(str_ch)));
  XPUSHs(str_sv);
  XSRETURN(1);
}

複雑な型の変換

さて、今回はもう少し複雑な型の変換を取り上げたいと思います。たとえば、配列を受け取る関数があったとしたらどうでしょうか。Perlの配列のリファレンスを、C言語の配列に変換する必要がありますね。このような複雑な型の変換について今回は考えてみましょう。

配列のリファレンスをC言語の配列に変換

まず配列のリファレンスを、配列(AV*型)に変換した後に、配列の要素をひとつづつ取得して、C言語の配列に変換します。リファレンスはSV*型です。「SvRV(rv)」でデリファレンスできます。リファレンスではないものをデリファレンスするとセグメンテーションフォールトを起こすので、必ず「SvROK(rv)」で、SV*型の値が、リファレンスであることをチェックします。

void
foo(...)
  PPCODE:
{
  SV* nums_avrv = ST(0);
  AV* nums_av;
  if(SvROK(nums_avrv)) {
    // デリファレンスしてAV*型に変換
    nums_av = (AV*)SvRV(nums_avrv);
  }
  else {
    croak("first argument must be array reference");
  }
  
  //配列の長さ(av_lenは(長さ - 1)が返ってくる)
  size_t nums_len = av_len(nums_av) + 1;
  
  // メモリ割り当て
  I32* nums = (I32*)malloc(sizeof(I32) * nums_len);
  
  // 配列の作成
  for (int i = 0; i < nums_len; i++) {
    // av_fetchで配列の要素を取り出す(SV*型へのポインタが返される)
    SV** num_sv_ptr = av_fetch(nums_av, i, FALSE);
    
    // 要素が見つかればその値を、見つからなければundefを設定
    SV* num_sv = num_sv_ptr ? *num_sv_ptr : &PL_sv_undef;
    
    // I32型に変換
    I32 num = SvIV(num_sv);
    
    // 配列に代入
    nums[i] = num;
    printf("%d\n", num);
  }
  
  // いらなくなったら解放
  free(nums);
  
  XSRETURN(0);
}
C言語の配列を配列のリファレンスに変換

C言語の配列を、Perlの配列のリファレンスに変換します。配列(AV*型)は「newAV()」で作成できます。配列を作成した場合は、モータルにする必要があります。sv_2mortalはSV*型しか受け取れないので、(SV*)とキャストしていることに注意してください。またsv_2mortalはSV*型を返却するので、(AV*)とキャストしていることにも注意してください。

配列に要素を追加するにはav_pushを使用します。I32型の値から新しくSV型の値を作成して、それを配列に追加しています。SV*型の値を配列の要素として追加するときは、手動でリファレンスカウントを増やす必要があります。このためにSvREFCNT_incを使用しています。

newRV_incでリファレンスを生成することができます。newRVはSV*型の引数を受け取るので(SV*)とキャストしています。リファレンスを作成した場合も、sv_2mortalでモータルにする必要があります。

void
foo(...)
  PPCODE:
{
  // C言語の配列
  I32 nums[] = {1, 2, 3};
  
  // 配列の長さ
  size_t nums_len = sizeof nums / sizeof nums[0];
  
  // Perlの配列
  AV* nums_av = (AV*)sv_2mortal((SV*)newAV());
  
  // 配列に追加
  for (int i = 0; i < nums_len; i++) {
    av_push(nums_av, SvREFCNT_inc(sv_2mortal(newSViv(nums[i]))));
  }
  
  // 配列のリファレンスを作成
  SV* nums_avrv = sv_2mortal(newRV_inc((SV*)nums_av));
  
  XPUSHs(nums_avrv);
  XSRETURN(1);
}
ハッシュのリファレンスを構造体に変換

C言語セクションで構造体を宣言します。

typedef struct {
  double x;
  double y;
} Point;

以下はXSセクションに書きます。

void
foo(...)
  PPCODE:
{
  SV* point_hvrv = ST(0);
  HV* point_hv;
  if(SvROK(point_hvrv)) {
    // デリファレンスしてHV*型に変換
    point_hv = (HV*)SvRV(point_hvrv);
  }
  else {
    croak("first argument must be hash reference");
  }
  
  // hv_hetchでxに対応するハッシュの要素を取り出す(SV*型へのポインタが返される)
  SV** x_sv_ptr = hv_fetch(point_hv, "x", strlen("x"), 0);
  
  // 要素が見つかればその値を、見つからなければundefを設定
  SV* x_sv = x_sv_ptr ? *x_sv_ptr : &PL_sv_undef;
  
  // double型に変換
  double x = SvNV(x_sv);
  
  // hv_hetchでyに対応するハッシュの要素を取り出す(SV*型へのポインタが返される)
  SV** y_sv_ptr = hv_fetch(point_hv, "y", strlen("y"), 0);
  
  // 要素が見つかればその値を、見つからなければundefを設定
  SV* y_sv = y_sv_ptr ? *y_sv_ptr : &PL_sv_undef;
  
  // double型に変換
  double y = SvNV(y_sv);  
  
  // 構造体のポインタの作成
  Point* point = (Point*)malloc(sizeof(Point));
  point->x = x;
  point->y = y;
  
  printf("%f\n", point->x);
  printf("%f\n", point->y);
  
  // いらなくなったら解放
  free(point);
  
  XSRETURN(0);
}
構造体をハッシュのリファレンスに変換

構造体をハッシュのリファレンスに変換します。

C言語セクションで構造体を宣言します。

typedef struct {
  double x;
  double y;
} Point;

以下はXSセクションに書きます。ハッシュの作成は「newHV()」で行います。モータルにすることを忘れないでください。ハッシュの要素に追加するときは、SvREFCNT_incでリファレンスカウントをひとつ増やします。

void
foo(...)
  PPCODE:
{
  // 構造体
  Point *point = (Point*)malloc(sizeof(Point));
  point->x = 1;
  point->y = 2;
  
  // 値をSV*型に変換
  SV* x_sv = sv_2mortal(newSVnv(point->x));
  SV* y_sv = sv_2mortal(newSVnv(point->y));
  
  // ハッシュの作成
  HV* point_hv = (HV*)sv_2mortal((SV*)newHV());
  
  // ハッシュに値を追加
  hv_store(point_hv, "x", strlen("x"), SvREFCNT_inc(x_sv), 0);
  hv_store(point_hv, "y", strlen("y"), SvREFCNT_inc(y_sv), 0);
  
  // ハッシュのリファレンスを作成
  SV* point_hvrv = sv_2mortal(newRV_inc((SV*)point_hv));
  
  // 構造体の解放
  free(point);
  
  XPUSHs(point_hvrv);
  XSRETURN(1);
}

複数モジュールを使うXSの簡単な実装方法

XSを使ってモジュールを作ろうとしたときに、複数のモジュールに分割するときにふと困りました。たとえば、SomeModuleとSomeModule::Utilの両方でXSの実装を行いたい場合です。方法を調べていたのですが、実装方法が結構煩雑で難しいなと感じました。

いろいろと試していたのですが、次の方法が簡単なように思えたので紹介しておきます。この方法の利点は、次のとおりです。

  1. Makefile.PLを修正する必要がない
  2. SomeModule.xsというファイルの名前を修正する必要がない
  3. SomeModule.xsという一枚のXSファイルだけでOK
  4. 標準ツールのh2xsでも大丈夫
h2xsでモジュールを作成

最初にh2xsでXS用のモジュールを作成します。

h2xs -A -n SomeModule

こうすると「SomeModule」というディレクトリが作成されます。次のようなファイルとディレクトリが作成されます。

Changes
lib/
Makefile.PL
MANIFEST
ppport.h
README
SomeModule.xs
t/
XSファイルの記述

XSファイルを作成していきます。「foo」と表示するfooという関数をSomeModuleに追加、「bar」と表示するbarという関数をSomeModule::Utilに追加してみます。

まず最初にXSファイルを開くと次のようになっていると思います。

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include "ppport.h"


MODULE = SomeModule		PACKAGE = SomeModule		

まず覚えてほしいことは、「MODULE = SomeModule PACKAGE = SomeModule」のセクションは一番下側に置く必要があるということです。この部分を最後においておかないとXSのロードに失敗してしまいます。

では記述しましょう。「MODULE = SomeModule::Util PACKAGE = SomeModule::Util」を追加します。そしてXSでbarを定義します。また「MODULE = SomeModule PACKAGE = SomeModule」のセクションに、fooを定義します。

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include "ppport.h"

MODULE = SomeModule::Util PACKAGE = SomeModule::Util

void
bar(...)
  PPCODE:
{
  PerlIO_printf(PerlIO_stdout(), "bar\n");
  XSRETURN(0);
}

MODULE = SomeModule		PACKAGE = SomeModule		

void
foo(...)
  PPCODE:
{
  PerlIO_printf(PerlIO_stdout(), "foo\n");
  XSRETURN(0);
}	
モジュールの作成

ここまでできれば、SomeModuleとSomeModule::Utilを作成するだけです。

SomeModuleのソースコード

SomeModule.pmのソースコードは最初のままでもかまいません。少し整理して以下のようにしました。

package SomeModule;

use strict;
use warnings;

our $VERSION = '0.01';

require XSLoader;
XSLoader::load('SomeModule', $VERSION);

1;

SomeModule::Utilのソースコード

SomeModule::Util::barも作ってみましょう。これは「lib/SomeModule/Util.pm」に保存してください。これはパッケージ名だけが宣言されたほとんど空のファイルです。

package SomeModule::Util;

1;
テストスクリプト

テストスクリプトを作成します。これは、XSファイルがあるディレクトリと同じディレクトリにおいてください。

use strict;
use warnings;

use SomeModule;
use SomeModule::Util;

SomeModule::foo();
SomeModule::Util::bar();
コンパイルして実行

コンパイルして実行してみましょう。

perl Makefile.PL
make
perl -Mblib test.pl

次のように出力されれば成功です。

foo
bar

XSを複数モジュールで記述するのは、それほど難しくなさそうですね。

C言語のライブラリをXSファイルから読み込む方法

C言語のライブラリをXSファイルから読み込む方法を解説します。

h2xsでモジュールを作成

最初にh2xsでXS用のモジュールを作成します。

h2xs -A -n SomeModule

こうすると「SomeModule」というディレクトリが作成されます。次のようなファイルとディレクトリが作成されます。

Changes
lib/
Makefile.PL
MANIFEST
ppport.h
README
SomeModule.xs
t/
C言語のライブラリの作成

まずC言語のライブラリを作成しましょう。ヘッダファイルとソースファイルを作成します。

mylib.h

ヘッダファイルです。mylib_printを宣言しています。

void mylib_print();

mylib.c

ソースファイルです。mylib_printは「mylib」という文字列を出力する関数です。

#include <stdio.h>

void mylib_print() {
  printf("mylib\n");
}

ヘッダファイルとソースファイルは保存して、XSファイルが存在するディレクトリと同じディレクトリに配置してください。同じディレクトリにおいておくのが、後の設定が少なくて観点です。

XSファイルの記述

XSファイルを記述しましょう。mylib_printを呼び出しています。

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include "ppport.h"
#include "mylib.h"

MODULE = SomeModule		PACKAGE = SomeModule		

void
foo(...)
  PPCODE:
{
  mylib_print();
  XSRETURN(0);
}
Makefile.PLの修正

次にMakefile.PLを少し修正しましょう。一番下の「OBJECT」オプションがデフォルトではコメントアウトされているので、コメントを取り除きます。「$(O_FILES)」という設定をすれば、カレントディレクトリのすべてのC言語ソースファイルがコンパイルの対象になります。

use ExtUtils::MakeMaker;

WriteMakefile(
    NAME              => 'SomeModule',
    VERSION_FROM      => 'lib/SomeModule.pm', # finds $VERSION
    PREREQ_PM         => {}, # e.g., Module::Name => 1.1
    ($] >= 5.005 ?     ## Add these new keywords supported since 5.005
      (ABSTRACT_FROM  => 'lib/SomeModule.pm', # retrieve abstract from module
       AUTHOR         => 'A. U. Thor <kimoto@sakura.ne.jp>') : ()),
    LIBS              => [''], # e.g., '-lm'
    DEFINE            => '', # e.g., '-DHAVE_SOMETHING'
    INC               => '-I.', # e.g., '-I. -I/usr/include/other'
    OBJECT            => '$(O_FILES)', # link all the C files too
);
テストスクリプト

テストスクリプトを作成します。これは、XSファイルがあるディレクトリと同じディレクトリにおいてください。

use strict;
use warnings;

use SomeModule;

SomeModule::foo();
コンパイルして実行

コンパイルして実行してみましょう。

perl Makefile.PL
make
perl -Mblib test.pl

次のように出力されれば成功です。

mylib

これで、C言語のライブラリをXSファイルから読み込むことができるようになりました。

C言語の構造体をPerlのオブジェクトとして扱う方法

XSでは、構造体自体をPerlのオブジェクトとして扱うこともできます。C言語の構造体をPerlのオブジェクトとして扱う方法を解説します。

h2xsでモジュールを作成

最初にh2xsでXS用のモジュールを作成します。

h2xs -A -n SomeModule

こうすると「SomeModule」というディレクトリが作成されます。次のようなファイルとディレクトリが作成されます。

Changes
lib/
Makefile.PL
MANIFEST
ppport.h
README
SomeModule.xs
t/
XSファイルの記述

XSファイルを記述しましょう。構造体のポインタをPTR2INTでsize_t型に変換しています。さらに、size_t型をSV*型に変換し、SV*型をSV*型へのリファレンスに変換し、最後にblessして、オブジェクトに変換しています。

size_t型というのは整数型ですが、アドレスの値はsize_t型で受け取るようにします。

取り出すときは、デリファレンスを行い、SV*に含まれるIVの値を取り出し、INT2PTRで構造体へのポインタに変換しています。

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include "ppport.h"

typedef struct {
  double x;
  double y;
} Point;

MODULE = Point		PACKAGE = Point		

void
new(...)
  PPCODE:
{
  // クラス名
  char* class_name = SvPV_nolen(ST(0));
  
  // xとy
  double x = SvNV(ST(1));
  double y = SvNV(ST(2));
  
  // 構造体の作成(ポインタとして作成)
  Point* point = (Point*)malloc(sizeof(Point));
  point->x = x;
  point->y = y;
  
  // ポインタをsize_t型に変換
  size_t point_iv = PTR2IV(point);
  
  // size_t型をSV*型に変換
  SV* point_sv = sv_2mortal(newSViv(point_iv));
  
  // SV*型のリファレンスを作成
  SV* point_svrv = sv_2mortal(newRV_inc(point_sv));
  
  // オブジェクトを作成
  SV* point_obj = sv_bless(point_svrv, gv_stashpv(class_name, 1));
  
  XPUSHs(point_obj);
  XSRETURN(1);
}

void
x(...)
  PPCODE:
{
  // オブジェクトを取得
  SV* point_obj = ST(0);
  
  // デリファレンス
  SV* point_sv = SvROK(point_obj) ? SvRV(point_obj) : point_obj;
  
  // SV*型をsize_t型に変換
  size_t point_iv = SvIV(point_sv);
  
  // size_t型をポインタに変換
  Point* point = INT2PTR(Point*, point_iv);
  
  // xを取得
  double x = point->x;
  
  // xをSV*型に変換
  SV* x_sv = sv_2mortal(newSVnv(x));
  
  XPUSHs(x_sv);
  XSRETURN(1);
}

void
y(...)
  PPCODE:
{
  // オブジェクトを取得
  SV* point_obj = ST(0);
  
  // デリファレンス
  SV* point_sv = SvROK(point_obj) ? SvRV(point_obj) : point_obj;
  
  // SV*型をsize_t型に変換
  size_t point_iv = SvIV(point_sv);
  
  // size_t型をポインタに変換
  Point* point = INT2PTR(Point*, point_iv);
  
  // xを取得
  double y = point->y;
  
  // xをSV*型に変換
  SV* y_sv = sv_2mortal(newSVnv(y));
  
  XPUSHs(y_sv);
  XSRETURN(1);
}

void
DESTORY(...)
  PPCODE:
{
  // オブジェクトを取得
  SV* point_obj = ST(0);
  
  // デリファレンス
  SV* point_sv = SvROK(point_obj) ? SvRV(point_obj) : point_obj;
  
  // SV*型をsize_t型に変換
  size_t point_iv = SvIV(point_sv);
  
  // size_t型をポインタに変換
  Point* point = INT2PTR(Point*, point_iv);
  
  // Point*を解放
  free(point);
  
  XSRETURN(0);
}

MODULE = SomeModule		PACKAGE = SomeModule		
Pointモジュールの作成

Point.pmというファイルをlib以下においてください。SomeModuleを読み込んでいるのは、SomeModuleにバインディングの記述があるためにです。

package Point;
use SomeModule;

1;
テストスクリプト

テストスクリプトを作成します。これは、XSファイルがあるディレクトリと同じディレクトリにおいてください。

use strict;
use warnings;
use Point;

my $point = Point->new(1, 2);
print $point->x . "\n";
print $point->y . "\n";
コンパイルして実行

コンパイルして実行してみましょう。

perl Makefile.PL
make
perl -Mblib test.pl

次のように出力されれば成功です。

1
2

C++のライブラリをXSから利用する方法

PerlのXSではC++で書かれたライブラリを呼び出すこともできます。コンパイラとリンカに、C++用の「g++」を指定するだけです。

h2xsでモジュールを作成

最初にh2xsでXS用のモジュールを作成します。

h2xs -A -n SomeModule

こうすると「SomeModule」というディレクトリが作成されます。次のようなファイルとディレクトリが作成されます。

Changes
lib/
Makefile.PL
MANIFEST
ppport.h
README
SomeModule.xs
t/
C++のライブラリの作成

C++のライブラリを作成しましょう。ヘッダファイルとソースファイルを作成します。

mylib.h

ヘッダファイルです。mylib_printを宣言しています。

void mylib_print();

mylib.cpp

C++のソースファイルです。mylib_printは「mylib_cpp」という文字列を出力する関数です。ファイル名の拡張子が「.cpp」であることに注意してください。

#include <iostream>

void mylib_print() {
  std::cout << "mylib_cpp\n";
}

ヘッダファイルとソースファイルは保存して、XSファイルが存在するディレクトリと同じディレクトリに配置してください。

XSファイルの記述

XSファイルを記述しましょう。mylib_printを呼び出しています。

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include "ppport.h"
#include "mylib.h"

MODULE = SomeModule		PACKAGE = SomeModule		

void
foo(...)
  PPCODE:
{
  mylib_print();
  XSRETURN(0);
}
Makefile.PLの修正

次にMakefile.PLを少し修正しましょう。一番下の「OBJECT」オプションがデフォルトではコメントアウトされているので、コメントを取り除きます。「$(O_FILES)」という設定をすれば、カレントディレクトリのすべてのC言語のソースファイルとC++のソースファイルがコンパイルの対象になります。

そしてコンパイラとリンカを「g++」に変更します。コンパイラは「CC」オプション、リンカは「LD」で設定できます。

use ExtUtils::MakeMaker;
use strict;
use warnings;

WriteMakefile(
    NAME              => 'SomeModule',
    VERSION_FROM      => 'lib/SomeModule.pm', # finds $VERSION
    PREREQ_PM         => {}, # e.g., Module::Name => 1.1
    ($] >= 5.005 ?     ## Add these new keywords supported since 5.005
      (ABSTRACT_FROM  => 'lib/SomeModule.pm', # retrieve abstract from module
       AUTHOR         => 'A. U. Thor <kimoto@sakura.ne.jp>') : ()),
    LIBS              => [''], # e.g., '-lm'
    DEFINE            => '', # e.g., '-DHAVE_SOMETHING'
    INC               => '-I.', # e.g., '-I. -I/usr/include/other'
    OBJECT            => '$(O_FILES)', # link all the C files too
    CC =>'g++',
    LD => 'g++',
);
テストスクリプト

テストスクリプトを作成します。これは、XSファイルがあるディレクトリと同じディレクトリにおいてください。

use strict;
use warnings;

use SomeModule;

SomeModule::foo();
コンパイルして実行

コンパイルして実行してみましょう。

perl Makefile.PL
make
perl -Mblib test.pl

次のように出力されれば成功です。

mylib_cpp

これで、C++のライブラリをXSファイルから利用することができるようになりました。

XSファイルの中に直接C++を記述することはできますか。

できます。けれども、Perlのシンボルと衝突してしまうなどの不具合が起こりがちですので、ソースファイルとヘッダファイルに記述するのがよいと思います。

C言語のライブラリとC++のライブラリを混在させて利用することはできますか。

C++はC言語のほぼ上位互換なので、C言語のライブラリがg++でコンパイル可能であれば大丈夫だと思います。そうでない場合は、ちょっとわかりません。

C++のクラスをXSから利用する方法

XSでC++のクラスを呼び出す方法を学びましょう。これができるようになれば、どんなC++のライブラリでも、Perlにバインディングができるようになると思います。

h2xsでモジュールを作成

最初にh2xsでXS用のモジュールを作成します。

h2xs -A -n MyClass

こうすると「MyClass」というディレクトリが作成されます。次のようなファイルとディレクトリが作成されます。

Changes
lib/
Makefile.PL
MANIFEST
ppport.h
README
MyClass.xs
t/
C++でクラスの作成

C++でクラスを作成しましょう。ヘッダファイルとソースファイルを作成します。

MyClass.h

ヘッダファイルです。コンストラクタ、メソッド、クラスメソッドを宣言しています。通常のメソッドとクラスメソッドの呼び出し方の違いを理解するために、このような構成にしています。ファイル名は「MyClass.h」です。

#ifndef MYCLASS_INCLUDE
#define MYCLASS_INCLUDE
class MyClass {
  
  public:
  
  // コンストラクタ
  MyClass();
  
  // メソッド
  void print();
  
  // クラスメソッド
  static void print_static();
};
#endif

「#ifndef」で始まっているのは、インクルードガードと呼ばれるものです。ヘッダファイルは、XSファイルと、ソースファイルの2箇所から読み込まれる必要があります。すると、そのままでは、ヘッダファイルが二重に取り込まれて、コンパイルエラーになってしまいます。それを防ぐために、インクルードを行っています。

MyClass_src.cpp

C++のソースファイルです。コンストラクタ、メソッド、クラスメソッドの実装を行っています。ソースファイルでは、MyClassというシンボルを解決するために、ヘッダファイルを読み込む必要があります。ファイル名は「MyClass_src.cpp」にしてあります。これは「MyClass.cpp」としてしまうと、コンパイル後の名前は「MyClass.o」となりますが、これはXSファイル「MyClass.xs」のコンパイル後の名前「MyClass.o」とかぶってしまうためです。

#include <iostream>
#include "MyClass.h"

// コンストラクタ
MyClass::MyClass() {}

// メソッド
void MyClass::print() {
  std::cout << "MyClass::print\n";
}

// クラスメソッド
void MyClass::print_static() {
  std::cout << "MyClass::print_static\n";
}

ヘッダファイルとソースファイルは保存して、XSファイルが存在するディレクトリと同じディレクトリに配置してください。

XSファイルの記述

XSファイルを記述しましょう。

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include "ppport.h"
#include "MyClass.h"

#define XS_OBJ_TO_PTR(x, type)     (INT2PTR(type, SvROK(x) ? SvIV(SvRV(x)) : SvIV(x)))
#define XS_PTR_TO_OBJ(x, class) \
  sv_bless( \
    sv_2mortal( \
      newRV_inc( \
        sv_2mortal( \
          newSViv(PTR2IV(x)) \
        ) \
      ) \
    ), \
    gv_stashpv(class, 1) \
  );

MODULE = MyClass		PACKAGE = MyClass		

void
new(...)
  PPCODE:
{
  MyClass* self = new MyClass();
  SV* self_sv = XS_PTR_TO_OBJ(self, "MyClass");
  
  XPUSHs(self_sv);
  XSRETURN(1);
}

void
print(...)
  PPCODE:
{
  MyClass* self = XS_OBJ_TO_PTR(ST(0), MyClass*);
  
  self->print();
  XSRETURN(0);
}

void
print_static(...)
  PPCODE:
{
  MyClass::print_static();
  XSRETURN(0);
}

void DESTORY(...)
  PPCODE:
{
  MyClass* self = XS_OBJ_TO_PTR(ST(0), MyClass*);
  delete self;
}

型の変換を行うためのマクロ

少し解説をしておきます。以下は、Perlのオブジェクト(SV*型)をC(あるいはC++)のポインタに変換するマクロと、C(あるいはC++)のポインタをPerlのオブジェクト(SV*型)に変換するマクロになっています。

#define XS_OBJ_TO_PTR(x, type)     (INT2PTR(type, SvROK(x) ? SvIV(SvRV(x)) : SvIV(x)))
#define XS_PTR_TO_OBJ(x, class) \
  sv_bless( \
    sv_2mortal( \
      newRV_inc( \
        sv_2mortal( \
          newSViv(PTR2IV(x)) \
        ) \
      ) \
    ), \
    gv_stashpv(class, 1) \
  );

コンストラクタ

コンストラクタです。newを使ってMyClassをインスタンス化してMyClass*型に代入します。そして、このポンイタを、Perlのオブジェクト変換して、返します。

void
new(...)
  PPCODE:
{
  MyClass* self = new MyClass();
  SV* self_sv = XS_PTR_TO_OBJ(self, "MyClass");
  
  XPUSHs(self_sv);
  XSRETURN(1);
}

メソッド呼び出し

メソッドの呼び出しです。

void
print(...)
  PPCODE:
{
  MyClass* self = XS_OBJ_TO_PTR(ST(0), MyClass*);
  
  self->print();
  XSRETURN(0);
}

メソッド呼び出しでは、第一引数にはPerlのオブジェクトが渡ってくるので、これをポインタ「MyClass*」に変換します。そして「self->print」としてメソッドを呼び出します。

クラスメソッドの呼び出し

クラスメソッドの呼び出しです。

void
print_static(...)
  PPCODE:
{
  MyClass::print_static();
  XSRETURN(0);
}

「MyClass::print_static()」と完全修飾名で呼び出しているだけです。

デストラクタ

デストラクタです。deleteを使って、メモリの開放を行う必要があります。

void DESTORY(...)
  PPCODE:
{
  MyClass* self = XS_OBJ_TO_PTR(ST(0), MyClass*);
  delete self;
}

Makefile.PLの修正

次にMakefile.PLを少し修正しましょう。一番下の「OBJECT」オプションがデフォルトではコメントアウトされているので、コメントを取り除きます。「$(O_FILES)」という設定をすれば、カレントディレクトリのすべてのC言語のソースファイルとC++のソースファイルがコンパイルの対象になります。

そしてコンパイラとリンカを「g++」に変更します。コンパイラは「CC」オプション、リンカは「LD」で設定できます。

use ExtUtils::MakeMaker;
use strict;
use warnings;

WriteMakefile(
    NAME              => 'MyClass',
    VERSION_FROM      => 'lib/MyClass.pm', # finds $VERSION
    PREREQ_PM         => {}, # e.g., Module::Name => 1.1
    ($] >= 5.005 ?     ## Add these new keywords supported since 5.005
      (ABSTRACT_FROM  => 'lib/MyClass.pm', # retrieve abstract from module
       AUTHOR         => 'A. U. Thor <kimoto@sakura.ne.jp>') : ()),
    LIBS              => [''], # e.g., '-lm'
    DEFINE            => '', # e.g., '-DHAVE_SOMETHING'
    INC               => '-I.', # e.g., '-I. -I/usr/include/other'
    OBJECT            => '$(O_FILES)', # link all the C files too
    CC =>'g++',
    LD => 'g++',
);
テストスクリプト

テストスクリプトを作成します。これは、XSファイルがあるディレクトリと同じディレクトリにおいてください。

use strict;
use warnings;

use MyClass;

my $obj = MyClass->new;
$obj->print;
MyClass->print_static;
コンパイルして実行

コンパイルして実行してみましょう。

perl Makefile.PL
make
perl -Mblib test.pl

次のように出力されれば成功です。

MyClass::print
MyClass::print_static

これで、C++のクラスをXSファイルから利用することができるようになりました。

Perl API

PerlはC言語で実装されていますが、Perlの機能をC言語から呼び出すためのAPIがたくさん用意されています。

Makefile.PL関連

Makefileを生成するために、Perlでは、Makefile.PLというファイルを作って、ExtUtil::MakeMakerというモジュールを使用します。このExtUtil::MakeMakerというモジュールを便利に使用するための記事を掲載します。

カエルぽんカエルぽん 2012/11/08 07:37 上記の通り実行した所、
Windows環境では思うようなフィードバックが
得られません。WindowでXSファイルを
ビルドし操作しやすいマクロを構築するにはどういった
手順が適切なのでしょうか?
XSファイルの基本と応用は初心には敷居が高いと
アキラめてしまった方がよいのでしょうか

perlcodesampleperlcodesample 2012/11/08 14:12 カエルぽんさん

 Windows環境はC言語をコンパイルするツール(linuxでのgcc)などがデフォルトでは含まれていません。MinGWとdmakeというツールがあれば、コンパイルできるかもしれません。

 またcygwinというツールを使えば、Windows上にUnixに似た環境を構築できるので、上記を実行することができると思いますので、試してみるのはいかがでしょうか。

投稿したコメントは管理者が承認するまで公開されません。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証