CによるMaxオブジェクト開発における
アトリビュートの追加

アトリビュートは、Jitterで使用するものとして一部の開発者にはよく知られているものです。 同様に、Max 4.5 では、アトリビュートはカーネルコードに追加されています。アトリビュートおよびその使用に関する十分な説明は、ユーザの立場からのものは、Max に同梱されるドキュメントMax45GettingStarted.pdf で見ることができます(訳注:Maxチュートリアル5253を参照して下さい)。開発者の立場から言えば、アトリビュートはシンプルなある種のオブジェクト設計の要素であり、データに自動的な pattr認識を提供するものです。とはいえ、アトリビュートの実装には、開発者の側での若干の余分な作業が必要になります。

pattr オブジェクトは、Max および Jitter アトリビュートでもバインドすることが可能です。(Jitter アトリビュートは、Jitter 1.5 以降で、Max 4.5.5 以降に同梱されている pattr オブジェクトを使用することによってサポートされます。)これは、bindto シンタックス ‘bindto[オブジェクト名]::[アトリビュート名]’を使います。アトリビュート[アトリビュート名] が、オブジェクト[オブジェクト名] で見つかった場合、pattr はそれをバインドします。これは、オブジェクトが上記のようなテクニックを使用して直接 pattrをサポートしていない場合であっても行われます。これは非常に有用です。

Max 4.5.5 から登場した autopattr オブジェクトはアトリビュート(@greedy)を持っています。これにより、オブジェクトは、autopattr オブジェクトのインクルードインレットに接続されたクライアントオブジェクトが内部に持つアトリビュートをすべて漏れなく公開します。あなたのオブジェクトを autopattr互換にしたいが、アトリビュートを使いたくない場合や、autopattrとあなたのオブジェクトを直接接続する必要がないようにしたい場合、 getvalueof() setvalueof() メソッドを使って、直接 pattr API をサポートする必要があります。

Max 4.5 で私たちは、obex という Max オブジェクトクラスの拡張を導入しました。アトリビュートをサポートしようと思う開発者はこれを使用しなければなりません。オブジェクトを定義するための従来の API はアトリビュートの定義をサポートしていません。この定義を使いたいと思う開発者は、オブジェクトを obex API 上に移行させる必要があります。

obex とは?

setup()addmess()、および その仲間による従来の Max のクラス宣言方法には制限があります。これは、1つのエクスターナルについて1つのクラス宣言を使し、クラス名はディスク上のエクスターナルのファイルと同じであり、オブジェクトボックス内でのインスタンス化が可能であるというものです。他の任意のクラスを作る場合、クラス定義は手作業で書き込む必要があり、これらの「boxを持たない(boxless)」クラスの認識は、定義されたエクスターナルに限定されています。このような、手作業によって作られた「box を持たない」クラスの例は、SDKで提供されているcoll エクスターナルソースコードで示されています。

従来の Max クラスのもう1つの制限は、このようなクラスのオブジェクトのステート(状態)を確認し、アクセスすることが多少難しいという点です。これは、オブジェクトに動詞的概念(メッセージ)しかなく、オブジェクトのステートのある部分を表すような名詞的概念(アトリビュート)が存在しないためです。Max クラスへの obex エクステンションはこれら2つの問題を解決します。すなわち、「box を持たない」あるいは「box を持つ」クラスを、エクスターナルのロードとは切り離された方法で作成、登録することを可能にし、オブジェクトのステートとしてのアトリビュートの定義を可能にします。

そのうえ、基本的にキーワード/オブジェクトのペアを保持するハッシュテーブルである “obex”ポインタによって、オブジェクトをさらに拡張することができます。これは、ダンプアウトレット、アトリビュートに関連したオブジェクトの保存、その他にはオブジェクトのメインテナンス用に内部的に使われてきたものです。

obex API を使うことにより、オブジェクトを登録し、他のオブジェクトがクライアントとして接続できるようにすることも可能になります。このクライアントには、接続する登録済みオブジェクトに変更があった場合、それが通知されます。このオブジェクト登録機能は pattr システムによって頻繁に利用されます。

