Julia の組み込み

C と Fortran コードの呼び出しで見たように、Julia は C で書かれた関数を簡単かつ高速に呼び出せます。しかしときには、この逆が必要となることもあります: C コードからの Julia 関数の呼び出しです。この機能があれば、Julia コードを全て C/C++ で書き直さずとも大きな C/C++ プロジェクトにそのまま統合できるようになります。ほとんど全てプログラミング言語は何らかの方法で C 関数を呼び出せるので、Julia C API を使えばさらに遠くまで言語の橋を伸ばすこともできます (Julia を Python や C# から呼び出すなど)。

高水準の組み込み

注意: この節では Unix ライクな OS で Julia コードを C に組み込む方法を説明します。Windows で Julia を組み込む方法については次の節を見てください。

Julia を初期化して Julia コードを呼び出す単純な C プログラムから始めます:

#include <julia.h>

// 高速なコードが必要なら、実行形式の中で一度だけ定義する (共有ライブラリでは不可)。
JULIA_DEFINE_FAST_TLS()

int main(int argc, char *argv[])
{
    /* 必須: Julia コンテキストの初期化 */
    jl_init();

    /* Julia コマンドの実行 */
    jl_eval_string("print(sqrt(2.0))");

    /* 強く推奨: プログラムが終了することを Julia に伝える。
                 Julia は差し止めている書き込みリクエストをクリーンアップし、
                 全てのファイナライザを実行する。
    */
    jl_atexit_hook(0);
    return 0;
}

このプログラムをビルドするには Julia ヘッダーをインクルードパスに追加して libjulia をリンクする必要があります。例えば Julia が $JULIA_DIR にインストールされているなら、上記のテストプログラム test.c は次のコマンドでコンパイルできます:

gcc -o test -fPIC -I$JULIA_DIR/include/julia -L$JULIA_DIR/lib -Wl,-rpath,$JULIA_DIR/lib test.c -ljulia

加えて Julia ソースツリーの test/embedding フォルダにある embedding.c に目を通すこともお勧めします。libjulia をリンクするときに jl_options オプションを設定する方法についてはファイル ui/repl.c も参考になります。

Julia が用意する C 関数を呼び出すには、まず jl_init 関数による Julia の初期化が必要です。この関数は Julia のインストール場所を自動的に検出します。通常とは異なるインストール場所を指定する場合、および読み込むべきシステムイメージを指定する場合は jl_init_with_image を使ってください。

テストプログラムの二つ目の文は jl_eval_string を使って Julia の文を評価しています。

Julia を使ったプログラムでは終了する前に jl_atexit_hook を呼ぶことが強く推奨されます。上記のプログラムは main から返る直前にこの関数を呼び出しています。

情報

現在、動的リンクで libjulia を使うときは RTLD_GLOBAL オプションが必要です。例えば Python では次のようにします:

>>> julia=CDLL('./libjulia.dylib',RTLD_GLOBAL)
>>> julia.jl_init.argtypes = []
>>> julia.jl_init()
250593296
情報

メインの実行形式が持つシンボルにアクセスする Julia プログラムでは、Linux でコンパイルするときに julia-config.jl が生成するリンカフラグに加えて -Wl--export-dynamic がおそらく必要です。これは共有ライブラリでは必要になりません。

julia-config.jl を使ったビルドパラメータの自動的な設定

julia-config.jl は Julia を組み込むプログラムで使うべきビルドパラメータの設定を簡単に行うためのスクリプトです。このスクリプトは自身を呼び出した Julia ディストリビューションのシステム構成とビルドパラメータを使って、そのディストリビューションと対話する組み込み側プログラムに必要なコンパイラフラグを出力します。スクリプト julia-config.jl は Julia の shared/julia ディレクトリに入っています。

次のファイル embed_example.c をコンパイルすることを考えます:

#include <julia.h>

int main(int argc, char *argv[])
{
    jl_init();
    (void)jl_eval_string("println(sqrt(2.0))");
    jl_atexit_hook(0);
    return 0;
}

julia-config.jl をコマンドラインから使う

このスクリプトはコマンドラインからだと簡単に利用できます。/usr/local/julia/share/juliajulia-config.jl があるとすれば、次のようにコマンドラインから直接起動できます。そのとき三つのフラグの任意の組み合わせを指定します:

/usr/local/julia/share/julia/julia-config.jl
Usage: julia-config [--cflags|--ldflags|--ldlibs]

Linux および Windows (MSYS2 環境) では次のコマンドで実行形式を生成できます。OS X では gcc の代わりに clang としてください:

/usr/local/julia/share/julia/julia-config.jl --cflags --ldflags --ldlibs | xargs gcc embed_example.c

julia-config.jl を Makefile から使う

しかし一般には、組み込み側のプロジェクトはこの例よりずっと複雑です。そのような場合には次の方法を使えば一般的に Makefile をサポートできます。ここでは shell マクロの展開が必要なので、GNU Make を仮定しています。また julia-config.jl が通常の /usr/local ディレクトリに存在しないこともあるので、Julia を使って julia-config.jl を見つけるようにもしています。次に示すのは上記の例を Makefile 用に拡張したものです:

JL_SHARE = $(shell julia -e 'print(joinpath(Sys.BINDIR, Base.DATAROOTDIR, "julia"))')
CFLAGS   += $(shell $(JL_SHARE)/julia-config.jl --cflags)
CXXFLAGS += $(shell $(JL_SHARE)/julia-config.jl --cflags)
LDFLAGS  += $(shell $(JL_SHARE)/julia-config.jl --ldflags)
LDLIBS   += $(shell $(JL_SHARE)/julia-config.jl --ldlibs)

all: embed_example

こうすればビルドコマンドが make だけになります。

Windows で Visual Studio を使った高水準な組み込み

環境変数 JULIA_DIR が未設定なら、Visual Studio を開始する前に設定してください。「コントロールパネル->システムのプロパティ->システムの詳細設定->詳細設定->環境変数」から行えます。また JULIA_DIR 下の bin フォルダもシステムの PATH に入っている必要があります。

Visual Studio を開いて、新しくコンソールアプリケーションプロジェクトを作成します。stdafx.h ヘッダーファイルを開いて、次の行を最後に追加してください:

#include <julia.h>

続いてプロジェクトの main() 関数を次のコードに置き換えます:

int main(int argc, char *argv[])
{
    /* 必須: Julia コンテキストの初期化 */
    jl_init();

    /* Julia コマンドの実行 */
    jl_eval_string("print(sqrt(2.0))");

    /* 強く推奨: プログラムが終了することを Julia に伝える。
                 Julia は差し止めている書き込みリクエストをクリーンアップし、
                 全てのファイナライザを実行する。
    */
    jl_atexit_hook(0);
    return 0;
}

次はプロジェクトが Julia のインクルードファイルとライブラリを見つけられるよう設定を変更します。32 ビットの Julia と 64 ビットの Julia のどちらをインストールしたかが重要なので、インストールされた Julia と異なるプラットフォーム構成を全て削除してから設定を変更してください。

プロジェクトの「プロパティ」を開き、「C/C++->全般」の「追加のインクルードディレクトリ」に $(JULIA_DIR)\include\julia\ を追加します。さらに「リンカー->全般」の「追加のライブラリディレクトリ」に $(JULIA_DIR)\lib を追加します。最後に「リンカー->入力」の「追加の依存ファイル」に libjulia.dll.a;libopenlibm.dll.a; を追加します。

以上の設定でプログラムをビルド・実行できるはずです。

型の変換

現実のアプリケーションは式の実行だけではなくホストプログラムへの値の返却も行います。jl_eval_string の返り値は jl_value_t* 型の値であり、これはヒープにアロケートされた Julia オブジェクトを指すポインタです。Float64 のようなプリミティブ型のデータをヒープに保存することをボックス化 (boxing) とよび、ボックス化されたプリミティブ型のデータを取り出すことをボックス化解除 (unboxing) と呼びます。次の拡張したサンプルプログラムは Julia で 2 の平方根を計算し、その結果を C で読み取っています:

jl_value_t *ret = jl_eval_string("sqrt(2.0)");

if (jl_typeis(ret, jl_float64_type)) {
    double ret_unboxed = jl_unbox_float64(ret);
    printf("sqrt(2.0) in C: %e \n", ret_unboxed);
}
else {
    printf("ERROR: unexpected return type from sqrt(::Float64)\n");
}

ret が特定の Julia 型を持っているかを確認するには jl_isa, jl_typeis, jl_is_... といった関数が利用できます。Julia の REPL に typeof(sqrt(2.0)) と入力すれば、返り値が Float64 (C の double) だと分かります。上のコードに示したように、このボックス化された Julia 値を C の double に変換するときに使う関数は jl_unbox_float64 です。

それぞれの型に対応する jl_box_... 関数を使えば逆方向の変換を行えます:

jl_value_t *a = jl_box_float64(3.0);
jl_value_t *b = jl_box_float32(3.0f);
jl_value_t *c = jl_box_int32(3);

次の節で見るように、ボックス化は特定の引数を使って Julia の関数を呼ぶときに必要です。

Julia 関数の呼び出し

jl_eval_string を使えば C から Julia 式の評価結果を取得できますが、この方法では C で計算した引数を Julia に渡すことができません。このためには Julia の関数を直接的な呼び出しが必要であり、jl_call を使うとこれを行えます:

jl_function_t *func = jl_get_function(jl_base_module, "sqrt");
jl_value_t *argument = jl_box_float64(2.0);
jl_value_t *ret = jl_call1(func, argument);

最初の行では jl_get_function を呼び出して Julia の関数 sqrt へのハンドルを取得しています。jl_get_function の第一引数は sqrt が定義される Base モジュールを指すポインタです。その後 double 値を jl_box_float64 でボックス化し、最後に jl_call1 を使って sqrt 関数を呼び出しています。jl_call0 の他に jl_call0, jl_call2, jl_call3 関数も存在し、異なる個数の引数を渡すことができます。任意個の引数を渡すときは jl_call を使ってください:

jl_value_t *jl_call(jl_function_t *f, jl_value_t **args, int32_t nargs)

第二引数 arg が引数を表す jl_value_t* の配列であり、nargs が引数の個数です。

メモリ管理

ここまでのコードから分かるように、Julia オブジェクトは C でポインタとして表されます。しかしこうすると、そういったオブジェクトの解放を誰が行うのかという問題が生じます。

通常 Julia のオブジェクトを解放するのはガベージコレクタ (GC) です。しかし C が Julia の値への参照を取得しても、GC はそれを関知することはできません。つまり、あなたが C で持っているオブジェクトは GC によって勝手に解放されて無効になる可能性があります。

GC は Julia オブジェクトがアロケートされるときにだけ実行されます。jl_box_float64 のような呼び出しはアロケートを行いますが、他にも Julia コード中の様々な場所でアロケートは発生します。ただし二つの jl_... 呼び出しの間であればポインタを安全に使えるということは一般に言えます。しかし jl_... の呼び出しをまたいで値が生存することを保証するには、その値に対応する Julia のルート値への参照を C コードが保持していることを Julia に伝える必要があります。この処理を「GC ルート (GC root)」と呼びます。GC はルートされた値を利用中と認識し、それに関連するメモリを解放しません。GC ルートは JL_GC_PUSH マクロで行います:

jl_value_t *ret = jl_eval_string("sqrt(2.0)");
JL_GC_PUSH1(&ret);
// ret を使った処理を行う。
JL_GC_POP();

JL_GC_POP の呼び出しは前回の JL_GC_PUSH で作成された参照を解放します。JL_GC_PUSH は C スタックに参照を保存するので、スコープを外れる前に対となる JL_GC_POP を呼ぶ必要があることに注意してください。つまり関数が返る前、または JL_GC_PUSH を読んだブロックを抜ける制御構造の前です。

JL_GC_PUSH2, JL_GC_PUSH3, JL_GC_PUSH4, JL_GC_PUSH5, JL_GC_PUSH6 といったマクロを使うと複数の Julia の値を一度にプッシュできます。Julia 値の配列をプッシュするのに使えるのは JL_GC_PUSHARGS マクロであり、次のように使います:

jl_value_t **args;
JL_GC_PUSHARGS(args, 2); // args は二つの jl_value_t* を保持できるようになる
args[0] = some_value;
args[1] = some_other_value;
// args を使って処理 (jl_... 関数の呼び出しなど) を行う
JL_GC_POP();

一つのスコープは JL_GC_PUSH* を一度しか呼び出せません。そのため、一度の JL_GC_PUSH* で全ての変数をプッシュできない場合や、配列に入っていない変数を七つ以上プッシュする場合は、ブロックを二つ使ってください:

jl_value_t *ret1 = jl_eval_string("sqrt(2.0)");
JL_GC_PUSH1(&ret1);
jl_value_t *ret2 = 0;
{
    jl_function_t *func = jl_get_function(jl_base_module, "exp");
    ret2 = jl_call1(func, ret1);
    JL_GC_PUSH1(&ret2);
    // ret1 と ret2 で処理を行う。
    JL_GC_POP();    // ret2 がポップされる。
}
JL_GC_POP();    // ret1 がポップされる。

関数 (あるいはブロックスコープ) をまたいで変数へのポインタを保持するときは JL_GC_PUSH* を利用できません。このときは Julia のグローバルスコープに参照を作成してそれを保持することになります。これを実現する簡単な方法は、参照を保持する IdDict 型のグローバル変数を作り、これを使って GC によるメモリの解放を避けるというものです。ただし、この方法は可変な型に対してのみ正しく動作します:

// この二つの処理は初期化中に一度だけ実行させる。
jl_value_t* refs = jl_eval_string("refs = IdDict()");
jl_function_t* setindex = jl_get_function(jl_base_module, "setindex!");

...

// 変数 var を関数呼び出しをまたいで保護したい。
jl_value_t* var = 0;

...

// var は Vector{Float64} であり、可変型である。
var = jl_eval_string("[sqrt(2.0); sqrt(4.0); sqrt(6.0)]");

// var を保護するために、その参照を refs に追加する。
jl_call3(setindex, refs, var, var);

変数が不変なら、IdDict へプッシュする前に等価な可変コンテナまたは RefValue{Any} でラップする必要があります (後者が推奨されます)。このアプローチで使うコンテナの作成と値の設定は jl_new_struct のような関数を使って行います。コンテナを jl_call* で作った場合には、C コードで使うためにポインタの再読み込みが必要です:

// この三つの処理は初期中に一度だけ実行させる。
jl_value_t* refs = jl_eval_string("refs = IdDict()");
jl_function_t* setindex = jl_get_function(jl_base_module, "setindex!");
jl_datatype_t* reft = (jl_datatype_t*)jl_eval_string("Base.RefValue{Any}");

...

// 変数 var を関数呼び出しをまたいで保護したい。
jl_value_t* var = 0;

...

// var は Float64 であり、不変型である。
var = jl_eval_string("sqrt(2.0)");

// var の参照を refs に追加するまで var を保護する。
JL_GC_PUSH1(&var);

// var を RefValue{Any} でラップし、refs にプッシュして保護する。
jl_value_t* rvar = jl_new_struct(reft, var);
JL_GC_POP();

jl_call3(setindex, refs, rvar, rvar);

delete! 関数を使って refs から参照を削除すれば、(その変数への参照が他に存在しないとき) GC はその参照が持つメモリを解放できるようになります:

jl_function_t* delete = jl_get_function(jl_base_module, "delete!");
jl_call2(delete, refs, rvar);

非常に簡単な場合であれば、Vector{Any} 型のグローバルなコンテナを作って必要になるたびに要素をフェッチするという方法も可能です。あるいはポインタごとにグローバル変数を一つ作ることさえできます:

jl_set_global(jl_main_module, jl_symbol("var"), var);

GC に管理されるオブジェクトのフィールドの更新

Julia の GC は必ず古い世代のオブジェクトが新しい世代のオブジェクトを指すものとして動作します。この仮定を破る形でポインタを更新するときは、jl_gc_wb 関数 (wb: write barrier, 書き込みバリア) を使ってそのことを Julia に伝える必要があります:

jl_value_t *parent = some_old_value, *child = some_young_value;
((some_specific_type*)parent)->field = child;
jl_gc_wb(parent, child);

実行時にどちらの値が古くなるかを予測するのは一般に不可能なので、値を格納したときは必ず書き込みバリアを挿入するべきです。注目に値する例外は paren オブジェクトをアロケートした直後、ガベージコレクションがまだ実行されてないときに行う値の格納です。ほとんどの jl_... 関数はガベージコレクションを呼ぶ可能性があることに注意してください。

ポインタの配列が持つデータを直接更新したときも書き込みバリアが必要です。例を示します:

jl_array_t *some_array = ...; // 例えば Vector{Any}
void **data = (void**)jl_array_data(some_array);
jl_value_t *some_value = ...;
data[0] = some_value;
jl_gc_wb(some_array, some_value);

ガベージコレクタの操作

GC を操作するための関数はいくつか用意されていますが、通常のプログラムでは必要にならないはずです:

関数 説明
jl_gc_collect() GC の実行を強制する。
jl_gc_enable(0) GC を無効化し、直前の状態を int として返す。
jl_gc_enable(1) GC を有効化し、直前の状態を int として返す。
jl_gc_is_enabled() GC の現在状態を int として返す。

配列の利用

Julia と C の間では配列のデータをコピーせずに共有できます。次の例にこれを行う方法を示します。

Julia 配列は C でデータ型 jl_array_t* として表されます。jl_array_t は基本的に次のデータを持つ構造体です:

簡単のため最初は一次元配列を考えます。要素型が Float64 で長さ 10 の一次元配列を作成するには次のようにします:

jl_value_t* array_type = jl_apply_array_type((jl_value_t*)jl_float64_type, 1);
jl_array_t* x          = jl_alloc_array_1d(array_type, 10);

もし配列を既にアロケートしているなら、配列をデータに対する薄いラッパーとして生成できます:

double *existingArray = (double*)malloc(sizeof(double)*10);
jl_array_t *x = jl_ptr_to_array_1d(array_type, existingArray, 10, 0);

jl_ptr_to_array_1d の最後の引数は Julia がデータの所有権を取得すべきかどうかを表す真偽値です。この引数が 0 でない値だと、配列が参照されなくなったときに GC がデータポインタに対して free を呼び出します。

配列 x のデータにアクセスするには jl_array_data を使います:

double *xData = (double*)jl_array_data(x);

配列にデータを格納してみましょう:

for(size_t i=0; i<jl_array_len(x); i++)
    xData[i] = i;

続いて x に対するインプレースな操作を行う Julia 関数 reverse! を呼び出します:

jl_function_t *func = jl_get_function(jl_base_module, "reverse!");
jl_call1(func, (jl_value_t*)x);

配列のデータを出力すれば、x の要素が確かに反転したことを確認できます。

返り値の配列へのアクセス

Julia 関数が配列を返すときは、jl_eval_string および jl_call の返り値を jl_array_t* にキャストできます:

jl_function_t *func  = jl_get_function(jl_base_module, "reverse");
jl_array_t *y = (jl_array_t*)jl_call1(func, (jl_value_t*)x);

こうして手に入る y の要素には jl_array_data を使って一つ前の例と同様にアクセスできます。またこれまで通り、C から y を使っている間は Julia における参照を保っておく必要があります。

多次元配列

Julia の多次元配列はメモリ上に列優先の順序で格納されます。二次元配列を作成してそのプロパティにアクセスするコードを示します:

// 要素型が float64 の 2D 配列を作成する。
jl_value_t *array_type = jl_apply_array_type(jl_float64_type, 2);
jl_array_t *x  = jl_alloc_array_2d(array_type, 10, 5);

// 配列のポインタを取得する。
double *p = (double*)jl_array_data(x);
// 次元数を取得する。
int ndims = jl_array_ndims(x);
// i 次元のサイズを取得する。
size_t size0 = jl_array_dim(x,0);
size_t size1 = jl_array_dim(x,1);

// 配列にデータを詰める。
for(size_t i=0; i<size1; i++)
    for(size_t j=0; j<size0; j++)
        p[j + size0*i] = i + j;

Julia の配列は 1 始まりの添え字を使いますが、jl_array_dim といった C API は 0 始まりの添え字を使います。これは通常の C コードとして読めるようにするためです。

例外

Julia コードは例外を送出することがあります。例えば次のコードです:

jl_eval_string("this_function_does_not_exist()");

実行すると、この呼び出しは何もしないように見えるはずです。次のコードで例外が発生したかどうかを確認できます:

if (jl_exception_occurred())
    printf("%s \n", jl_typeof_str(jl_exception_occurred()));

例外をサポートする言語 (Python, C#, C++, ...) から Julia の C API を利用するなら、libjulia に含まれる関数を呼び出すたびに例外の発生を確認し、もし例外が発生していたらホスト言語の例外として改めて送出するラッパーを用意するのが理にかなっているでしょう。

Julia 例外の送出

Julia から呼び出せる関数を書くときは、引数を検査してエラーがあれば例外を送出する必要があるかもしれません。典型的な型検査は次の形をしています:

if (!jl_typeis(val, jl_float64_type)) {
    jl_type_error(function_name, (jl_value_t*)jl_float64_type, val);
}

一般的な例外は次の関数で送出できます:

void jl_error(const char *str);
void jl_errorf(const char *fmt, ...);

jl_error は C 文字列を受け取り、jl_errorfprintf と同じように呼び出します:

jl_errorf("argument x = %d is too large", x);

この例で x は整数と仮定されています。

広告