C++ APIをラップしKLで使用するには

このガイドでは、EDKを用いた「C++エクステンションをKLで使用するためのAPIのラッピング」を行う際、使用すべきベストプラクティスと、従うべき一般的な規則を紹介します。

概要

KL言語は、他の言語から容易に移行してこられるようデザインされています。KLはハイレベルなシステム、例えば参照カウントされるコンテナ、インタフェースを備えています。

あるAPIを KLにラッピングする目標とは、開発者が KL言語で作業 ―― そのAPIが公開されている C++ あるいは他の言語でのやり方と同じようにその APIを利用し作業できるようにすることです。一貫性のあるAPIのマッピングのおかげで、そのAPIに堪能なユーザが、新しい概念の習得の必要なくKLへ移行し素早く力を発揮することができるようなるのです。

C++ API Code:

class A {
  std::string getName();
}

C++ Example Code:

A *val1 = new A();
sprintf(val1->getName());
delete val1;

KL Example Code:

A val1 = A();
report(val1.getName());
val1 = null;

よい具合にラップされたAPIであれば、C++のコードをほんの少しの変更でKLへ移植できます。C++に定義したクラスは、KLにも同じインタフェースをもち公開されます。つまり、C++と同じようにそのクラスに対し KLの型を作成したりメソッドを呼び出したりすることが可能です。一方 KLと C++ API間での違いは、おもに KLで利用可能なメモリマネジメントがより単純であること起因します。KLにはポインターやメモリアロケーションといった概念がありません。

KLの型と直接重なるのであれば、APIの型を直接 KLの型へと「APIラッピングレイヤで」直接変換するとより良いでしょう。例えば、基本math型 ― 整数、浮動小数点数、ベクタ、クォータニオンといった型です。Fabric Engine にも mathライブラリ一式そろっているので、独自の math型をKLへと持っていくような余程の理由がない限り、シームレスな統合のため KLのmath型をAPIにより生成するべきです。

型の変換

あるAPIをC++ からKLへとマッピングする第一歩として、共通(common)なデータ型間での変換を行うユーティリティ関数一式用意します。これら関数は双方向の変換を実装し、値をKLからC++ APIへ push, また逆に値をAPIレイヤからKLへとpull することができるようにしましょう。

利便性のためC++の use namespace 機能を利用し、型,関数のプリフィクシングを避けてもよいでしょう。

Bullet統合からの参考コード

inline bool vec3ToBtVector3(const KL::Vec3 & from, btVector3 & to) {
  to.setX(from.x);
  to.setY(from.y);
  to.setZ(from.z);
  return true;
}

inline bool btVector3ToVec3(const btVector3 & from, KL::Vec3 & to) {
  to.x = from.getX();
  to.y = from.getY();
  to.z = from.getZ();
  return true;
}

これらのユーティリティ関数は APIラッピングのキーコンポーネントとなります。可能な限り単純に, しかも効率的にすべきです。なぜなら、メソッドの引数を 『KLから C++ API型へ』マップするためこのユーティリティ関数を使用するため、一度のグラフ評価のたびに大量に呼ばれる可能性があるためです。

注釈

KL型のメモリレイアウトと、C++ APIの型のメモリレイアウトが一致すると保証されている状況では、KL型はC++型へと単純にキャストすることができます。 kl2edk ユーティリティ ユーティリティを使い、生成されたヘッダファイルをチェックし、KL型のメモリレイアウトが完全に一致するか検証できます。 kl2edk ユーティリティ 参照

Class から Object へのマッピング

ある APIは 『1個の階層にまとめあげた、C++クラスのコレクション』として定義することができます。API利用者は、それらクラスをコンストラクトし、そのメソッドを呼び出すことができます。理想的には、API中に定義された 全class を KLの object にマップすべきです。KLオブジェクトのインスタンスは、C++クラスのインスタンスを表すべきです。この2つの間の関係は、<KL objectのマッピングメソッド> を通じ <C++ラッパレイヤの staticメソッド> へと管理されます。

メモリ管理

KLにおいて、「オブジェクト」は参照カウントされます。したがい、あるオブジェクトへのすべての参照が失われることで、オブジェクトは破棄されます。KLのオブジェクトには、「コンストラクタ」と「デストラクタ」があり、オブジェクト生成、破棄のタイミングで呼びだされます。KLオブジェクトの生存期間とは、インスタンス化されたそのKLオブジェクトを表すC++クラスの生存期間の管理に充てられます。