これらの変更を実装するためには、クラスの定義やアクセスのための API を変更する必要があります。あなたが新しいエクステンションを何も利用したくない場合、従来の API は以前としてサポートされ、ほとんどの場合互換性を保っていることは覚えておいて下さい。

以下は、開発者がオブジェクトを obex化するために行わなければならないステップの概要です。その後には、より詳細なリファレンスがあります。

最初のステップ

1. あなたのコードに、次のようなインクルード行を追加します:
   
  #include "ext_obex.h"

2. オブジェクトの構造体に obex ポインタを追加します:
   
 

obex メンバはあなたの Max オブジェクトに、アトリビュート管理などの追加機能を提供し、これは void * (void へのポインタ)として定義されます。従来、obex はオブジェクト構造体の2番目のアイテムとして追加されましたが、この位置に関しては義務づけられてはいません。

typedef struct _myobex { t_object ob; void *obex; ... } t_myobex;


main() 関数

3. あなたのオブジェクトクラスを定義します:
   
 

obex API を使用するためのクラス定義では、従来の関数setup() class_new()で置き換えなければなりません。class_new() は次のようにプロトタイプ宣言されます:

t_class *class_new(char *name, method mnew, method mfree, long size, method mmenu, short type, ...);

class_new()setup()と同じですが、次の点に注意して下さい:

  • class_new()t_class *を返します。これは登録されなければなりません(下記を参照)。
  • class_new()は 最初の引数として、t_messlist **ではなく、C文字列の 'name' を取ります。

例えば、新しい obex クラスとして A_GIMME 引数リストを取る myobex を作る場合、次のような class_new() 関数が使われます。

t_class *c = class_new("myobex",(method)myobex_new, (method)myobex_free,sizeof(t_myobex), (method)0L,A_GIMME, 0);


4. obex オフセットをセットします。
   
 

obex クラスメンバのバイトオフセットは、class_obexoffset_set() 関数を使って、クラスとともに登録されなければなりません。通常は、ヘルパーマクロ calcoffset() が使われます。 

class_obexoffset_set(c, calcoffset(t_myobex, obex));


4a. (オブション) コモンシンボル(common symbol)の初期化:
   
 

プロジェクトに、ソースコードファイルcommonsyms.c(c74support/max-includes/common/ 内にあります)を加え、main() に次の呼出しを追加して下さい。これは、多くの有用なシンボル(このリストを見るためには、ヘッダファイルcommonsyms.h を参照して下さい)を初期化します。このドキュメントでは、コモンシンボル が利用できるものと仮定しています。  

common_symbols_init();


5. メソッドの定義と登録を行います:
   
 

obex を使用するクラスでは、メソッドの定義で addmess()addint()addfloat() などを使う代わりに、関数 class_addmethod() を使って従来型の Max メソッドの定義、登録を行わなければなりません:

t_max_err class_addmethod(t_class *x, method m, char *name, ...);

開発者が今までのオブジェクトを移植するためには、addmess() の呼出しは簡単に変更できます。例えば:

addmess((method)myobj_test, "test", A_GIMME, 0);

は、obex インターフェイスを使うと次のようになります:

class_addmethod(c, (method)myobex_test, "test", A_GIMME, 0);

最初の引数(この例では c )は、上のように class_new() によって返される t_class * になります。

addmess() によってサポートされる全ての型、 A_LONGA_FLOAT A_SYMA_DEFLONGA_DEFFLOATA_DEFSYMA_GIMMEA_CANT は、class_addmethod() によってもサポートされます。

注: addint()addfloat()addbang() は、同様に次のような class_addmethod() 呼出しで置き換える必要があります:

class_addmethod(c, (method)myobex_bang, "bang", 0L); class_addmethod(c, (method)myobex_int, "int", A_LONG, 0L); class_addmethod(c, (method)myobex_float, "float", A_FLOAT, 0L);

