Tech Note 05d: 共有ライブラリへの改良点

November 25, 2003

© NSB Corporation. All rights reserved.

[英語版]

本稿はNSBasic共有ライブラリ インターフェースへの改良点を説明します。この内容は、すでにNS Basic共有ライブラリを理解している人向けです。

1. 改良された.infファイルフォーマット

NSBasicは、以下の決まりを持ったサブルーチン、ファンクションとして、PALM OS共有ライブラリをサポートします。

例:

Err Multiply(UInt16 refNum, double src1, double src2, double *ret)
{
    *ret = src1 + src2;
    return 0;
}
これはファンクションとしてNSBasic内での使用に適しています。(新しい共有ライブラリのメカニズムでは、サブルーチンとしても定義できます。)
Err ChangeStatus(UInt16 refNum, Int32 in)
{
    if (in) TurnOn(); else TurnOff();
    return 0;
}

これはサブルーチンとしての使用に適しています。ファンクションの代わりにサブルーチンを使用したり、サブルーチンの代わりにファンクションを使うのはエラーになります。

NSBasic内でライブラリがどのように使われるかを指定する.infファイルが伴います。一般的な.infファイルはこのような見た目になります:

[General]
ExtensionName=Test-library
PrcName=TestLib.prc
Version=2.0 Beta
Manufacturer=NSBasic.com

[GlobalMethods]
Multiply=1,func,2,""
ChangeStatus=2,proc,1, ""
最後の2つのラインが重要です。ここではMultiplyが最初のサブルーチンかファンクションで、2つの引数を持つことを示しています。ChangeStatusは2番目のサブルーチンかファンクションで、引数を1つ持ちます。

古いメカニズムでは、NSBasicで与えられる引数のタイプを基に、これらの引数のタイプを想像しなければなりませんでした。これでは、リターンファンクション以外の数値の戻り値を妨げたり、ファンクションの呼び方を選ぶ時に十分な注意が必要であったり、タイプの判定が難しい時に修正が困難なバグという結果になるなど、多くの共有ライブラリが使えないままでした。

新しいメカニズム

新しいメカニズムでは、これらの問題を解決しました。唯一の変更は.infで、以下のような見た目になります:
[General]
ExtensionName=Test-library
PrcName=TestLib.prc
Version=2.0 Beta
Manufacturer=NSBasic.com
InfVers=2.0

[GlobalMethods]
Multiply=1,func,2,"Multiply(in a as double, in b as double) as double"
ChangeStatus=2,proc,1, "ChangeStatus(in a as integer)"

唯一の違いは、引用符で囲まれた要素です。古いメカニズムでは、これは空白か、重要でないコメントなどでしたが、新しいメカニズムでは、タイプ情報を含んだファンクションの説明です。この引用符に続くものは全て無視されますので、コメントとして使用することができます。

ファンクションまたはサブルーチンの説明は、NSBasicでの定義に似ていますが、コンパイラーに渡すための重要な情報を入れるために一工夫されています:

   ChangeStatus(in a as integer)

これは1つの引数を持つサブルーチンを指定しています。"In a as integer"は、1つの引数(integer)があることを意味しています。さらにこれはパラメーターの中にあることを指定しています。全部で3つの異なった引数があります:in、out、inoutです。(inoutは一文字です。)in パラメーターは、値が共有ライブラリファンクションに入っていくだけの時に使います。引数がある場合、共有ライブラリのCコードは、doubleやInt16などのシンプルなタイプを期待します。outとinoutパラメーターは、共有ライブラリから値が出てくる時や、入って出てくる時に使います。outやinoutの場合、Cコードはdouble*やInt16などのタイプへのポインターを期待します。

タイプは大文字や小文字の違いは関係ありません。

.infファイルをCを好む人と、NSBasicを好む人の両方へ利用可能にするため、いくつかのタイプは複数の指定の仕方ができます:

NSBasic Type    C Type         .inf types
------------    ------         ----------
Integer         long, Int32    integer, int32, int4, long
Short           short          short, int16, int2, int
Float           double         double, flt8, flt64
Single          float          float, single, flt4, flt32
String          char *         string, char
Variant         ? *            variant

"float"に関して、NSBasicでは64ビット浮動小数点数で、Cでは32ビット浮動小数点数ですので、混乱を招く恐れがありますのでご注意下さい。

ファンクションは、最後にタイプ スペシファイアーを置くことを除いて、サブルーチンと同様に指定します:

   Multiply(in a as double, in b as double) as double

