バインド(XtalからC++のオブジェクトを操作する)

バインドとは

バインド(bind)とは、その意味どおり、「C++の関数やクラスを使えるようにするためにXtalから見えるところに関連付ける」ということです。

これを行うことによって、Xtalから、C++の関数の呼び出し、C++のクラスのインスタンス生成・メソッド呼び出し、などC++のオブジェクトの参照・操作ができるようになります。

LuaやSquirrelでは、C++とスクリプトはスタック操作によってやりとりすることができますが、いちいち面倒です。そのため、登録や関数呼び出し周りのごたごたをサポートするライブラリを使うことが多いです。それを「バインダ」と呼んでいます。

バインダとしてはluabindやSqplusといったものが有名ですが、Xtalは言語仕様にバインダを含んでおり、ライブラリなどを追加することなく簡単にバインドすることができということは特徴にも挙げられている通りです。さらに、その作業をさらに簡単にするマクロも多数定義されています。

バインドの基本は、xtal::Any::defです。これを使ってlibオブジェクトやClassオブジェクトにバインドします。
次項からは実際にさまざまなC++オブジェクトをバインドしていきます。

バインド以外の渡し方

バインドする以外にもC++からXtalに渡す手段はあります。

  1. 実行時に引数として渡す
  2. 関数呼び出し時に引数として渡す

まあ要するに引数として渡す以外に無いですね。たぶん。

変数などのバインド

まずはlibオブジェクトにバインドしてみます。

バインドなどを行うbind_xtal関数を追加したので、一応今一度全体を提示しておきます。

#include <xtal.h>
#include <xtal_macro.h> // Xidなど便利なマクロが定義されている
 
#include <xtal_lib/xtal_cstdiostream.h>    // CStdioStdStreamLibのため
#include <xtal_lib/xtal_winthread.h>       // WinThreadLibのため
#include <xtal_lib/xtal_winfilesystem.h>   // WinFilesystemLibのため
#include <xtal_lib/xtal_chcode.h>          // SJISChCodeLibのため
#include <xtal_lib/xtal_errormessage.h>    // bind_error_messageのため
 
void bind_xtal(){
    using namespace xtal;
 
    // 変数(定数ですけど)のバインド
    lib()->def(Xid(testVar), "foo");
 
    // 何も入っていないクラスのバインド
    lib()->def(Xid(Foo), xnew<Class>());
 
    // シングルトンにすることもできる
    ClassPtr pSingleton = xnew<Class>();
    pSingleton->set_singleton();
    lib()->def(Xid(FooSingleton), pSingleton);
 
    // Arrayをバインド
    lib()->def(Xid(FooArray), xnew<Array>());
}
void exec_xtal(){
    using namespace xtal;
 
    load("test.xtal");
}
 
int main(int argc, char* argv[]){
    using namespace xtal;
 
    CStdioStdStreamLib std_stream_lib;     // std*はCの標準ライブラリを使う
    WinThreadLib thread_lib;               // Windowsのスレッドを使う
    WinFilesystemLib filesystem_lib;       // Windowsのファイルシステムを使う
    SJISChCodeLib ch_code_lib;             // SJISを使う
 
    // 環境依存である機能についてどれを使うかを設定
    // 他にもアロケータなども設定できるが割愛
    Setting setting; 
    setting.std_stream_lib = &std_stream_lib;
    setting.thread_lib = &thread_lib;
    setting.filesystem_lib = &filesystem_lib;
    setting.ch_code_lib = &ch_code_lib;
 
    // ここで指定したstd_stream_libなどのポインタが示す先のオブジェクトは、
    // uninitializeを呼び出すまで存在している必要がある。
 
    // Xtalを初期化
    initialize(setting);
 
    // エラーメッセージをバインド
    bind_error_message();
 
    bind_xtal();
    exec_xtal();
 
    // Xtalを破棄
    uninitialize();
 
    return 0;
}
// test.xtal
lib::testVar.p;
lib::Foo.p;
lib::FooSingleton.p;
(lib::FooSingleton.class() === lib::FooSingleton).p;
lib::FooArray.push_back(1);
lib::FooArray.push_back(2);
lib::FooArray.p;
xtal::lib()->def(Xid([Name]), [xtal::AnyPtr]);

