C言語の関数を取り込む(import)その2

gnuplot 5.0以降では、C言語で書かれた関数をgnuplotで取り込んで使うための非常に強力なコマンド「import」が使えるようになりました。ここでは、そのimportコマンドの使い方について詳細に解説します。

importコマンドの使い方

例として、その1で作成したdllファイルdemo_plugin.dll(Linuxなどでは.soファイル)が、スクリプトファイルと同じフォルダにある場合を考えます。このdllファイル内では、いくつかの関数が定義されていますが、例えばその中のnsinc関数をgnuplotで利用できるようにするには、以下のようにします。

import nsinc(n,x) from "demo_plugin"

もし、gnuplot上で使いたい名前と、dllファイル中で使いたい名前が異なる場合は、以下のようにします。以下では、my_func(n,x)という名前でnsinc関数を使うことになります。

import my_func(n,x) from "demo_plugin:nsinc"

これらのコマンドでエラーが出なければ、これでnsinc(n,x)(またはmy_func(n,x))という関数を、あたかもgnuplot上で定義された関数であるかのように使うことができます。例えば、以下のスクリプトで、nsinc(n,x)をプロットできます。

import nsinc(n,x) from "demo_plugin"
plot nsinc(1,x), nsinc(2,x), nsinc(3,x), nsinc(4,x)

なお、その1や1.5でも書きましたが、gnuplotのbit数と、用いたコンパイラのbit数が整合しない場合は、

warning: dynamic library error
failed to load external function

という2つのエラーが出て、スクリプトの実行が止まってしまいます。

Cプログラムの書き方

さて、いよいよ、gnuplotで読み込めるc言語プログラムを書いてみることにしましょう。参考にするために「demo_plugin.c」の中身を見てみると、関数定義の部分は以下のようになっていることがわかります。

DLLEXPORT struct value sinc(int nargs, struct value *arg, void *p)
{
  double x = RVAL(arg[0]);
  struct value r;
  r.type = CMPLX;

  /* Enforce a match between the number of parameters declared
   * by the gnuplot import command and the number implemented here.
   */
  RETURN_ERROR_IF_WRONG_NARGS(r, nargs, 1);

  /* Sanity check on argument type */
  RETURN_ERROR_IF_NONNUMERIC(r, arg[0]);

  r.v.cmplx_val.real = sin(x)/x;
  r.v.cmplx_val.imag = 0.0;
  
  return r;
}

いくつか、この関数定義の特徴を列挙したいと思います。

  1. 関数の返す値の型はDLLEXPORT struct valueで宣言します。
  2. 引数はint nargs, struct value *arg, void *pの3つです。初めの2つは、それぞれ、(gnuplotから呼ぶときの)引数の数と、引数を格納するポインタです。(void *pの用途はよくわかりません)
  3. 関数が返す値を格納するための構造体struct value rを関数の中で定義して、そこに関数の返す値を入れる必要があります。
  4. 関数への引数はx = RVAL(arg[0]);ようにして受け取ります。
  5. 関数への引数の個数が、定義とあっているかどうかをRETURN_ERROR_IF_WRONG_NARGS(r, nargs, 1);の部分でチェックします。
  6. 関数への引数の型が、数値型になっているかどうかをRETURN_ERROR_IF_NONNUMERIC(r, arg[0]);でチェックします。
  7. 実数を返す場合は、r.v.cmplx_val.real = ?????;として、rの実数部分に答えを代入します。虚数部分r.v.cmplx_val.imagにはゼロを代入します。

以上の流れに倣うと、自分なりに新しい関数を定義するCプログラムを作ることができます。例えば、以下のような例(ファイルpow.cとします)を見てみましょう。

#include "gnuplot_plugin.h"
#include <math.h>

/* xのy乗を計算する関数 gp_pow(x,y) を定義*/
DLLEXPORT struct value gp_pow(int nargs, struct value *arg, void *p)
{
  double x = RVAL(arg[0]);  /* 一番目の引数を数値に変換してxに格納 */
  double y = RVAL(arg[1]);  /* 二番目の引数を数値に変換してyに格納 */
  struct value r;           /* 返す数値を格納するために使う構造体を定義 */
  r.type = CMPLX;           /* 返す値の種類を複素数(実数含む)に */

  RETURN_ERROR_IF_WRONG_NARGS(r, nargs, 2); /* 引数が2個であることをチェック */

  RETURN_ERROR_IF_NONNUMERIC(r, arg[0]);  /* 引数が数値であることをチェック */
  RETURN_ERROR_IF_NONNUMERIC(r, arg[1]);  /* 引数が数値であることをチェック */

  r.v.cmplx_val.real = pow(x, y);   /* pow(x,y)を計算して、rの実部へ代入 */
  r.v.cmplx_val.imag = 0.0;         /* rの虚部はゼロ */
  
  return r;       /* rを返す */
}