引数がin、out、inoutのどれであろうと、引数のタイプが何であろうと、また戻り値が何であろうと、タイプはCとNSBasicの間で自動的に変換されます。

inoutパラメータによって、以前よりも機能的に充実しました。 次のCで書かれた共有ライブラリファンクションを見てみましょう:

    Err DoIt(UInt16 refNum, double src1, double *ret, double src2)

retをリターン引数と想定した場合、以前はこの値を受け取ることは無理でした。inoutパラメーターを使うと、とても簡単です:

    DoIt(in src1 as double, inout ret as double, in src2 as double)
これをNSBasicからこのように呼びます:
    TestLib.DoIt(3, x, 4)

結果は、適切にタイプ変換が行われ、xにDoltがretに入れた値が入ります。

旧バージョンとの互換性

古いライブラリは、今まで通り動作しますが、古い.infファイルを使うと、IDEが警告メッセージを出します。新しいメカニズムは古いものより数段良く、アップグレードも容易です。

2. .INFファイルのバージョン番号

コンパイラーが正しく.INFファイルを解析するために、.INFファイルの最初のセクションに"InfVers=2.0"というラインを入れます。

3. 更に多くの引数

共有ライブラリとシステムトラップの引数の数は、最高で50まで持つことができます。

これは新旧両方の共有ライブラリに適用されています。

4. ダイナミック文字列

旧バージョンの共有ライブラリは、スタティック文字列にしか文字を保管できませんでした。これからは任意の長さの文字列を、メモリー領域以外の制限を持たずに、文字列を返す方法が2つあります。これらは新しいスタイルの共有ライブラリでしか実装されていません。

方法0: 古い方法

以下のファンクションを考えてみて下さい:
/**
	Yuk generator after Doug Lee
	Returns a sentence of n Yuks

    Yuk(3) => "Yuk, yuk, yuk."
    Yuk(1) => "Yuk."
  */
Err Yuk(UInt16 refNum, short in, char *ret)
{
    char *s;
    int k;
    
    if (in <= 0) return 0;
    
    s = ret;
    
    for (k = 0; k < in; ++k)
    {
        if (k)
        {
            *s++ = ',';
            *s++ = ' ';
        }
    	*s++ = k ? 'y' : 'Y';
    	*s++ = 'u';
    	*s++ = 'k';
    }
    *s++ = '.';
    *s = '\0';
    
    return 0;  // No error
}

.infファイルによって、1つのshort引数を持つファンクションか、2つの引数(shortと文字列)を持つサブルーチンです。ここでのポイントは、.infファイルでどちらに定義しようが構わないということです。

ここでの問題は、 結果文字列を*retから始まるところに保管しますが、メモリーがどれだけ長いかを知ることができません。オリジナルの共有ライブラリコードは、前回返した文字列のサイズが300バイトより長い場合はそのサイズを、短い場合は300バイトを確保します。しかし、これはアプリケーションによっては十分ではないでしょう。

方法1: ハンドルをリサイズ

*retに渡されるポインターは、実際はロックされたハンドルへのポインターです。文字列は*retから始まります。しかし、*retのすぐ前は*retへのポインターです。方法1は、ハンドルを取り戻し、それをサイズ変更、再ロックし、正しく設定することが含まれます。

方法1は、入力データによって文字列がその場で作られる場合に最も有効でしょう。

この方法で書かれたYukをご覧下さい:

/**
	Yuk generator after Doug Lee
	Returns a sentence of in Yuks

    Yuk(3) => "Yuk, yuk, yuk."
    Yuk(1) => "Yuk."
  */
Err Yuk(UInt16 refNum, short in, Char *ret)
{
    char *s;
    int k;
    
    if (in <= 0) return 0;
    
    /* NEW CODE */
    {
    	MemHandle m = MemPtrRecoverHandle(ret - sizeof(Char *));
    	MemHandleUnlock(m);

    	if (0 != MemHandleResize(m, in * 5 * sizeof(Char) + sizeof(Char *)))
    	{
    	    ErrFatalDisplay("Resize failed in Yuk.");
    	}
    	
    	s = MemHandleLock(m);
    	*((Char **) s) = s + sizeof(Char *);
    	s += sizeof(Char *);
    }
    /* END NEW CODE */
    
    for (k = 0; k < in; ++k)
    {
        if (k)
        {
            *s++ = ',';
            *s++ = ' ';
        }
    	*s++ = k ? 'y' : 'Y';
    	*s++ = 'u';
    	*s++ = 'k';
    }
    *s++ = '.';
    *s = '\0';
    
    return 0;  // No error
}