object ObjectA {
  Data pointer;
}

function ObjectA() = "ObjectA_construct";
function ~ObjectA() = "ObjectA_destroy";

KLオブジェクトを、所有元クラスあるいはノードから少なくとも一度は参照されるようにし、オブジェクトが破棄されないようにします。オブジェクトへの参照を保持することで、そのオブジェクトが破棄されないことを保証でき、さらに実際に開放するべき時を制御することが可能となります。相互依存するようなシステムのクラスについては、下記の データの所有権と双方向の関係を管理するには を参照してください。

Publicメソッドのマッピング

KL「から」C++へメソッドのマッピングを行うには、まずKLメソッドを定義し、C++ でそのメソッドが呼ばれるときに呼び出される static メソッドの名前も定義します。KLからC++へマップする必要があるのは、クラスのインタフェースを定義する publicメソッドのみです。これらのメソッドが、クラスの利用者が、そのクラスを活用する際に呼びださなければならないメソッドに該当します。KLオブジェクトとは、全proceted/privateメソッドの完全なマッピングをするの『ではなく』、ターゲットとなるクラスの pubilcインタフェースのマッピングを表します。

object ObjectA {
  ...
}
function ObjectA.methodA() = "ObjectA_methodA";

Publicメンバ

Publicメンバの、KLからC++へのマッピングは自動ではありません。KLには「KLオブジェクトあるいは構造体」のメンバの値がいつ変わったかを検知し、「マップされたC++クラスあるいは構造体」へと自動で同期するような便利機能はありません。理想的には全ての、「構造体あるいはクラス」との相互作用は、Publicメソッドを通じ行いましょう。(例外として ‘Simple Data-Container Structs’ に例があります。)

純粋なデータコンテナ構造体(Pure data container structs)

場合によって、C++ APIには単純な構造体 ―コンストラクタあるいはメソッドへ多数の変数を受け渡す― を定義することもできます。この場合、対応するKL構造体の定義が可能で、全メンバをうめていきます。このようなKL構造体をメソッドへと受け渡す際には、C++のメソッドのマッピングは、手動による「KL構造体からC++構造体」への変換として扱うことが可能です。このようにして Math型はマップサれ、どのメンバも C++ APIの型へと単純に変換されています。

struct  ClassAConstructionInfo
{
  Scalar      foo;
  Xfo         xfo;
  Vec3        bar;
};
FABRIC_EXT_EXPORT void ClassA_construct(
  Fabric::EDK::KL::Traits< Fabric::EDK::KL::ClassA >::IOParam this_,
  Fabric::EDK::KL::Traits< Fabric::EDK::KL::ClassAConstructionInfo >::INParam constructionInfo
)
{
  ClassAConstructionInfo info;

  scalarToFloat(constructionInfo.foo, info.m_foo);
  xfoToTransform(constructionInfo.xfo, info.m_xfo);
  vec3ToVector3(constructionInfo.bar, info.m_bar);

  this_->pointer = new ClassA(info);
}

KLからC-スタイルのAPIへ配列を渡すには

いくつかのC++ APIでは、とくにゲームランタイム向けとして開発されたAPIに顕著なのですが、「C++ での配列(例: std::vector)や、より高レベルの配列を表象するもの」を忌避し、かわりに「ポインタとカウンタ値」の組み合わせを使用しています。KLではポインタが公開されていないため、公開されるKL APIでは、C++メソッドの引数をKLに直接こうかいするのではなく、若干高レベルな操作を必要とします。ある一つの配列の値をメソッドに渡すと、メソッドのあるC++のラッピングするコード中で、引数を CスタイルのAPIのために展開します。これにより若干高レベルであるが KL中での利用が C++APIより容易な APIを提供します。A:必要に応じて高レベルな関数シグネチャを受け入れること, B: C++ラッピングコードの中で引数のマッピングを提供すること、この二点は開発者の裁量に委ねられています。これらの関数で必要となるKLの引数を、kl2edk では自動で展開することができません。したがってこれらのメソッドは手動で実装しなければなりません。

function A.setWeights(Vec3 weights[]) = 'A_setWeights';
FABRIC_EXT_EXPORT void A_setWeights(
  Fabric::EDK::KL::Traits< Fabric::EDK::KL::A >::IOParam this_,
  Fabric::EDK::KL::Traits< Fabric::EDK::KL::VariableArray< Fabric::EDK::KL::Float32 > >::INParam weights
)
{
  A* cThis_ = 0;
  if(!KLObjectToCPP<KL::A, A>(this_, cThis_)){
    setError("Error in A_setPositionsArray. unable to convert: this_");
    return;
  }

  this_->pointer.setPositionsArray(weights.size(), &weights[0]);
}

