実例で学ぶPHP拡張モジュールの作り方

第4回WEBカメラから画像をキャプチャ(その2)

今回は前回製作したcvcaptureモジュールを改良し、リソースを使った継続的なキャプチャを実装します。

追加する機能は、CvCapture構造体を保持するリソース⁠cvcapture⁠を定義し、カメラからcvcaptureリソースを作成する関数cv_create_camera_capture()、ファイルからcvcaprureリソースを作成する関数cv_create_file_capture()、フレームを取得し、静止画として保存する関数cv_save_capture()です。

specファイルを記述する

前回のspecファイルをベースに、リソース定義と新しい関数の定義を追加します。

リソースを定義する

まずはリソースです。リソースは<resources>タグ内の<resource>タグで記述します。<resoure>タグではname属性でリソース名、payload属性でリソースに保持する型を、allocで保持する値(payload属性で指定した型のポインタ)がZend APIのメモリを確保する関数emalloc()で確保された領域のアドレスかどうかを指定します。

また、<resource>タグ内の<destruct>タグでリソースに保持されている値を破棄する処理を記述します。ここで使っている⁠resoure⁠はCodeGen_PECLによって自動で定義される変数で、型はpayload属性で指定した型のポインタ(この場合は⁠CvCapture *⁠⁠)です。

リスト1 リソース定義(cvcapture-0.2.0.xml)

<resources>
  <resource name="cvcapture" payload="CvCapture" alloc="no">
    <description><?data
Storage for CvCapture.
    ?></description>
    <destruct><?data
cvReleaseCapture(&resource);
    ?></destruct>
  </resource>
</resources>

関数を定義する

次に、関数を定義します。今回作成する関数は、前回作成した関数をリソースを作成する関数と静止画を保存する関数に分割したものとなります。

図1 前回作成した関数との関係
図1 前回作成した関数との関係

リソースを作成する関数cv_create_camera_capture()とcv_create_file_capture()は、それぞれカメラを指定する数値、動画ファイルのパスを引数とします。リソースを返す関数は戻り値の型を⁠resource リソースの種類と指定します。今回はcvcaptureリソースを返すので、⁠resource cvcapture⁠です。テストでは戻り値がcvcaptureリソースかどうかを調べています。

リスト2 関数定義1(cvcapture-0.2.0.xml)
<function name="cv_create_camera_capture">
  <proto>resource cvcapture cv_create_camera_capture([int index])</proto>
  <summary>Start capturing frames from camera.</summary>
  <description>(省略)</description>
   <test>
    <code><?data
var_dump(cv_create_camera_capture());
    ?></code>
    <result mode="format"><?data
resource(%d) of type (cvcapture)
    ?></result>
  </test>
</function>

<function name="cv_create_file_capture">
  <proto>resource cvcapture cv_create_file_capture(string filename)</proto>
  <summary>Start capturing frames from video file.</summary>
  <description>(省略)</description>
  <test>
    <skipif><?data
if (!file_exists("sample.3g2")) {
  die("skip sample video file does not exist");
}
    ?></skipif>
    <code><?data
var_dump(cv_create_file_capture("sample.3g2"));
  ?></code>
  <result mode="format"><?data
resource(%d) of type (cvcapture)
    ?></result>
  </test>
</function>

cv_save_capture()は作成したcvcaptureリソースを第1引数とし、第2引数で保存先のパス、第3引数が指定されていればキャプチャした画像の高さのペアの配列を代入します。戻り値はキャプチャに成功すればtrue、失敗した場合はfalseです。

リスト3 関数定義2(cvcapture-0.2.0.xml)
<function name="cv_save_capture">
 <proto>bool cv_save_capture(resource cvcapture capture, string filename[, array &amp;size])</proto>
  <summary>Capture a frame.</summary>
  <description>(省略)</description>
   <test>
	  <code><?data
$capture = cv_create_camera_capture();
if ($capture && cv_save_capture($capture, "test_save.jpg", $size)) {
    print_r($size);
}
    ?></code>
    <result mode="format"><?data
Array
(
  [0] => %d
  [1] => %d
)
    ?></result>
  </test>
</function>

ソースコードを生成する

ここまでできたら、前回同様にpecl-genコマンドでモジュールのソースコードを生成します。

操作1 pecl-genを実行
$ pecl-gen --dir=cvcapture-0.2.0 cvcapture-0.2.0.xml
Creating 'cvcapture' extension in './cvcapture-0.2.0'

Your extension has been created in directory ./cvcapture.
See ./cvcapture-0.2.0/README and/or ./cvcapture-0.2.0/INSTALL for further instructions.

ソースコード解説

生成されたソースコードのうち、リソース定義に関わる箇所の解説をします。

php_cvcapture.hではリスト4のようなリソースを開放するマクロが定義されています。Z_LVAL_Pは変数の整数としての値(long型)にアクセスするマクロです。リソースの実体はハッシュテーブルEG(regular_list)に登録されており、変数にはそのキーが整数として格納されているのです。