技術的な注:同様にaddinx() およびaddftx() 関数はclass_addmethod()で置き換えられます。特別なメソッドの名前 “in1”、”in2”、... はintにバインドされるメソッドに使用され、”ft1”、”ft2”、 ...はfloat の場合に用いられます。例えば:

// 2番目のインレットへのintにmyobex_in1メソッドをバインドする
class_addmethod(c,(method)myobex_in1,"in1", A_LONG, 0L);
// 3番目のインレットへのfloatにmyobex_ft1メソッドをバインドする
class_addmethod(c,(method)myobex_ft1,"ft2",A_FLOAT,0L);

新しいオブジェクトを作る開発者には、Max のプロキシインレットを使うことを推奨します。


6. アトリビュートの定義と登録を行います:
   
 

アトリビュートの定義はメソッドの定義に比べてやや複雑です。実際、アトリビュートには3つの異なる型(attributeattr_offsetattr_offset_array)が存在します。ほとんどの場合、attr_offset または、attr_offset_array 型を使います。アトリビュートは、実際にはオブジェクトで、および様々な関数やメソッドに応答するものです。より詳しくは、後述の API リファレンスを参照して下さい。

attribute 型は自分自身のデータを格納するアトリビュートのために用いられます。このデータはサポートされるどのような型、長さでもかまいません。attr_offsetattr_offset_array 型は、それ自身のデータを格納しませんが、その代わりに他のデータを参照します。これは、通常、値、あるいはオブジェクト構造体の中の値で、オブジェクト構造体の中でのオフセットによって定義されるものです。attr_offset 型は、サポートされる型のどれか1つによる値を返すオフセット・アトリビュートのために用いられます。attr_offset_array 型は複数の値(リストまたはベクタ)を返すオフセット・アトリビュートのために用いられます。

重要:アトリビュート・アーギュメント(@アトリビュート構文を使用したインスタンス生成に用いられる、オブジェクトの「コマンドライン」上でのアトリビュート設定)をサポートするために、アトリビュートを使用するオブジェクトは、class_new() A_GIMMEとして定義されなければなりません

attribute オブジェクトのインスタンス生成には、関数 attribute_new() を使用します。

t_object *attribute_new(char *name, t_symbol *type, long flags,method mget, method mset);

attr_offset オブジェクトのインスタンス生成には、関数attr_offset_new() を使用します。

t_object *attr_offset_new(char *name, t_symbol *type, long flags, methood mget, method mset, long offset);

attr_offset_array オブジェクトのインスタンス生成には、関数 attr_offset_array_new() を使用します。

t_object *attr_offset_array_new(char *name, t_symbol *type, long flags, method mget, method mset, long offsetcount, long offset);

引数 mget または mset が NULLの場合、デフォルトの get および set メソッドが用いられます。attr_offset および attr_offset_array アトリビュートの場合には、引数 offset はオブジェクト構造体の中でのデータの開始位置からのバイト単位で表されるオフセットになります。attr_offset_array 型の中では、引数 offsetcount が、データベクタのメンバの数(同様に、あなたはこの数をオブジェクトの構造体に格納します)のバイト単位でのオフセットになります。マクロ calcoffset() はこれらのオフセットの計算を容易にするために用いられます。

アトリビュートの型に関わらず、新しく作られたアトリビュートはオブジェクトクラスと共に登録しなければなりません。また、これを行わないと使用することができません。

class_addattr(c, the_new_attr);

例えば、私たちの myobex オブジェクトが次のように定義されると仮定します。