注釈

C++のクラスで、渡された配列の値をデータの取り出しのみに使うのであれば、コールスタックが抜け次第、その配列を安全に破棄することができます。もしC++ API クラスでこの配列へのポインタを保持しているのであれば、その配列に関連づいたメモリを、KLオブジェクトから参照しなければなりません。クラスが破棄される前に、その配列が解放されてしまうことがないようにするためです。換言すると、KL中からのその配列への参照により、KLクラスが破棄される前に、その配列が KL により開放されないようにすることができます。C++クラスが、どこか他の場所に割当てられたメモリに依存しているのであれば常に、KLからのそのデータへの参照も設定しなければなりません。 データの所有権と双方向の関係を管理するには を参照してください。
function A.setWeights(Vec3 weights[]){
   this.__weights = weights;
   this.__setWeights(weights);
}
function A.__setWeights(Vec3 weights[]) = 'A_setWeights';

注釈

組み込みの Bulletエクステンションでは、Bullet APIをこのように高レベルにラッピングしたものを実装しています。したがって KLの配列を、C++ API ではポインタとカウントが渡されるべきメソッドへと渡すことが可能となっています。提供されているBulletエクステンションのソースコードをご覧頂き、どのようにこれらが実装されているかの参考にしてください。

クラス階層のマッピング

C++ APIは互いに継承しあう複数のクラスの階層として構築されることがあります。それに対しKLのオブジェクトは現状、基本オブジェクトの継承が不可能であり、それゆえ直接C++クラス階層をマッピングすることが無理となります。

KLにはインタフェースシステムがあり、とあるクラスが実装しなければならない一連のメソッドを規定することができます。インタフェースは、C++での純粋な仮想クラスと似たものであるといえます。したがってメソッドの実装やメンバの値を持っていません。オブジェクトは複数のインタフェースを持つことができるので、クラス階層中の継承されているクラスをそれぞれ、別個インタフェースとして実装することで、オブジェクトでのクラス階層のサポートとすることができます。

したがってどのKLオブジェクトも、クラス階層中のそれぞれのクラスを継承するために定義したインタフェースをサポートしなければなりません。

C++ APIコード

// A base class that implements a method called ‘getName’.
class A {
  std::string getName();
}

// A derived class that inherits from A.
class B : public A {
  method2(A a);
}

KLラッピングコード

// The ‘A’ interface declares a method called ‘getName’.
// Objects that support the ‘A’ interface must implement ‘getName’
interface A {
  String getName();
}

// Object B supports the ‘A’ interface (can be automatically cast to A)
// and so must implement all methods defined in ‘getName’.
object B : A {
  Data pointer;
}

// Object B must implement its own methods, and all the methods inherited
// from its interfaces.
function String B.getName() = "b_getName";
function B.method2(A a) = "b_method1";

C++ラッピングコード

FABRIC_EXT_EXPORT void KL::String b_getName(...){
  return KL::String(this_.pointer->getName());
}

FABRIC_EXT_EXPORT void b_method1(
  KL::B::IOParam this_,
  KL::A::INParam a
){
  ...
}

KL用例コード

// An instance of B can be created, and assigned to a reference of
// type A interface.
A val1 = B();
report(val1.getName());

// A new instance of type B is created and passed to the first.
// method1 accepts a value of type A interface, so the B object is
// automatically cast.
A val2 = B();
val1.method1(val2);

とくに深いクラス階層では、多数のインタフェースが必要となります。継承チェーンを通じ公開する全publicメソッドは、leafオブジェクトによって直接実装する必要があります。。

注釈

継承OKなKLオブジェクトの完全なサポートは、これらかのリリースに予定されています。C++階層のマッピングをより簡便にします。

データの所有権と双方向の関係を管理するには

APIのうちいくつかでは、生存時間が相対的で、特定の順序で破棄すべきであるような、クラスを集めたものを用いることができます。KLを使用することで、システムにおけるそのようなオブジェクトの生存期間の管理に役立てることができ、常に想定通りの順序で破棄します。

KLの Ref<> 機能とは 生のアンマネージドポインタのことです。Ref<> ポインタはオブジェクトの生存期間に影響を与えません。なのでバックポインタが必要なときに使用しましょう。