リスト4 リソースを開放するマクロ(php_cvcapture.h)
#define FREE_RESOURCE(resource) zend_list_delete(Z_LVAL_P(resource))

CodeGen_PECLが生成するコードではZ_LVAL_Pを使っていますが、php/Zend/zend_operators.hではリソースIDにアクセスするマクロZ_RESVAL_Pも定義されています。今のところ両者は同じもので、実装方法からして仕様が変わる可能性も極めて低いと考えられますが、それでもZ_RESVAL_Pを使った方が無難だと思います。もっとも、リソースは通常の変数と同様に、unset()やリソースが作成されたスコープを抜けて不要になった時点でも破棄されるので、cvcaptureモジュールではリソースを破棄する関数を用意しておらず、このマクロも使っていません。

次にcvcapture.cの最初のあたりを見てください。リスト5のようなコードが生成されています。

リスト5 リソース識別番号とデストラクタ(cvcapture.c)
/* {{{ Resource destructors */
int le_cvcapture; /* リソース識別番号 */
void cvcapture_dtor(zend_rsrc_list_entry *rsrc TSRMLS_DC)
{
  /* “CvCapture”は<resource>タグのname属性値 */
  CvCapture * resource = (CvCapture *)(rsrc->ptr);

  do {
    /* ここがspecファイルの<destruct>タグに記述したコード */
    cvReleaseCapture(&resource);
  } while (0);
}

/* }}} */

cvcapture_dtor()はリソースを格納した変数が不要になったときに呼ばれるコールバック関数です。<resource>タグのalloc属性がyesの場合は、do~whileブロックの後にefree()で開放するコードも追加されます。

le_cvcaptureはリソースの種類を識別するのに使われるグローバル変数で、モジュール初期化関数内で初期化されます。このようにPHPではいくつかの重要な値をグローバル変数として持っているため、CLIを除くZTS(マルチスレッド)モードのSAPIではdl()関数が使えないのです。

リスト6 モジュール初期化関数(cvcapture.c)
/* {{{ PHP_MINIT_FUNCTION */
PHP_MINIT_FUNCTION(cvcapture)
{
  /* (定数定義は省略) */
  le_cvcapture = zend_register_list_destructors_ex(cvcapture_dtor,
                       NULL, "cvcapture", module_number);

  /* add your stuff here */

  return SUCCESS;
}
/* }}} */

関数を実装する

ここからはcvcapture.cに関数を実装していきます。前回作成した関数に変更はありませんので、そのままコピー&ペーストしてください。

リソースを返す関数

まずはcv_create_camera_capture()から実装していきましょう。リスト7はpecl-gen直後の状態です。

リスト7 cv_create_camera_capture()関数(cvcapture.c)
/* {{{ proto resource cvcapture cv_create_camera_capture([int index])
  Start capturing frames from camera. */
PHP_FUNCTION(cv_create_camera_capture)
{
  /* 戻り値のリソースを代入する変数の宣言 */
  /* 変数名は固定。型はプロトタイプが“resource cvcapture”、リソース定義が
     <resource name="cvcapture" payload="CvCapture"/>なら、“CvCapture *” */
  CvCapture * return_res;
  long return_res_id = -1;

  /* 引数が代入される変数の宣言 */
  long index = 0;

  /* 引数をパース */
  if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|l", &index) == FAILURE) {
    return;
  }

  /* 実装されていない旨のエラーを出力 */
  php_error(E_WARNING, "cv_create_camera_capture: not yet implemented"); RETURN_FALSE;

  /* 戻り値を実体のアドレスがreturn_res、タイプがle_cvcaptureのリソースにする */
  ZEND_REGISTER_RESOURCE(return_value, return_res, le_cvcapture);
}
/* }}} cv_create_camera_capture */

戻り値をリソースにするコードは生成されているので、あとは未実装エラーを出力している箇所をcv_camera_capture()と同じキャプチャを作成する処理で置き換えれば実装完了です。cv_camera_capture()の該当箇所との違いは変数名が⁠capture⁠から自動生成された変数名⁠return_res⁠になっている点だけです。

リスト8 キャプチャを作成
return_res = cvCreateCameraCapture((int)index);
if (return_res == NULL) {
  php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot create camera capture");
  efree(fullpath);
  RETURN_FALSE;
}

cv_create_file_capture()の実装も、自動生成されたコードの未実装エラーを出力している箇所をcv_file_capture()のキャプチャを作成する処理で置き換えるだけなので、省略します。

リソースを受け取る関数

次に、cv_save_capture()を実装します。リスト9はpecl-gen直後の状態です。

リスト9 cv_save_capture()関数(cvcapture.c)
/* {{{ proto bool cv_save_capture(resource cvcapture capture, string filename[, array &mixed])
  Capture a frame. */