が大事です。lib()に[Name]を名前としてAnyPtrを定義しているんだ、という感覚です。これさえわかれば、AnyPtrにもっていくことさえできれば大体のオブジェクトをバインドすることができます。

他に、globalやfilelocalにバインドすることもできます。

void bind_xtal(){
    using namespace xtal;
 
    // globalにバインド
    global()->def(Xid(Foo), 10);
}
void exec_xtal(){
    using namespace xtal;
 
    CodePtr code = compile_file("test.xtal");
    // filelocalにバインド
    code->def(Xid(Foo), 15);
    code->call();
}
// test.xtal
global::Foo.p;
filelocal::Foo.p;
Foo.p;

関数のバインド

関数をバインドするには、xtal::Any::defもしくはxtal::Any::def_funを用います。

関数の引数と戻り値に使える型及びそれの渡し方は制限されています。特に、ユーザー定義型は「Xtalの管理下に『こういう条件で』委任しますよー」という意味を込めてxtal::SmartPtrに包んであげる必要があります。

  • 引数
    • 整数型・浮動小数点数型
      • 値渡し
    • クラス型
      • 値渡し
      • SmartPtr(及びそれのconst参照)
      • ポインタ
      • const参照
  • 戻り値
    • 整数型・浮動小数点数型
      • 値渡し
    • クラス型
      • SmartPtr
      • 生ポインタ1)
int twice(int x){
    return x * 2;
}
int add(int x, int y){
    return x + y;
}
void bind_xtal(){
    using namespace xtal;
 
    lib()->def(Xid(twice), fun(&twice));
    lib()->def_fun(Xid(add), &add);
}
void exec_xtal(){
    using namespace xtal;
 
    load("test.xtal");
}

簡単なバインドのサンプルです。

    lib()->def(Xid(twice), fun(&twice));

xtal::funというもので関数ポインタを包むことで、Xtalから関数と認識可能(呼び出しも当然可能)な*Ptr型のインスタンスにすることができます。

    lib()->def_fun(Xid(add), &add);

これは、上のxtal::funを内部で呼び出しているラッパ関数です。どちらでも書きやすい方を使えばよいでしょう。

オーバーロードされた関数のバインド

オーバーロードされた関数をバインドしようとすると、コンパイル時にはどちらを呼び出せばよいかわからないためにコンパイルエラーになってしまいます。

void func(int x){
    xtal::stdout_stream()->print("int : ");
    xtal::stdout_stream()->println(x);
}
void func(float x){
    xtal::stdout_stream()->print("float : ");
    xtal::stdout_stream()->println(x);
}
void bind_xtal(){
    using namespace xtal;
 
    // コンパイルエラー
    lib()->def_fun(Xid(func), &func);
}
void exec_xtal(){
    using namespace xtal;
 
    load("test.xtal");
}

要はコンパイル時にどちらを使いたいかを明示すればよいのです。それには関数ポインタのキャストを使います。

intの方を使いたいとするならば次のようにキャストします。

void func(int x){
    xtal::stdout_stream()->print("int : ");
    xtal::stdout_stream()->println(x);
}
void func(float x){
    xtal::stdout_stream()->print("float : ");
    xtal::stdout_stream()->println(x);
}
void bind_xtal(){
    using namespace xtal;
 
    lib()->def_fun(Xid(func), static_cast<void (*)(int)>(&func));
}
void exec_xtal(){
    using namespace xtal;
 
    load("test.xtal");
}

クラスのバインド

Xtalへバインドできるクラスの要素は

  • 継承構造
  • コンストラクタ
  • メンバ変数
  • メンバ関数
  • クラス定数
  • クラス関数

となっています。

解放されるタイミングの関係でデストラクタだけprintf使いました……

// クラスの定義
class Foo{
public:
    Foo(int x)
    : x_(x){
        xtal::stdout_stream()->println("Foo constructed");
    }
 
    virtual ~Foo(){
        printf("Foo destructed\n");
    }
 
    virtual void Print() const{
        xtal::stdout_stream()->print("Foo::x : ");
        xtal::stdout_stream()->println(x_);
    }
 
private:
    int x_;
};
class Bar : public Foo{
public:
    Bar(int new_x)
    : Foo(new_x), x(new_x){
        xtal::stdout_stream()->println("Bar constructed");
    }
 