注意: 仮に2つのクラスが互いに参照しあう場合、サイクリックな参照のせいでどちらも永遠に破棄されることがありません。オーナとなるオブジェクトが『所有』するオブジェクトを参照すべきです。この『所有される』オブジェクトから単純な『Ref』ポインタをオーナへと張ります。Refポインタは手動で維持すべきです。もしこのポインタが null にされることなく、オーナが破棄されてしまうとこのポインタはゴミと化し、アクセスしてしまうとクラッシュを引き起こします。いかなる状態であろうともコードを安定させるためには、クリーナップが必要となります。

object Slave {
  Data pointer;
  // The slave maintains a raw pointer to the master(not a reference)
  // Only if this pointer is valid is the slave in a valid state.
  Ref<Owner> master;
};
function Boolean Slave.isValid(){
  return this.master != null;
}

object Master {
  Data pointer;
  Slave slaves[];
};
function Master.addSlave(Slave slave){
  slave.master = this;
  this.__addSlave(slave);
  // Maintain a reference to the slave so that it is not destoyed.
  // The calling code may have only a stack-allocated reference to the slave.
  this.slaves.push(slave);
}
function Master.__addSlave(Slave slave) = "Master_addSlave";

function ~Master(){
  // By nulling the back-pointer on the slaves, they become invalid.
  // This can be used to protect against evaluation
  for(Integer i=0; i<this.slaves.size; i++){
    this.slaves[i].master = null;
  }
  this.__destroy();
  // removing all the references from the master to the slaves may cause all
  // the slaves to be destroyed, unless another class references the slaves.
  this.slaves.resize(0);
}
function Master.__destroy() = "Master_destroy";

クラス間の依存の取り扱い

クラス階層をKLにマップしたとき、クラス宣言間の依存もKLへとマップする必要があります。エクステンションのKLファイルを読み込むファルであるfpm.json ファイルがこの依存をマップする場所になります。

C++コード クラスA

class A {
  std::string getName();
}

C++コード クラスB

#include <A.h>;
class B {
  A* m_a;
}

KLコード オブジェクトA

object A {
  Data pointer;
}

KLコード オブジェクトB

object B {
  Data pointer;
  A a;
}

結果の MyExt.fpm.json ファイル

{
 "libs": [
  "MyExt"
 ],
 "code": [
  "A.kl",
  "B.kl",
 ]
}

C++クラス間の依存を、KLオブジェクトの定義中にも反映させねばなりません。さらにC++クラス間の依存を、fpm.json ファイルによって読み込まれるクラスの順にも反映させる必要があります。

注意: オブジェクトBがオブジェクトAへのKL参照を保持し、BよりもAの生存期間が長くなるようにします。Aが破棄される、あるいはBへの参照が開放される(AのみがBを参照する限り)とBが破棄されます。

便利な小技

const関数をKLのconst関数にマッピング

既定では、KLメソッドの戻す値は const です。非constな参照を引数に取り、処理結果をその引数に対して戻すようなメソッドを宣言したクラスを作成しても構いません。KL関数の宣言を、単純に関数名の末尾に ‘?’ と付け足します。

C++コード

class MyCPPClass {
   void doStuff( Scalar& value) const;
}

KLコード

object MyCPPClass {
  Data pointer;
}
function MyCPPClass.doStuff?(io Scalar value) = "MyCPPClass_doStuff";

C++コード

class MyCPPClass {
   int computeDataReturnCode();
}

KLコード

object MyCPPClass {
  Data pointer;
}
function MyCPPClass.computeDataReturnCode!(io Scalar value) = "MyCPPClass_computeDataReturnCode";

KLでオブジェクトのクローン

なんらかのC++データを参照するKLオブジェクトを、KLでクローンする時には、C++クラスも一緒にクローンされることが期待されます。オブジェクトとC++クラスの 1-1 マッピング対応を確実にします。

KLコード

object MyObj {
  String s;
  // …
};
function MyObj MyObj.clone() = "MyObj_clone";

C++コード

FABRIC_EXT_EXPORT void MyObj_clone(
  KL::Traits< KL::MyObj >::Result result,
  KL::Traits< KL::MyObj >::INParam other
{
  // The wrapped class must be cloned,
  // and the cloned wrapper KL object must
  // reference the newly constructed class.
  result->pointer = MyObj(other->pointer);
};