PHP_FUNCTION(cv_save_capture)
{
  /* リソース関連変数の宣言 */
  zval * capture = NULL;
  int capture_id = -1;
  CvCapture * res_capture;

  /* その他の変数の宣言 */
  const char * filename = NULL;
  int filename_len = 0;
  zval * size = NULL;

  /* 引数をパース */
  if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "rs|z", &capture, &filename, &filename_len, &size) == FAILURE) {
    return;
  }
  /* リソースの実体を取得 */
  ZEND_FETCH_RESOURCE(res_capture, CvCapture *, &capture, capture_id, "cvcapture", le_cvcapture);

  /* 実装されていない旨のエラーを出力 */
  php_error(E_WARNING, "cv_save_capture: not yet implemented"); RETURN_FALSE;

  RETURN_FALSE;
}
/* }}} cv_save_capture */

引数をリソースをして取得する場合、指示子は⁠r⁠で、対応する型は⁠zval *⁠です。このままではキャプチャ構造体にアクセスできないので、続いてマクロZEND_FETCH_RESOURCE()でリソースの実体を取得し、⁠CvCapture *⁠型にキャストしています。

もし引数がリソースでない場合はzend_parse_parameter()でエラー、リソースだけどタイプがle_cvcaptureでない場合はZEND_FETCH_RESOURCE()でエラーとなり、falseを返します。capture_idの値を-1以外にすると、引数の代わりにcapture_idと同じIDのリソースを取得できますが、ここでは-1のままにしておいてください。文字列の"cvcapture"は、エラーメッセージに表示されるリソースタイプ名です。

この関数の実装もcv_camera_capture()関数から流用できます。先頭に変数の宣言リスト10を追加し、引数のパースの後にリスト11の内容を加えれば完了です。

リスト10 追加の変数宣言
char *fullpath;     /* 保存先のフルパス */
IplImage *image;    /* イメージ構造体 */
リスト11 キャプチャした画像を保存
/* パスのチェック */
if (strlen(filename) != filename_len ||
  (fullpath = expand_filepath(filename, NULL TSRMLS_CC)) == NULL)
{
  php_error_docref(NULL TSRMLS_CC, E_WARNING, "Wrong filename given");
  RETURN_FALSE;
}

if (php_check_open_basedir(fullpath TSRMLS_CC) ||
  (PG(safe_mode) && !php_checkuid(fullpath, NULL, CHECKUID_CHECK_FILE_AND_DIR)))
{
  efree(fullpath);
  RETURN_FALSE;
}

/* フレームを取得・保存 */
image = cvRetrieveFrame(res_capture);
if (image == NULL) {
  php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot retrieve image");
  efree(fullpath);
  RETURN_FALSE;
}

if (!cvSaveImage(fullpath, image)) {
  php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot save image to '%s'", fullpath);
  efree(fullpath);
  RETURN_FALSE;
}

/* 画像サイズを代入 */
if (size != NULL) {
  CvSize imgsize = cvGetSize(image);
  zval_dtor(size);
  array_init(size);
  add_next_index_long(size, (long)imgsize.width);
  add_next_index_long(size, (long)imgsize.height);
}

efree(fullpath);
RETURN_TRUE;

インストール

実装が終わったら、お決まりの手順でインストールします。テストも忘れずにしておきましょう。

操作2 ビルド&テスト&インストール
$ phpize
$ ./configure --enable-cvcapture
$ make
$ make test
$ sudo make install

実際に使ってみる

リスト12は前回のサンプルをリソースを使うように書き直したものです。

リスト12 サンプルスクリプト(cvcapture-sample2.php)
<?php
extension_loaded('cvcapture') || dl('cvcapture.so');

$capture = cv_create_camera_capture();
if ($capture) {
    for ($i = 1; $i 

今回の目的の一つは、リソースを使って毎回カメラに接続するオーバーヘッドを節約することでしたので、サンプルに少し手を加えてキャプチャにかかる時間を計ってみました。

表1 リソースを使わないときと使ったときの比較
リソースを使わないときリソースを使ったとき
初期化-0.312秒
1回目1.085秒0.548秒
2回目0.949秒0.040秒
3回目0.564秒0.039秒
4回目0.565秒0.038秒
5回目0.565秒0.039秒
6回目0.565秒0.039秒
7回目0.564秒0.038秒
8回目0.568秒0.038秒
9回目0.570秒0.037秒
10回目0.565秒0.041秒
合計6.566秒1.214秒

どうやら期待通りの結果が得られたようです。このようにリソースを使えばPHPの組み込み型でない構造体を使い回すことができます。

おわりに

今回はリソースとリソースを扱う関数を定義しました。しかし、今どきのPHPプログラミングでリソースをそのまま使うのは少々時代遅れと言えます。そこで、次回はこれをラップして、リソースをプロパティとして持つクラスを定義し、拡張モジュールでオブジェクト指向のAPIを実装する方法を紹介します。

サンプルファイルのダウンロード

付録は後日掲載

おすすめ記事

記事・ニュース一覧