この例は、xのy乗を計算する関数pow(x,y)をgnuplotで使えるようにするためのプログラムです。それぞれの行の意味はコメントに書いていますが、注意すべきなのは、関数が引数を2個取るため、最初の例からの変更(青字部分)が必要なことです。これが書けたら、MinGW-w64 Win32 Shell(またはMinGW-w32 Win32 Shell; 32bitの場合)から以下のようにコンパイルします。(その1で説明したMinGW/MSYSをつかう場合は、gccの部分をC:/MinGW/bin/gcc.exeに変更します)

gcc -shared -o pow.dll pow.c -DHAVE_CONFIG_H -I.

すると、pow.dllというファイルができます。このファイルと同じフォルダで以下のようなgnuplotスクリプトを走らせれば、pow(x,y)関数が使えます。

import pow(x,y) from "pow:gp_pow"
set xrange [0:1]
set key left
plot pow(x,0.5), pow(x,1), pow(x,2), pow(x,3)

このスクリプトの実行結果は以下のようになり、確かにxのy乗を計算するpow(x,y)が使えるようになっていることがわかります。

文字列を扱う関数を定義する例

gnuplot付属の例(demo_plugin.c)には、数値を引数に取る関数しか挙げられていませんが、実は文字列を引数に取るC言語関数も作ることができます。以下の例を見てみましょう。

#include "gnuplot_plugin.h"
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <stdlib.h>

/* 引数が文字列かどうかチェックするためのマクロ */
#define RETURN_ERROR_IF_NONSTRING(r, arg) \
    if (arg.type != STRING) { \
	r.type = INVALID_VALUE; \
	return r; \
    }

/* 文字列中に含まれる大文字を全て小文字に変換する関数 */
DLLEXPORT struct value strlower(int nargs, struct value *arg, void *p)
{
  struct value r;  /* 返す値を入れる構造体rを定義 */
  int i;

  RETURN_ERROR_IF_WRONG_NARGS(r, nargs, 1); /* 引数の数をチェック */
  RETURN_ERROR_IF_NONSTRING(r, arg[0]);     /* 引数が文字列かどうかチェック */

  r.type = STRING;  /* rを文字列に */
  
  /* 文字列の操作 必要に応じて書き換えればよい */
  r.v.string_val = arg[0].v.string_val;
  for(i=0; i<strlen(r.v.string_val); ++i){
    *(r.v.string_val+i)=tolower(*(r.v.string_val+i));
  }
  
  return r;    /* rを返して関数終了 */
}

ここでは、c言語の関数tolower(c)関数を用いて、文字列中に含まれる大文字を全て小文字に変換する関数strlowerを定義しています。まず、冒頭で、引数が文字列以外だったときにエラーを返すためのマクロRETURN_ERROR_IF_NONSTRING(r, arg)を定義しています。これは、gnuplot_plugin.hの中で定義されているRETURN_ERROR_IF_NONNUMERIC(r, arg)を少し書き換えたものです。関数定義の中身はほとんど数値引数の場合と一緒ですが、上で定義したRETURN_ERROR_IF_NONSTRING(r, arg)を使うところが異なります。また、返す値も文字列である場合は、r.type = STRING;としておく必要があります。

上記のc言語ファイルをstrlower.cとして保存し、MinGW-w64 Win32 Shell(またはMinGW-w32 Win32 Shell; 32bitの場合)から

gcc -shared -o strlower.dll strlower.c -DHAVE_CONFIG_H -I.

としてコンパイルします(その1で説明したMinGW/MSYSをつかう場合は、gccの部分をC:/MinGW/bin/gcc.exeに変更します)。すると、strlower.dllというファイルができます。そして、以下のようなスクリプトでstrlowerを呼び出すと、下図のような出力がコンソールに現れ、確かに大文字を小文字に変換できていることがわかります。

import strlower(s) from "strlower"
str = "This Is Test"
print str
print strlower(str)

# 結果を見るためのダミープロット
plot x

長くなってきたので、その3へ続きます。