typedef struct _myobex { t_object ob; void *obex; long ldata; // 1つのlongの値 float fdata[4]; // 4つのfloatの値のベクタ long fdatacnt; // fdata の値の数(== 4) t_object *attr; // アトリビュート、自分自身のデータを管理します } t_myobex;

私たちは3つの異なるアトリビュートを、次の例のように準備できます。

t_object *attr; long attrflags = 0;
attr = attr_offset_array_new( "float_data", // attrの名前 _sym_float32, // attrの型 (32-bit float) 4, // 配列の最大値 attrflags, // フラグ (method)0L, // デフォルトのgetメソッド (method)0L, // デフォルトのsetメソッド calcoffset(t_myobex, fdatacnt),// カウント calcoffset(t_myobex, fdata)// データ );
class_addattr(c, attr);
attr = attr_offset_new ("long_data", //attrの名前 _sym_long,// attrの型(long) attrflags,// フラグ (method)0L, // デフォルトのgetメソッド (method)0L, // デフォルトのsetメソッド calcoffset(t_myobex, ldata));// データ
class_addattr(c, attr);
attr = attribute_new ("atom_data", // attrの名前 _sym_atom, //attrの型(atom) attrflags, // フラグ (method)0L, // デフォルトのgetメソッド (method)0L); // デフォルトのsetメソッド
class_addattr(c, attr);

アトリビュートの値は attribute_new() の呼出しによって作られ、そして、 class_addattr() によってクラスに追加され、完全なクラスとしてグローバルに使用できるようになる点に注意して下さい。この値を、あなたのクラスの、どのインスタンス化されたオブジェクトからセットしても、クラスの全てのメンバの値を変更します。アトリビュートの他の2つの型は、単に(オブジェクトの構造体の中での、データのオフセットに基づいて)データを参照するだけなので、この動作を行ないません。attribute_new() 関数によってオブジェクトに特有のアトリビュートを作るためには、クラスの代わりに、このアトリビュートをオブジェクトと共に登録する必要があります。詳しくは、後述する obex API リファレンスを参照して下さい。

アトリビュートフラグについては、後で、より詳しく説明します。


7. dumpout および quickref 機能のサポートを追加します::
   
 

あなたのオブジェクトが、ダンプアウト・アウトレットを使用し、quickref ポップアップメニューでのアトリビュートの表示を行うためには、これら2つのメソッドを定義する必要があります。

class_addmethod(c,(method)object_obex_dumpout, "dumpout",A_CANT,0);

class_addmethod(c,(method)object_obex_quickref, "quickref",A_CANT,0);

開発者がobject_obex_dumpout() およびobject_obex_quickref() 関数を定義する必要がない点に注意して下さい。これらは Max カーネルからエクスポートされます。


8. あなたのクラスを登録します:
   
 

最終的に、Max があなたのクラスを認識することができるように、class_register() 関数を使ってこれを登録します。

t_max_err class_register(t_symbol *name_space, t_class *x);

引数 name_space は、Max パッチャーの中でインスタンス化される obex クラス(すなわち、box、UI オブジェクトなど) の場合には、定数 CLASS_BOXでなければなりません。また、内部的に使用されるだけのクラスの場合には、定数 CLASS_NOBOX でなければなりません。通常、name_spaceには CLASS_BOX が使用されます。

重要:すべてのメソッドとアトリビュートの定義を行なった後、main() の最後で class_register() 関数を呼び出されなければなりません。

このため、上の myobex クラスのケースでは、次の行が main() の最後で呼び出されます。

class_register(CLASS_BOX, c);

古いクラスモデルの場合と同様、あなたのオブジェクトのクラスをグローバル変数に保存する必要があります。


new() メソッド

9. あなたのオブジェクトクラスのインスタンスを生成します:
   
 

obex API 関数 class_new() によるクラス定義のために、従来オブジェクトクラスのインスタンス生成に用いられていた関数 newobject() を、object_alloc() に置き換えなければなりません。

void *object_alloc(t_class *c);

使い方は同じです。


10. ダンプアウト・アウトレットを作ります:
   
 

ダンプアウト・アウトレットは、アトリビュートを持つオブジェクトが‘get’という問い合わせに対する応答を出力するために用いられます。実際には、これは通常のアウトレットですが、obex オブジェクトと共に登録する必要があります。これを作る最良の方法は次のようなものです。

object_obex_store(x, _sym_dumpout, outlet_new(x, NULL));

最初の引数(ここでは x )は object_alloc()によって返されるオブジェクトポインタです。ダンプアウト・アウトレットは、オブジェクトの右端に置くという仕様になっているため、通常、他のアウトレットを作成する前に、まずダンプアウト・アウトレットを最初に作ります。


11. アトリビュートのアーギュメントを処理します:
   
 

アトリビュートは、通常、オブジェクトボックスの中で、@attrname 構文を使うことによって、直接指定されます。アトリビュートが正しく読み取られるように、次の関数を呼び出さなければなりません。

attr_args_process(x, ac, av);

最初の引数(x)は object_alloc() によって返されるオブジェクトポインタです。2番目と3番目の引数 (ac, av)はあなたの new() メソッドのためのアーギュメントで、 atom のリストによって構成されるものです。main() の中の class_new() 呼出しの中で、あなたのオブジェクトのメッセージリストが、 A_GIMMEとして定義されていたのを思い出して下さい。


サンプルリスト

これは、新しいスタイルの Max オブジェクトの骨組みとなる、非常にシンプルな、しかし完全なコードリストです。このオブジェクトは、単に、入力される値(int または float)に、アトリビュートによって定義されたオペランドを掛け合わせるものです。

#include "ext.h" #include "ext_obex.h" typedef struct _testobex { t_object ob; void *obex; float operand; void *out; } t_testobex; void *class_testobex; void *testobex_new(t_symbol *s, long ac, t_atom *av); void testobex_free(t_testobex *x); void testobex_int(t_testobex *x, long d); void testobex_float(t_testobex *x, double f); void main(void) { t_class *c; t_object *attr; long attrflags = 0; // クラス定義 - アトリビュートのために、A_GIMMEが必要 c = class_new("testobex", (method)testobex_new, (method)testobex_free, sizeof(t_testobex), (method)0L,A_GIMME, 0); // obex オブジェクトのオフセットを登録 class_obexoffset_set(c, calcoffset(t_testobex, obex)); // アトリビュートの作成と登録(attr_offset) attr = attr_offset_new("operand", _sym_float32, attrflags,(method)0L, (method)0L, calcoffset(t_testobex, operand)); class_addattr(c, attr); // いくつかのメソッドの作成と登録 class_addmethod(c, (method)testobex_int, "int", A_LONG, 0); class_addmethod(c, (method)testobex_float, "float", A_FLOAT, 0); // dumpout および quickrefメソッドの追加 class_addmethod(c, (method)object_obex_dumpout, "dumpout", A_CANT, 0); class_addmethod(c, (method)object_obex_quickref, "quickref", A_CANT, 0); // クラスをインスタンス化された box として登録 class_register(CLASS_BOX, c); // グローバルクラス変数のセット testobex_class = c; } void *testobex_new(t_symbol *s, long ac, t_atom *av) { t_testobex *x = NULL; // 新しいオブジェクトのメモリ割り当て if (x = (t_testobex *)object_alloc(testobex_class)) { // 重要: 処理の前にクラスメンバを初期化 // アトリビュート args. またはデータで置き換えることもできます x->data = 0; // attribute アーギュメントの処理 (例 @operand 0.5) attr_args_process(x, ac, av); // ダンプアウト・アウトレットの作成, アトリビュートによって用いられます object_obex_store(x, _sym_dumpout, outlet_new(x, NULL)); // 他のアウトレットの作成 x->out = outlet_new(x, NULL); } return x; } void testobex_free(t_testobex *x) { ; // なし } void testobex_int(t_testobex *x, long d) { // int を入力、intを出力 outlet_int(x->out, (long)((float)d * x->data)); } void testobex_float(t_testobex *x, double f) { // floatを入力、floatを出力 outlet_float(x->out, f * x->data); }

メソッドとアトリビュートによる新しいスタイルの Max オブジェクトの、より完全な動作は、インクルードされるソースコード例 myobexを参照して下さい。(get および set メソッドはオーバーライドされています)