新しいコードは別のブロックに収まっていて、最初のバージョンの"s = ret;"を置き換えています。ではライン毎に:

    	MemHandle m = MemPtrRecoverHandle(ret - sizeof(Char *));

このポインターの最初に合わせるために、retからsizeof(Char *)を引き、そこを指すハンドルを取り戻します。

    	MemHandleUnlock(m);

ハンドルはサブルーチンに入るとロックされます。このロックを解除します。ここからはretによって与えられる値は信用できませんので、使わないように注意して下さい。

    	if (0 != MemHandleResize(m, in * 5 * sizeof(Char) + sizeof(Char *)))
    	{
    	    ErrFatalDisplay("Resize failed in Yuk.");
    	}

Yukの構造は、4つを使う最後を除き、各Yukは5つの文字を使うことを意味します。文字列の最後に必要なもう1つの文字を含み、in * 5は文字列パートには正しいバイト数になります。これはsizeof(Char)の倍数ですが、単に固執した理由でCharがcharと同じサイズの場合、sizeof(Char)は常に1になります。sizeof(Char *)の加算はポインターの為で、メモリーブロックの最初である必要があります。

これは1つの例です。商用のコードでは、単なる致命的な表示ではなく、エラーの復旧処理をすべきでしょう。

    	s = MemHandleLock(m);

ハンドルを再ロックし、ポインターをsに入れます。ここからは、sのみが使われ、retは使いません。

    	*((Char **) s) = s + sizeof(Char *);

*sの最初のパートは、sの残りの部分へのポインターで、そこに入れます。これをしない場合、NSBasicは文字列を探すことができません。

    	s += sizeof(Char *);

ファンクションの残りの部分で、この文字列へのポインターとして使いますので、sをこのポインターの先へ進めます。

方法2: ポインターを変更する

*retに渡されるポインターは、実際はロックされたハンドルへのポインターです。文字列は*retから始まります。しかし、*retのすぐ前は*retへのポインターです。方法2では、*retへのポインターを別のポインターと取替えます。

方法2は、返される文字列がメモリー内で固定長で、少なくとも次の共有ライブラリコールまでは変更されない場合に便利です。恐らく文字列はデータベース内にあるか、または、共有ライブラリによって管理されている「グローバル」文字列とかでしょう。

この方法で書かれたYukをご覧下さい:

**
	Yuk generator after Doug Lee
	Returns a sentence of in Yuks

    Yuk(3) => "Yuk, yuk, yuk."
    Yuk(1) => "Yuk."
  */
Err Yuk(UInt16 refNum, short in, Char *ret)
{
    char *s;
    int k;
    
    if (in <= 0) return 0;
    
    ret -= sizeof(Char *);
    * ((Char **) ret) = HaveSomeYuks(in);
    
    return 0;  // No error
}

HaveSomeYuksは定義されていませんが、スタティック文字列へのポインターを返すファンクションだと思って下さい。

リターンプロセスの一部として、この文字列はNSBasic文字列へとコピーされます。しかし、長い文字列でも、この方法で書けば、ファンクションはこれを越えた余分な臨時のメモリーを使うことはありません。

5. GetVersion(prcName)

この関数は、prcNameのTVERリソースに入っているバージョン文字列を返します。これを使ってプログラムの最新バージョンがインストールされているか確認できます。

6. リソースとしての共有ライブラリ

今までは、リソースとして共有ライブラリにアクセスする唯一の効果的な方法は、DbCreateDatabaseFromResourceを使うことでした。これは今でも動作しますが、新しい方法があります。プロジェクトに共有ラブラリを追加する時、Resource Typeを'libr'へ変更することができます。これ以降、共有ライブラリは、別のファイルに読み出す必要もなく、直接使用可能になります。これはより簡単なインストールと、より優れたバージョン管理を提供します。

7. 改善したライブラリ共有

NSBasicが他のプロセスとライブラリを共有する場合、より美しく処理するため、2つの変更が加えられました。ライブラリをロードする時、NSBasicがSysLibFindを使って既にライブラリがロードされていることを検出すると、2度目のロードは試みません。さらに、他のプロセスがライブラリを既にアンロードしている場合、2度目のアンロードは試みません。