    virtual ~Bar(){
        printf("Bar destructed\n");
    }
 
    virtual void Print() const{
        xtal::stdout_stream()->print("Bar::x : ");
        xtal::stdout_stream()->println(x);
    }
 
public:
    int x;
};
 
// バインド
// 継承構造やコンストラクタをバインドする
XTAL_PREBIND(Foo){
    // コンストラクタ登録
    Xdef_ctor1(int);
}
// メソッドやインスタンス変数、クラス関数などをバインドする
XTAL_BIND(Foo){
    // メソッド登録
    Xdef_method(Print);
}
XTAL_PREBIND(Bar){
    // 継承構造を登録
    it->inherit(xtal::cpp_class<Foo>());
    // コンストラクタ登録
    Xdef_ctor1(int);
        // デフォルト引数を登録
        Xparam(y, 0);
}
XTAL_BIND(Bar){
    // メソッド登録
    Xdef_method(Print);
 
    // メンバ変数登録
    Xdef_var(x);
}
 
void bind_xtal(){
    using namespace xtal;
 
    // クラスをlibにバインドする
    // これでlib::Fooなどでクラスオブジェクトにアクセスできるようになる
    lib()->def(Xid(Foo), cpp_class<Foo>());
    lib()->def(Xid(Bar), cpp_class<Bar>());
}
void exec_xtal(){
    using namespace xtal;
    load("test.xtal");
}
foo : lib::Foo(2);
foo.Print();
 
bar : lib::Bar();
bar.Print();
 
bar2 : lib::Bar(10);
bar2.x = 15;
bar2.Print();

XTAL_PREBIND, XTAL_BINDとdef(Xid([Name], cpp_class<[Name]>())がポイントです。

// バインド
// 継承構造やコンストラクタをバインドする
XTAL_PREBIND(Foo){
    // コンストラクタ登録
    Xdef_ctor1(int);
}
// メソッドやインスタンス変数、クラス関数などをバインドする
XTAL_BIND(Foo){
    // メソッド登録
    Xdef_method(Print);
}

ここでは、XtalスクリプトでFooクラスがどのようなメンバを持っているかを設定しています。ctorを登録しないと、インスタンスを生成することができません。 XTAL_BINDの中で使える登録マクロは次のようなものがあります

Xdef_method(method_name) メソッドを登録する
Xdef_method_alias(method_name, method_ptr) メソッドを別名で登録する
Xdef_fun(function_name) クラス関数を登録する
Xdef_fun_alias(function_name, function_ptr) クラス関数を別名で登録する
Xdef_getter(var_name) メンバ変数のgetterを登録する
Xdef_setter(var_name) メンバ変数のsetterを登録する
Xdef_var(var_name) メンバ変数(のsetter/getter)を登録する
Xparam(var_name, value) デフォルト引数を設定する Xdef_method/Xdef_fun/Xdef_ctorNの直後に使う
Xdef(name, AnyPtr) 何か(AnyPtr)を登録する
Xdef_const(const_name) クラス定数を登録する
    // クラスをlibにバインドする
    // これでlib::Fooなどでクラスオブジェクトにアクセスできるようになる
    lib()->def(Xid(Foo), cpp_class<Foo>());
    lib()->def(Xid(Bar), cpp_class<Bar>());

ここでは、libオブジェクトに「callするとインスタンスを生成できる、クラスオブジェクト(ClassPtr)」をバインドしています。これで、C++におけるFooクラスがXtalにおいてはどのような名前か、を指定しています。指定しないとXtalでどんな名前か、がわからないためインスタンスを生成することができません。

生成はC++で行って、Xtalからはメンバに触るだけで良い、となったらctor定義を抜いたりクラスオブジェクトのバインドを省くと良いでしょう。また、Xtalから触る必要が全く無い2)のであればバインド作業をする必要はありません。

型変換(2) AnyPtrとユーザー定義型

前ページに引き続き、今度はAnyPtrとユーザー定義型との相互変換です。

関数から返す際にはユーザー定義型をAnyPtrに変換できる型で返す必要があるため、割と重要だったりします。

AnyPtrからユーザー定義型への変換

AnyPtrから組み込み型への変換と同じ方法のほか、xtal::cast<T>や xtal::unchecked_cast<T>を用いる方法があります。
ptr_castに関しては型変換(1) AnyPtrとプリミティブ型・組み込み型と同じく、xtal::SmartPtr<T>が返ります。記述法などはそちらの項を参照のこと。
もし生ポインタが必要であればxtal::SmartPtr<T>::get()とすることで得ることができます。ただし、この方法で得た生ポインタはXtalのガベージコレクションでオブジェクトが解放された後は無効なアドレスを指すのでメモリ不正アクセスとなるので、オブジェクトの寿命に注意しなくてはいけません。

class Foo{
public:
    Foo(int x): x_(x){}
    int x() const{
        return x_;
    }
    void set_x(int x){
        x_ = x;
    }
    void Print() const{
        xtal::stdout_stream()->println(x_);
    }
 
private:
    int x_;
};
XTAL_PREBIND(Foo){
    Xdef_ctor1(int);
}
void GetPtr(const xtal::AnyPtr& foo){
    using namespace xtal;
    SmartPtr<Foo> p = ptr_cast<Foo>(foo);
 
    p->Print();
 
    Foo* const pfoo = p.get();
 
    pfoo->set_x(pfoo->x()+10);
    pfoo->Print();
}
 
void bind_xtal(){
    using namespace xtal;
}
void exec_xtal(){
    using namespace xtal;
 
    const CodePtr code(compile_file("test.xtal"));
    code->def(Xid(Foo), cpp_class<Foo>());
    code->def_fun(Xid(GetPtr), &GetPtr);
    code->call();
}
// test.xtal
foo : Foo(234);
GetPtr(foo);

しかし、この方法はいろいろな型が来得るかつそれぞれで異なる動作をしたいとき(例えばxtal::nullも受ける場合など3))に主に必要となるだけで、この変換部分をXtalに任せることもできます。

方法は、引数をユーザークラスのポインタかSmartPtrにするだけです。

class Foo{
public:
    Foo(int x): x_(x){}
    int x() const{
        return x_;
    }
    void set_x(int x){
        x_ = x;
    }
    void Print() const{
        xtal::stdout_stream()->println(x_);
    }
 
private:
    int x_;
};
XTAL_PREBIND(Foo){
    Xdef_ctor1(int);
}
void GetPtr(Foo* const foo){
    xtal::stdout_stream()->println("@GetPtr");
    foo->Print();
    foo->set_x(foo->x()+10);
    foo->Print();
}
void GetPtr2(const xtal::SmartPtr<Foo>& foo){
    using namespace xtal;
    stdout_stream()->println("@GetPtr2");
    foo->Print();
    GetPtr(foo.get());
}
 
void bind_xtal(){
    using namespace xtal;
}
void exec_xtal(){
    using namespace xtal;
 
    const CodePtr code(compile_file("test.xtal"));
    code->def(Xid(Foo), cpp_class<Foo>());
    code->def_fun(Xid(GetPtr), &GetPtr);
    code->def_fun(Xid(GetPtr2), &GetPtr2);
    code->call();
}
// test.xtal
foo : Foo(234);
GetPtr(foo);
GetPtr2(foo);

ユーザー定義型からAnyPtr

型変換(1) AnyPtrとプリミティブ型・組み込み型でも触れたとおり、SmartPtr<T>からAnyPtrへは暗黙で変換されます。つまり、ユーザー定義型をSmartPtr<T>に変換できれば良いわけです。

まず、新しくオブジェクトを作ってXtalに渡す(Xtalに管理してもらう)場合、xtal::xnew<T>を使うことができます。

class Foo{
public:
    Foo(int x): x_(x){}
    int x() const{
        return x_;
    }
    void set_x(int x){
        x_ = x;
    }
    void Print() const{
        xtal::stdout_stream()->println(x_);
    }
 
private:
    int x_;
};
// XtalではFooをインスタンス化できなくした
XTAL_PREBIND(Foo){}
XTAL_BIND(Foo){
    Xdef_method(Print);
    Xdef_method(set_x);
    Xdef_method(x);
}
xtal::SmartPtr<Foo> CreateFoo(int x){
    return xtal::xnew<Foo>(x);
}
 
void bind_xtal(){
    using namespace xtal;
}
void exec_xtal(){
    using namespace xtal;
 
    const CodePtr code(compile_file("test.xtal"));
    code->def_fun(Xid(CreateFoo), &CreateFoo);
    code->call();
}
// test.xtal
foo : CreateFoo(10);
foo.x += 20;
foo.Print();

new T がT*を返すのに対して、xnew<T>は代わりにSmartPtr<T>を返します。

あと、これは詳細を確認していないのでなんともいえないのですが、xnew<T>を用いる場合、xtal::Baseを継承していないとメモリアクセス例外を吐くことがある、かもしれません。とりあえず、Baseを継承していないクラスの生成にはnewとundeleterのセットを使うと良いと思います。


もう1つは、xnewで作られなかった(newを使った、スタックに作成した、静的変数、など)オブジェクトをSmartPtrに変換する方法です。

適切にSmartPtrに変換しなかった場合、オブジェクトが破棄されてメモリ不正アクセスが発生しますので、注意が必要です。

また、この方法ではオブジェクトを破壊するときに呼ばれる関数を指定できますが、公式リファレンスでも紹介されているデフォルトの「何もしない関数(xtal::undeleter)」を指定するとエラーでて止まります。まじかー。
と思っていたのですが、どうもXTAL_PREBINDをしていないとエラーが出る、ということらしいです。コンストラクタなど登録しないクラスでもXTAL_PREBINDは記述しましょう。

「newしたものなんだけどXtalにオブジェクト管理任せるわー」というときにxtal::deleterを指定するとき以外は第2引数は指定しなくても大丈夫だと思いますが、一応以下のデフォルト動作を確認して利用してください。

  • xtal::RefCountingBase, xtal::Base, xtal::Any, xtal::AnyPtrおよびそれらのサブクラスの場合
    • 参照カウンタが操作される
  • その他のクラスの場合
    • undeleterの指定

実際にいくつかSmartPtr構築法を実装してみます。

// xnewで直接作成
xtal::SmartPtr<Foo> CreateFoo(int x){
    return xtal::xnew<Foo>(x);
}
// newされたオブジェクトからの変換
xtal::SmartPtr<Foo> CreateFoo2(int x){
    Foo* foo = new Foo(x);
    return xtal::SmartPtr<Foo>(foo, xtal::deleter);
}
// 静的変数から変換
xtal::SmartPtr<Foo> CreateFoo3(int x){
    static Foo foo(0);
    foo.set_x(x);
    return xtal::SmartPtr<Foo>(&foo);
}
// こちらのパターンでも大丈夫
Foo *CreateFoo4(int x){
    static Foo foo(0);
    foo.set_x(x);
    return &foo;
}
 
void bind_xtal(){
    using namespace xtal;
}
void exec_xtal(){
    using namespace xtal;
 
    const CodePtr code(compile_file("test.xtal"));
    code->def_fun(Xid(CreateFoo), &CreateFoo);
    code->def_fun(Xid(CreateFoo2), &CreateFoo2);
    code->def_fun(Xid(CreateFoo3), &CreateFoo3);
    code->def_fun(Xid(CreateFoo4), &CreateFoo4);
    code->call();
}
foo : CreateFoo(10);
foo.x += 20;
foo.Print();
 
foo2 : CreateFoo2(20);
foo2.x += 30;
foo2.Print();
 
foo3_1 : CreateFoo3(10);
foo3_1.Print();
 
foo3_2 : CreateFoo3(20);
foo3_1.Print();
foo3_2.Print();
 
foo4_1 : CreateFoo4(10);
foo4_1.Print();
 
foo4_2 : CreateFoo4(20);
foo4_1.Print();
foo4_2.Print();

xnewとnewを使う方法は呼び出されるたびにオブジェクトを生成しているため、返すポインタの示す先は毎回新しいアドレスとなっています。それに対して、3番目の方法は実際は1つしかないオブジェクトのアドレスをポインタに格納しているために、返された変数は共有されているようなものです。どれか1つを変更したら全て変更されます。

1) XTAL_PREBINDを忘れないように。
2) 関数から返したりはするけどそれを他のC++関数に渡したりするだけ、とか
3) この場合に引数をユーザークラスのポインタにすると、nullを渡したときには型不整合でXtalの例外が飛びます
embedding/bind.txt · 最終更新: 2012/03/31 10:54 by sukai
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0