Excel 2007 のアドイン (XLL) の開発

  

Steve Dalton (Eigensys Ltd.)

2006 年 10 月
日本語版最終更新日 2008 年 4 月 3 日

適用対象:
   2007 Microsoft Office system
   Microsoft Office Excel 2007

要約: XLL アドインに関係し、新しい XLL 機能を有効にする Microsoft Office Excel 2007 の機能と、XLL C API 自体の変更について説明します。

Excel 2007 XLL Software Development Kit (英語) をダウンロードしてください。

目次

Excel 2007 における XLL の開発
XLL に関連する Excel 2007 機能の概要
XLL の概要
Excel 2007 における XLL の変更
クロス バージョン XLL の作成
スレッドセーフな XLL およびワークシート関数の作成
まとめ

Excel 2007 における XLL の開発

この記事は、Microsoft Office Excel アドイン (XLL) を作成した経験のある C 開発者および C++ 開発者を対象としています。 簡単な概要も記載されていますが、XLL 開発に関する初歩的な記事ではありません。 この記事を活用するには、次の事項についての知識が必要です。

  • C および C++ 言語の概念と構造 (コード例は C++ で記述されています)
  • 関数をエクスポートする DLL の構築
  • XLOPER データ構造や浮動小数点 (FP) 行列構造などの Excel のデータ型
  • アドイン マネージャ インターフェイス関数 (xlAutoOpenxlAutoClose など)
  • XLOPER のメモリ管理 (xlAutoFreexlFreexlbitDLLFreexlbitXLFree の使用に関する知識も必要です)

**重要:   **この記事で説明されているこの機能については、「Microsoft Office Excel 2007 XLL Software Development Kit (SDK) (英語)」をご覧ください。 Excel 2007 XLL SDK は、Microsoft Download Center (英語) からダウンロードできます。

XLL に関連する Excel 2007 機能の概要

Microsoft Office Excel 2007 における XLL 関連の機能で最も明白な変更点は、ワークシートが 224 セルから 234 セル、つまり 256 列 x 65,536 行 (28 x 216) から 16,384 列 x 1,048,576 行 (214 x 220) に拡張されたことです。この新しい条件では、従来の範囲および配列構造に存在する整数型でオーバーフローが発生します。この変更に対しては、範囲および配列のサイズを定義するためのより広い範囲の整数を使用する、新しいデータ構造が必要です。

関数が取ることのできる引数の個数の上限は 30 個から 255 個に引き上げられています。さらに、XLL では Excel との間で、長さ制限のあるバイト文字列に限らず、長い Unicode 文字列も交換できるようになりました。

マルチスレッド ブックの再計算は、シングルプロセッサおよびマルチプロセッサ コンピュータでサポートされます。 Microsoft Visual Basic for Applications (VBA) のユーザー定義関数 (UDF) とは異なり、XLL の UDF はスレッドセーフとして登録できます。 Excel に組み込まれた大部分のワークシート関数と同じように、これらの UDF を並行スレッドに割り当てて、再計算を高速化することができます。 このメリットを享受する際は、一定の制約に従うことが必要です。また、安全性を欠いた動作でマルチスレッドを濫用することのないように注意してください。

Analysis Toolpak 関数は、Excel に完全に統合されました。ただし、Data Analysis ツールのアドインは引き続き必要です。そのため、以前のバージョン用に作成された XLL (xlUDF を使用して ATP 関数を呼び出す) では、一部に互換性の問題が生じます。

ユーザー インターフェイスも大幅に変更されています。この記事ではユーザー インターフェイスのカスタマイズについては扱いません。なお、従来の XLL カスタム メニューとメニュー項目は引き続き有効ですが、古い C API 関数で指示されていた場所には配置されません。

XLL の概要

Microsoft Excel 4.0 以降、Excel と XLL の連携がサポートされるようになりました。 それに加えて、XLL が Excel の関数やコマンドにアクセスするためのインターフェイス (すなわち C API) もサポートされています。 XLL とは、Excel のアドイン マネージャが必要とするコールバック、およびエクスポート対象の XLL のコマンドおよびワークシート関数を含む DLL の名前です。 このインターフェイスは、Excel 5.0 以降、大幅な変更はありませんでした。 刷新された C API では、Excel 2007 の多くの新機能、およびこれまではサポートされていなかった以前のバージョンの機能のいくつかが使用できるようになりました。 この記事では、この新しい C API に追加された機能を紹介し、開発者が直面する移行に関するいくつかの問題について説明します。

マイクロソフトでは、Excel 97 のソフトウェア開発キット (SDK) を公開しました。この SDK には、次のコンポーネントが含まれます。

  • C ヘッダー ファイル xlcall.h。このファイルには、XLL とデータを交換するために Excel が使用するデータ構造の定義に加えて、組み込みの Excel ワークシート関数、XLM 情報関数、および多くのコマンドに対応する列挙関数とコマンドの定義、さらに Excel コールバック関数 Excel4Excel4v、および XLCallVer のプロトタイプが含まれています。
  • 静的インポート ライブラリ xlcall32.lib。Excel コールバックはこのライブラリから、C 名前装飾を使用してエクスポートされます。
  • XLL SDK Framework プロジェクト。このプロジェクトには、1 つの完全な XLL プロジェクトと、Excel データ型を処理して Excel4Excel4v への呼び出しを支援するさまざまなルーチンが含まれます。

新しいExcel 2007 XLL SDK は、Microsoft Download Center (英語) で入手可能です。

最も単純な XLL は、アドインが読み込まれるとき Excel によって呼び出される関数 xlAutoOpen をエクスポートするものです。この関数は初期化タスクを実行し、XLL がエクスポートする関数およびコマンドを登録します。その後、アドインは XLM 関数 REGISTER() に対応する C API の関数を呼び出します。Excel ライブラリ xlcall32.dll は、XLL による Excel へのコールバックを可能にする関数 Excel4 および Excel4v をエクスポートします (これは、XLL 機能が導入されたのがバージョン 4 だったことを反映しています)。現在、Excel 2007 では Excel12 および Excel12v が追加されています。

XLL は、Excel のアドイン マネージャによって読み込まれて管理されます。このマネージャは、次の XLL エクスポートを検索します。

  • xlAutoOpen: XLL の読み込み時に呼び出されます。XLL の関数およびコマンドを登録し、データ構造を初期化し、ユーザー インターフェイスをカスタマイズするための最適な要素です。
  • xlAutoClose: XLL の読み込み解除時に呼び出されます。関数とコマンドの登録を解除し、リソースを解放し、カスタマイズを元に戻します。
  • xlAutoAdd: セッション中に XLL のアクティブ化 (読み込み) が行われたとき呼び出されます。
  • xlAutoRemove: セッション中に XLL が非アクティブ化 (読み込み解除) されたとき呼び出されます。
  • xlAddInManagerInfo (xlAddInManagerInfo12): アドイン マネージャが初めて起動されたとき呼び出されます。引数として 1 が渡された場合には文字列 (アドイン名) を返します。それ以外の場合は #VALUE! を返します。
  • xlAutoRegister (xlAutoRegister12): 関数の戻り値および引数の型を指定せずに REGISTER (XLM) または xlfRegister (C API) を呼び出した場合に呼び出されます。XLL を内部的に検索して、検索された情報と共に関数を登録します。
  • xlAutoFree (xlAutoFree12): XLL が解放する必要のあるメモリをポイントする XLOPER フラグが Excel に返された場合に呼び出されます。

後ろから 3 つの関数は、XLOPER を受け取るかまたは返します。Excel 2007 では、これらの関数は XLOPER および XLOPER12 の両方でサポートされます。

xlAutoOpen のみが必須の関数となります。この関数がないと XLL を読み込めません。DLL 内部でメモリなどのリソースを割り当てる場合は、メモリ リークを防ぐために xlFree (xlFree12) を実装する必要があり、また、XLL を読み込み解除する場合は、xlAutoClose を実装してクリーンアップを行う必要がありますが、これらに該当しない場合は、いずれも省略可能です。

Excel はいくつかの標準的な C データ型を使用してデータを交換し、ライブラリ関数は C で名前装飾されてるので、C API と呼ばれます。また、データ構造は ANSI C です。 相応の経験があれば、C++ を使用することで、XLL プロジェクトの管理性、読みやすさ、安定性を向上させることができます。 したがって、この記事では以降、C++ クラスについての基本事項を理解していることを前提に説明を進めます。

表 1. には、Excel が XLL とのデータ交換に使用するデータ構造がまとめられています。ワークシートの UDF を登録する際、xlfRegister の 3 番目の引数に使用する型文字も示されています。

表 1. XLL とのデータ交換に使用される Excel データ構造

データ型 渡される値 渡される参照 (ポインタ) コメント
Boolean A L short (0=false または 1=true)
Double B E  
char *   C、F ヌルで終わる ASCII バイト文字列
unsigned char *   D、G カウント付き ASCII バイト文字列
[v12+] unsigned short *   C%、F% ヌルで終わる Unicode wide-char 文字列
[v12+] unsigned short *   D%、G% カウント付き Unicode wide-char 文字列
unsigned short [int] H   DWORD、size_t、wchar_t
[signed] short [int] I M 16 ビット
[signed long] int J N 32 ビット
FP   K 浮動小数点配列構造
[v12+] FP12   K% 大きなグリッドの浮動小数点配列構造
XLOPER   P 可変型のワークシートの値および配列
    R 値、配列、および範囲の参照
[v12+] XLOPER12   Q 可変型のワークシートの値および配列
    U 値、配列、および範囲の参照

型 C%、F%、D%、G%、K%、Q、U は、いずれも Excel 2007 の新しい型であり、以前のバージョンではサポートされていません。文字列型 F、F%、G、G% は、その場所で書き換えられる引数に使用します。XLOPERXLOPER12 の UDF 関数の引数が、それぞれ P 型、Q 型として登録された場合、Excel ではこれらの引数を用意する際、単一セル参照を単純な値へ、複数セル参照を配列へ、変換します。P 型および Q 型は常に、xltypeNumxltypeStrxltypeBoolxltypeErrxltypeMultixltypeMissing、または xltypeNil のいずれかの型として関数に渡されます。常に参照が解除される、xltypeRefxltypeSRef としては渡されません。

xlfRegister の 3 番目の引数 type_text は、上記コードの文字列となります。 この文字列に接尾辞としてナンバー記号 (#) を付け、関数がマクロ シートに対応することを示すこともできます。 また、この文字列に接尾辞として感嘆符 (!) を付け、関数がマクロ シートに対応することや、一時的なものとして取り扱うべきであることを示すこともできます。 関数をマクロ シート対応として宣言すると、その関数は再計算されないセル (呼び出し元のセルの現在値など) の値を取得できるようになり、XLM 情報関数を呼び出すことができます。 # として、かつ、R 型または U 型の引数を取るものとして登録された関数は、既定で一時的なものとなります。

Excel 2007 では、ドル記号 ($) を追加することにより、関数がスレッド セーフであることを示すことも可能になっていますが、マクロ シート関数はスレッド セーフとは見なされません。したがって、型文字が関数型の文字列である場合、# 記号と $ 記号の両方を追加することはできません。XLL で # と $ の両方を使った関数を登録しようとすると、エラーになります。

Excel 2007 における XLL の変更

Excel 2007 は、以前のバージョン用に作成されたアドインを読み込んで実行します。 すべての XLL が Excel 2007 で意図したように動作するとは限りません。 ある XLL が Excel 2007 と完全に互換であるためには、その前に複数の問題に対処する必要があります。 ここでは、無効になっている可能性のある、いくつかの暗黙的または明示的な前提条件について考察します。

新しい構造では 2 つの大きな変更が行われました。そのうちの 1 つは、より大きなグリッドの導入です。このグリッドの行および列は、次の 2 つの新しいデータ型定義によってカウントされます。

typedef INT32 RW;        /* XL 12 Row */
typedef INT32 COL;        /* XL 12 Column */

新しい XLOPER12 構造および FP12 構造で使用される、これらの符号付き 32 ビット整数は、XLOPER 範囲で使用される WORD 行と BYTE 列、および XLOPER 配列と FP データ構造で使用される WORD 行に代わるものです。 もう 1 つの大きな変更は、Unicode 文字列が XLL でサポートされるようになった点です。 XLOPER12 は、RW 型および COL 型を含む XLOPER で、ASCII バイト文字列の代わりに Unicode 文字列を使用しています。

新しいワークシート関数

Analysis Toolpak (ATP) 関数が Excel 2007 に組み込まれるようになりました。XLL は従来、xlUDF を使用して ATP アドイン関数を呼び出していました。Excel 2007 では、このような呼び出しを、xlfPrice などで置き換える必要があります。Excel 2007 を実行する場合にのみ呼び出せる、多くの新しいワークシート関数もあります。これらの関数を以前のバージョンで呼び出すと、C API により xlretInvXlfn が返されます。詳細については、「クロス バージョン XLL の作成」を参照してください。

文字列

Excel 2007 では、最大 32,767 (215-1) 文字の Unicode のワイド文字列に、初めて XLL から直接アクセスできるようになりました。 ワークシート内のこれらの文字列は、現時点では複数のバージョンでサポートされています。 ASCII の 255 バイト以内の文字列という制限のあった従来の C API と比べて、これは著しい向上です。 C、D、F、G 引数型と XLOPER の xltypeStr を通じて、長さ制限のあるバイト文字列も引き続きサポートされます。

Microsoft Windows では、バイト文字列と Unicode 文字列の変換は、ロケール依存です。つまり、255 バイト文字とワイド Unicode 文字間の変換は、システムのロケール設定に応じて処理されます。Unicode 標準では各コードに一意の文字が割り当てられますが、拡張 ASCII コードではそうではありません。このロケール固有の変換については注意が必要です。たとえば、2 つの異なる Unicode 文字列が、バイト文字列への変換後、等しいものとして照合される可能性があります。

Excel 2007 では、ユーザーに対して表示されるすべての文字列は、通常、内部的には Unicode で表記されます。 したがって、Excel 2007 で文字列を交換するには、これを使用するのが最も効率的です。 以前のバージョンでは、バイト文字列は C API を介して操作する場合にのみアクセスすることが可能ですが、ワイド Unicode 文字列は文字列 Variant を介して操作することができます。 これらは VBA から、または Excel 2007 COM インターフェイスを使用して、DLL または XLL に渡すことができます。 Excel 2007 を実行するときは、可能な限り Unicode 文字列で作業をしてください。

Excel の C API で使用可能な文字列型

表 2. に、C API xltypeStr の XLOPER を示します。

表 2. C API xltypeStr の XLOPER

バイト文字列: XLOPER wide-char 文字列: XLOPER12
Excel の全バージョン Excel 2007 以降のみ
最大長: 255 拡張 ASCII バイト 最大長: 32,767 Unicode 文字
最初の (符号なし) バイト: 長さ 最初のワイド文字: 長さ

**重要:   **ヌル終了を前提にしないでください。

表 3. に、C/C++ の文字列を示します。

表 3. C/C++ の文字列

バイト文字列 wide-char 文字列
ヌル終了 (char *) "C" 最大長: 255 拡張 ASCII バイト ヌル終了 (wchar_t *) "C%" 最大長: 32,767 Unicode 文字
長さのカウント (unsigned char *) "D" 長さのカウント (wchar_t *) "D%"
ある文字列型から別の文字列型への変換

XLL に新しい文字列型を追加すると、バイト文字列からワイド文字へ、または wide-char 文字列からバイト文字への変換が必要になる可能性があります。

文字列をコピーするときは、コピー元の文字列が、コピー先の文字列バッファの長さを超えないことを確認する必要があります。この場合、エラーにするか、それとも切り捨てるかは、実装上の問題となります。表 4. に、変換およびコピー用のライブラリ ルーチンを示します。

表 4. 変換およびコピー用のライブラリ ルーチン

Aa730920.aa730920_fig1(ja-jp,office.12).gif

表 4. に示すライブラリ関数はすべて (最大) 文字列長の引数を取る点に注意してください。 Excel で制限されるバッファのオーバーランを回避するために、この引数を常に指定する必要があります。

次の事項について検討してください。

  • [signed] char * として宣言される、長さカウントのあるバイト文字列を使用するときは、128 文字以上の文字列による問題を避けるために、長さを BYTE にキャストします。
  • 最大長のヌル終了文字列を、長さカウントのある文字列バッファにコピーするときは、ヌル終了文字をコピーしないでください。バッファのオーバーランが発生する可能性があるためです。
  • ヌル終了文字列に新しいメモリを割り当てるときは、ヌル終了でスペースを割り当ててください。
  • 長さカウントのある文字列をヌル終了文字列にコピーするときは、ヌル終了を明示的に設定してください。ただし、lstrcpynA() のように、この処理を常に自動的に実行する API 関数を使用する場合は除きます。
  • 処理速度が重要で、コピーするバイト数がわかっている場合は、strcpy()strncpy()wcscpy()、または wcsncpy() の代わりに、memcpy() を使用します。

次の関数はそれぞれ、長さカウント付きバイト文字列をヌル終了の C バイト文字列に、また、ヌル終了の C バイト文字列を長さカウント付きバイト文字列に、安全に変換します。最初の関数では十分に大きなターゲット バッファ、2 番目の関数では最大 256 バイトのバッファ (長さバイトも含む) が渡されていることが前提です。

char *BcountToC(char *cStr, const char *bcntStr)
{
    BYTE cs_len = (BYTE)bcntStr[0];
    if(cs_len) memcpy(cStr, bcntStr+1, cs_len);
    cStr[cs_len] = '\0';
    return cStr;
}
#define MAX_XL4_STR_LEN 255u
char *CToBcount(char *bcntStr, const char *cStr)
{
    size_t cs_len = strlen(cStr);
    if(cs_len > MAX_XL4_STR_LEN) cs_len = MAX_XL4_STR_LEN;
    memcpy(bcntStr+1, cStr, cs_len);
    bcntStr[0] = (BYTE)cs_len;
    return bcntStr;
}

大きなグリッドによるコードへの影響

Microsoft Office Excel 2003 では、単一ブロック範囲の最大サイズは 224 セルです。これは 32 ビット整数の制限内に十分に収まります。Excel 2007 では、234 セルが上限となっています。すべてのアプリケーションに課せられる 2 ギガバイトのメモリ制限には、約 228 セルの倍精度数の単純な配列で達するので、xltypeMulti 配列、FP または FP12 のサイズは、符号付き 32 ビット整数を使用して記録するのが安全です。ただし、Excel 2007 グリッド全体など、非常に大きな範囲のサイズを安全に取得するには、__int64、INT64 などの 64 ビット整数型が必要です。

範囲名

Excel 2007 では、有効な範囲名とそうでない範囲名を区別するルールが変更されています。最大の列は XFD になっています。たとえば、計算式 =RNG1 は、ブックが互換モードで動作している場合 (つまり Excel 2003 形式のブックを開いている場合) を除いて、371 番目の列の最初のセルの参照として解釈されます。ユーザーが 2003 のブックを Excel 2007 形式で保存すると、Excel は RNG1 などの名前を _RNG1 に再定義して、変更をユーザーに通知します。VBA コード内を除き、すべてのブックが置き換えられ、そのコードおよび他のブック内での外部参照が壊される結果となります。このような名前を作成、検証、または検索するコードをチェックする必要があります。どちらの保存形式でもコードが機能するように修正する方法の 1 つは、RNG1 が定義されていない場合に _RNG1 がないかどうかを調べることです。

大きな配列とメモリ不足に関する条件の処理

Excel 2007 では、強制された XLOPER 範囲型である XLOPER 配列 (xltypeMulti) のサイズは、行および列の数やカウントに使用される整数の幅ではなく、Excel の 32 ビット アドレス空間によって制限されます。Excel 2007 ではグリッドがはるかに大きくなっているため、このメモリ制限に達しやすくなります。xlCoerce を使用してコード内で範囲参照を明示的に指定しようとした場合、あるいは、エクスポートした関数 XLOPER の引数を P 型として、または XLOPER12 の引数を Q 型として登録することにより暗示的に指定しようとした場合、範囲がメモリの空き容量と比べて大きすぎると、エラーになります。前者の場合、Excel4 または Excel12 の呼び出しは、xlretFailed でエラーになります。後者の場合、Excel は呼び出し元のセルに #VALUE! を返します。

パフォーマンス上の理由から、アドインに対してユーザーが非常に大きな配列を渡すおそれがある場合は、その配列のサイズを検出し、一定の限界を超えている場合はリジェクトするか、または xlAbort を使用してワークシート関数のユーザー ブレークを可能にする必要があります。

大きな範囲参照および再計算の処理

Excel 2003 では、1 回の参照でポイントできるのは、ワークシート上の最大 224 セルでした。これらのごく一部分しか処理しない場合にも、ユーザー側ではコンピュータが実質的にハングする可能性があります。したがって、範囲のサイズをチェックし、より小さい部分に分割して処理すべきかどうかを判断する必要があり、大きな配列を処理する場合と同様に xlAbort を使用してユーザー ブレークを検出する必要があります。Excel 2007 では、範囲がおよそ 1,000 倍になる場合もあるので、入念なチェックがさらに重要です。Excel 2003 ではワークシート全体を処理するのに 1 秒しかかからないコマンドでも、Excel 2007 では、他の条件がすべて同一の状態で、15 分以上かかる場合があります。

関数の引数の増加

Excel 2007 では、XLL が最大 255 個の引数を使用する関数をエクスポートできます。 それぞれ固有の意味のある引数を、これほどの数使用する関数は、実際問題として複雑すぎるので、分割する必要があります。 このような数の引数を使用する可能性が高いのは、類似した、かつ可変の個数の入力を処理する関数です。

マルチ スレッドによる再計算

Excel 2007 では、スレッドセーフとして登録したワークシート関数を再計算するために、複数のスレッドを使用するように設定できます。 これにより、マルチプロセッサ コンピュータでは計算時間を短縮できます。一方、シングルプロセッサ コンピュータでは、この機能は UDF を使用してマルチスレッド サーバー上の機能にアクセスする場合に特に有益です。 VBA、COM、および C# アドインと比べた場合の XLL のメリットの 1 つは、関数をスレッドセーフとして登録できる点です。

新しいコマンドとワークシート関数へのアクセス

列挙関数の定義が拡張され、Excel 97 (バージョン9) 以降に追加されたすべてのワークシート関数、および多数の新しいコマンドが含まれるようになりました。新しい関数を表 5. に示します。

表 5. 新しい関数

xlfAccrint xlfCumprinc xlfImlog10 xlfQuotient
xlfAccrintm xlfDec2bin xlfImlog2 xlfRandbetween
xlfAmordegrc xlfDec2hex xlfImpower xlfReceived
xlfAmorlinc xlfDec2oct xlfImproduct xlfRoundbahtdown
xlfAveragea xlfDelta xlfImreal xlfRoundbahtup
xlfAverageif xlfDisc xlfImsin xlfRtd
xlfAverageifs xlfDollarde xlfImsqrt xlfSeriessum
xlfBahttext xlfDollarfr xlfImsub xlfSqrtpi
xlfBesseli xlfDuration xlfImsum xlfStdeva
xlfBesselj xlfEdate xlfIntrate xlfStdevpa
xlfBesselk xlfEffect xlfIseven xlfSumifs
xlfBessely xlfEomonth xlfIsodd xlfTbilleq
xlfBin2dec xlfErf xlfIsthaidigit xlfTbillprice
xlfBin2hex xlfErfc xlfLcm xlfTbillyield
xlfBin2oct xlfFactdouble xlfMaxa xlfThaidayofweek
xlfComplex xlfFvschedule xlfMduration xlfThaidigit
xlfConvert xlfGcd xlfMina xlfThaimonthofyear
xlfCountifs xlfGestep xlfMround xlfThainumsound
xlfCoupdaybs xlfGetpivotdata xlfMultinomial xlfThainumstring
xlfCoupdays xlfHex2bin xlfNetworkdays xlfThaistringlength
xlfCoupdaysnc xlfHex2dec xlfNominal xlfThaiyear
xlfCoupncd xlfHex2oct xlfOct2bin xlfVara
xlfCoupnum xlfHyperlink xlfOct2dec xlfVarpa
xlfCouppcd xlfIferror xlfOct2hex xlfViewGet
xlfCubekpimember xlfImabs xlfOddfprice xlfWeeknum
xlfCubemember xlfImaginary xlfOddfyield xlfWorkday
xlfCubememberproperty xlfImargument xlfOddlprice xlfXirr
xlfCuberankedmember xlfImconjugate xlfOddlyield xlfXnpv
xlfCubeset xlfImcos xlfPhonetic xlfYearfrac
xlfCubesetcount xlfImdiv xlfPrice xlfYield
xlfCubevalue xlfImexp xlfPricedisc xlfYielddisc
xlfCumipmt xlfImln xlfPricemat xlfYieldmat

新しいコマンドを表 6. に示します。

表 6. 新しいコマンド

 xlcActivateNotes   xlcInsertdatatable   xlcOptionsSettings   xlcUnprotectRevisions 
 xlcAddPrintArea   xlcInsertMapObject   xlcOptionsSpell   xlcVbaactivate 
 xlcAutocorrect   xlcLayout   xlcPicklist   xlcViewDefine 
 xlcClearPrintArea   xlcMoveBrk   xlcPivotTableChart   xlcViewDelete 
 xlcDeleteNote   xlcNewwebquery   xlcPostDocument   xlcViewShow 
 xlcEditodc   xlcNormal   xlcProtectRevisions   xlcWebPublish 
 xlcHideallInkannots   xlcOptionsMe   xlcRmPrintArea   xlcWorkgroupOptions 
 xlcHideallNotes   xlcOptionsMenono   xlcSheetBackground   
 xlcHidecurrNote   xlcOptionsSave   xlcTraverseNotes   

表 7. に示すコマンドは廃止されました。

表 7. 廃止されたコマンド

xlcStart xlcVbaObjectBrowser
xlcVbaAddWatch xlcVbaReferences
xlcVbaClearBreakpoints xlcVbaReset
xlcVbaDebugWindow xlcVbaStepInto
xlcVbaEditWatch xlcVbaStepOver
xlcVbaEnd xlcVbaToggleBreakpoint
xlcVbaInstantWatch  

C API 関数: Excel4、Excel4v、Excel12、Excel12v

経験豊富な XLL 開発者は、次のような従来の C API 関数を使い慣れていることでしょう。

int _cdecl Excel4(int xlfn, LPXLOPER operRes, int count,... ); 
/* followed by count LPXLOPERs */
int pascal Excel4v(int xlfn, LPXLOPER operRes, int count, LPXLOPER opers[]);

Excel 2007の新しい SDK には、動作は同じでも XLOPER12 引数を取る 2 つの C API 関数の定義を含む、次のようなソース コード モジュールが追加で含まれています。これらの関数は、Excel の以前のバージョンで呼び出すと、xlretFailed が返されます。

int _cdecl Excel12(int xlfn, LPXLOPER12 operRes, int count,... ); 
/* followed by count LPXLOPER12s */
int pascal Excel12v(int xlfn, LPXLOPER12 operRes, int count, LPXLOPER12 opers[]);

これらの Excel12Excel12v 関数は、以前のバージョンの Excel4Excel4v と同じ成功値およびエラー値を返します。ただし、Excel 2007 では、4 つの関数はいずれも新しいエラー xlretNotThreadSafe (128 として定義) を返す場合があります。このエラーは、スレッドセーフとして登録された関数が、スレッドセーフでない関数 (一般にマクロ シート関数または安全性に欠ける UDF) の呼び出しを試みた場合に返されます。

アドイン マネージャのインターフェイス関数

いくつかの xlAuto- 関数は、XLOPER を取るかまたは返します。これらは Excel 2007 でも所定の機能を実行しますが、XLOPER12 バージョンも認識されるようになっています。影響を受ける関数を次に示します。

xloper * __stdcall xlAutoRegister(xloper *p_name);
xloper * __stdcall xlAddInManagerInfo(xloper *p_arg);

新しい関数は次のとおりです。

xloper12 * __stdcall xlAutoRegister12(xloper12 *p_name);
xloper12 * __stdcall xlAddInManagerInfo12(xloper12 *p_arg);

これら 4 つの関数はいずれも必須ではありません。該当する関数が存在しない場合、Excel は既定の動作を使用します。Excel 2007 では XLOPER12 バージョンが存在すれば、それらは XLOPER バージョンに優先して呼び出されます。2 種類のバージョンの XLL を作成するときは、バージョン間で機能を同等にする必要があります。

浮動小数点行列型

次の構造 (K 型として登録される) は、Excel の多くのバージョンでサポートされています。この構造は Excel 2007 の xlcall.h SDK ヘッダー ファイルで定義されています。

typedef struct
{
    WORD rows;
    WORD columns;
    double array[1]; // Start of array[rows * columns]
}
    FP;

次に、問題を引き起こす可能性のあるコンテキストの例を示します。Excel 2007 より前のバージョンでは、次のコードはスタイルが拙劣とはいえ、安全であると見なすことができました。

// Unsafe function: returns the offset (from 0) of the column with max sum.
int __stdcall max_column_index(FP *pArray)
{
    int c, columns = pArray->columns;
    int r, rows = pArray->rows;
    double *p, column_sums[256]; // Explicit assumption!!!

    for(c = 0; c < columns; c++)
        column_sums[c] = 0.0; // Overrun possible if columns > 256!!!

    for(r = 0, p = pArray->array; r < rows; r++)
        for(c = 0; c < columns; c++)
            column_sums[c] += *p++; // overrun possible!!!

    int max_index = 0;
    for(c = 1; c < columns; c++)
        if(column_sums[c] > column_sums[max_index]) // overrun!!!
            max_index = c;
    return max_index;
}

FP 型は新しいデータ型ではありませんが、Excel 2007 ではこの構造は新しいグリッドの幅全体 (214 列) に広がる配列に対応できます。ただし、最大 216 行で切り捨てられます。これに対する修正は非常に簡単です。次のように、常に適切なサイズとなるようにバッファを動的に割り当てます。

    double *column_sums = new double[columns];
// ...
    delete[] column_sums;
    return max_index;

216 行を超える行を渡すことを可能にするため、Excel 2007 では、K% として登録される次のような新しいデータ型もサポートしています。

typedef struct
{
    RW rows;
    COLS columns;
    double array[1]; // Start of array[rows * columns]
}
    FP12;

XLCallVer

XLCallVer 関数のシグニチャは次のとおりです。

int pascal XLCallVer(void);

Microsoft Excel 97 から Excel 2003 までは、XLCallVer は 1280 = 0x0500 = 5*256 を返しますが、これは Excel バージョン 5 (C API に変更が加えられた最後のバージョン) を示します。Excel 2007 では、この関数は 0x0C00 を返し、バージョン 12 を示します。

これを利用すると、実行時に新しい C API を使用できるかどうかを判別できますが、実行中の Excel のバージョンを検出する場合、Excel4(xlfGetWorkspace, &version, 1, &arg) を使用する方が適切であるといえます。ここで arg は 2 に設定された数値 XLOPERであり、version は文字列 XLOPER です (その後強制的に整数に設定可能)。これは、Microsoft Excel 2000、Microsoft Excel 2002、および Excel 2003 にもそれぞれ違いがあり、その違いをアドインで検出しなければならない場合があるためです。たとえば、ある統計関数の精度が変更されており、その差を検出しなければならないというケースなどが考えられます。

バージョンによって動作が異なる C API 関数

一般にワークシート関数は、数値関数で精度の向上が見られる場合を除き、どのバージョンで実行しても動作は変わりません。ただし、C API 専用の関数の場合、Excel 2007 では次に説明する 3 つの関数は以前のバージョンと動作が異なります。

xlStack

この関数は、実際のスタック領域または 64 KB のいずれか少ない方を返すようになりました。次のコード例は、どのバージョンでもスタック領域を取得するための方法を示しています。

double __stdcall get_stack(void)
{
    if(gExcelVersion12plus)
    {
        xloper12 retval;
        if(xlretSuccess != Excel12(xlStack, &retval, 0))
            return -1.0;

        if(retval.xltype == xltypeInt)
            return (double)retval.val.w; // returns min(64Kb, actual space)
// This is not the returned type, but was returned in
// an Excel 12 beta, so the code is left here.
        if(retval.xltype == xltypeNum)
            return retval.val.num;
    }
    else
    {
        xloper retval;
        if(xlretSuccess != Excel4(xlStack, &retval, 0))
            return -1.0;

        if(retval.xltype == xltypeInt)
            return (double)(unsigned short)retval.val.w;
    }
    return -1.0;
}
xlGetHwnd

Excel 2003 では、xlGetHwnd は 2 バイトの短精度数 (Excel 用の完全な Windows HWND ハンドルの下位部分) を含む xltypeInt XLOPER を返します。 完全なハンドルを取得するには、次のセクションに示すように Windows API EnumWindows を使用する必要があります。 Excel 2007 では、Excel12 を使用して呼び出しを行った場合、返される xltypeInt XLOPER12 には、4 バイトの符号付き整数 (完全なハンドル) が含まれます。 Excel 2007 で呼び出した場合にも、Excel4 はハンドルの下位部分しか返さない点に注意してください。

HWND get_xl_main_handle(void)
{
    if(gExcelVersion12plus) // xlGetHwnd returns full handle
    {
        xloper12 main_xl_handle;
        if(Excel12(xlGetHwnd, &main_xl_handle, 0) != xlretSuccess)
            return 0;
        return (HWND)main_xl_handle.val.w;
    }
    else // xlGetHwnd returns low handle only
    {
        xloper main_xl_handle;
        if(Excel4(xlGetHwnd, &main_xl_handle, 0) != xlretSuccess)
            return 0;
        get_hwnd_enum_struct eproc_param = {main_xl_handle.val.w, 0};
        EnumWindows((WNDENUMPROC)get_hwnd_enum_proc, (LPARAM)&eproc_param);
        return eproc_param.full_handle;
    }
}

#define CLASS_NAME_BUFFER_SIZE    50

typedef struct
{
    short main_xl_handle;
    HWND full_handle;
}
    get_hwnd_struct;

// The callback function called by Windows for every top-level window
BOOL __stdcall get_hwnd_enum_proc(HWND hwnd, get_hwnd_struct *p_enum)
{
// Check if the low word of the handle matches Excel's
    if(LOWORD((DWORD)hwnd) != p_enum->main_xl_handle)
        return TRUE; // keep iterating

    char class_name[CLASS_NAME_BUFFER_SIZE + 1];
// Ensure that class_name is always null terminated
    class_name[CLASS_NAME_BUFFER_SIZE] = 0;
    GetClassName(hwnd, class_name, CLASS_NAME_BUFFER_SIZE);
// Do a case-insensitive comparison for the Excel main window class name
    if(_stricmp(class_name, "xlmain") == 0)
    {
        p_enum->full_handle = hwnd;
        return FALSE; // Tells Windows to stop iterating
    }
    return TRUE; // Tells Windows to continue iterating
}
xlGetInst

xlGetHwnd と同様、Excel12 を使って呼び出された場合、返される xltypeInt XLOPER12 には完全な実行インスタンス ハンドルが含まれます。一方、Excel4 は、ハンドルの下位部分のみを含む xltypeInt XLOPER を返します。

ユーザー インターフェイスのカスタマイズ

Excel の以前のバージョンでは、xlcAddBarxlcAddMenuxlcAddCommandxlcShowBarxlcAddToolbarxlcAddToolxlcShowToolbar などのコマンドを使用して、XLL コードでメニュー バー、メニュー、コマンド バー、ツール バーをカスタマイズすることができました。これらのコマンドは引き続きサポートされていますが、従来のメニュー バーやコマンド バーの構造が置き換えられ、リボンのアドイン グループの XLL コマンドにアクセスするようになっているので、ユーザーに対して意図したとおりのインターフェイスを提供できない場合があります。

2007 Microsoft Office リリースの UI のカスタマイズは、マネージ コードによってのみ可能です。Excel 2007 で UI をカスタマイズする 1 つの方法は、UI をカスタマイズする関数を個別のマネージ コード リソースまたはアドインに組み込むことです。そのリソースを XLL に緊密に連結させ、XLL コードをコールバックして、中に含まれているコマンドや関数を起動します。

クロス バージョン XLL の作成

ここでは、Excel の複数のバージョン間で互換性を持つ XLL の作成方法を説明します。

便利な定数の定義

次のような定義を XLL プロジェクト コードに組み込み、このコンテキストで使用されるすべてのリテラルな数値のインスタンスを置き換えることを検討してください。そうすることにより、バージョン固有のコードが大幅に明確化され、一見無害な数値という形で現れるバージョン関連の不具合の可能性を抑えることができます。

#define MAX_XL11_ROWS        65536
#define MAX_XL11_COLS        256
#define MAX_XL12_ROWS        1048576
#define MAX_XL12_COLS        16384
#define MAX_XL11_UDF_ARGS    30
#define MAX_XL12_UDF_ARGS    255
#define MAX_XL4_STR_LEN        255u
#define MAX_XL12_STR_LEN    32767u

実行中のバージョンの取得

実行中のバージョン を取得するには、Excel4(xlfGetWorkspace, & version , 1, & arg )を使用する必要があります。ここで arg は 2 に設定された数値型の XLOPER であり、version は文字列 XLOPER です。文字列は、その後強制的に整数に設定することが可能です。これは Excel 2007 の場合 12 です。この操作は xlAutoOpen から実行する必要があります。XLCallVer を呼び出すこともできますが、その場合は 2007 より前のどのバージョンが実行されているかは示されません。

xlcall32 ライブラリおよび C API 関数へのリンク

Excel 97 SDK および Framework プロジェクトに基づいて、xlcall32 ライブラリにリンクするための標準的な方法は、プロジェクト内の参照を xlcall32.lib インポート ライブラリに含めることです (その他の方法としては、LoadLibrary および GetProcAddress を使用して実行時に xlcall32.dll に明示的にリンクすることが可能です)。この方法で作成したプロジェクトの場合、コンパイル時にライブラリがリンクされ、標準的な方法でエクスポートがプロトタイプ化されます。 次に例を示します。

#ifdef __cplusplus
extern "C" {
#endif
int _cdecl Excel4(int xlfn, LPXLOPER operRes, int count,... );
//...
#ifdef __cplusplus
} // extern "C" block end
#endif

実行時、XLL が Excel によって読み込まれると、xlcall32.dll に暗黙的にリンクされます。

以前のバージョンのインポート ライブラリを使用して作成された (コンパイル時にリンクされる) アドインは、Excel 2007 で動作しますが、Excel12 および Excel12v コールバックにアクセスすることはできません。これらの関数が定義されていないためです。 Excel 2007 SDK バージョンの xlcall.h および C++ ソース ファイル [name?].cpp を使用して、Excel 2007 バージョンの xlcall32.lib にリンクされるコードは、Excel 2007 の全バージョンにおいて、上記の関数を安全に呼び出すことができます。 Excel 2007 より前のバージョンから呼び出しを行った場合は、xlretFailed が返されるだけです。 これは単なるフェール セーフ機構にすぎないため、コードが実行中のバージョンを確実に認識し、適切なコールバックを呼び出すようにする必要があります。

デュアル インターフェイスをエクスポートするアドインの作成

1 つの文字列を取り、任意のワークシート データ型または範囲となる可能性のある 1 つの引数を返すXLL 関数について考察してみましょう。Excel 2003 および Excel 2007 では、RD 型として登録され、次のようにプロトタイプ化された関数をエクスポートできます。ここで、文字列は長さカウント付きバイト文字列として渡されます。

xloper * __stdcall my_xll_fn(unsigned char *arg);

ここで考慮すべき点について述べます。まず、このコードは Excel 2007 の全バージョンで正常に動作しますが、従来の C API 文字列の制約を受けます。次に、Excel 2007 は XLOPER の交換が可能であるとはいえ、内部的にこれらを XLOPER12 に変換するので、Excel 2003 でこのコードを実行するときには発生しなかった変換のためのオーバーヘッドが暗黙的に発生します。さらに、この関数はスレッドセーフにされることがあるかもしれませんが、型の文字列を RD$ に変更すると、Excel 2003 で登録に失敗します。 以上の理由から、Excel 2007 ユーザーに対しては、UD%$ として登録され、次のようにプロトタイプ化された関数をエクスポートするのが理想的です。

xloper12 * __stdcall my_xll_fn_v12(wchar_t *arg);

Excel 2007 を実行するとき、別の関数を登録することが望ましいもう 1 つの理由は、XLL 関数が最大 255 個の引数を取れることです (従来は 30 個に制限されていました)。 幸いにも、プロジェクトから両方のバージョンをエクスポートすれば、両方のメリットを享受できます。 エクスポートを行った後で、実行中の Excel バージョンを検出して、最も適切な関数を条件付きで登録します。

プロジェクトで XLL のエクスポートを登録するときに渡されるデータは、さまざまな方法で管理することができます。

簡単な方法の 1 つは、次に示すコードの ws_func_export_data のようなデータ構造を定義した後、 ws_func_export_data の配列を宣言および初期化し、XLL コード内でこれを使用して、xlfRegister に渡される XLOPER または XLOPER12 を初期化することです。 次に例を示します。

#define XL12_UDF_ARG_LIMIT    255
typedef struct
{
// REQUIRED (if v12+ strings undefined use v11- strings):
    char *name_in_code;    // RegArg2: Function name as in code (v11-)
    char *types;        // RegArg3: Return type and argument types (v11-)
    char *name_in_code12;        // RegArg2: Function name as in code (v12+)
    char *types12;        // RegArg3: Return type and argument types (v12+)
    char *ws_name;        // RegArg4: Fn name as it appears on worksheet
    char *arg_names;        // RegArg5: Argument names (Excel 11-: max 30)
    char *arg_names12;        // RegArg5: Argument names (Excel 12+: max 64)
// OPTIONAL:
    char *fn_category;        // RegArg7: Function category for Function Wizard
    char *help_file;        // RegArg9: Help file (optional)
    char *fn_description;    // RegArg10: Function description text (optional)
    char *arg_help[MAX_XL12_UDF_ARGS - 11]; // RegArg11...: Arg help text
}
    ws_func_export_data;

登録関数がこのデータにどのような処理を実行するとしても、提供されるワークシート関数名は 1 つだけなので、ワークシートは、どの関数が呼び出されているかを認識しない (認識する必要がない) 点に注目してください。次に、標準的なライブラリ関数を呼び出して、ワークシート文字列を逆にする関数の例を示します。

// Excel 11-:  Register as type "1F"
void __stdcall reverse_text_xl4(char *text) {strrev(text);}

// Excel 12+:  Register as type "1F%$" (if linking with thread-safe library)
void __stdcall reverse_text_xl12(wchar_t *text) {wcsrev(text);}

この関数の構造は、次のように初期化することができます。

ws_func_export_data WsFuncExports[1] =
{
    {
        "reverse_text_xl4",
        "1F",
        "reverse_text_xl12",
        "1F%$",
        "Reverse",
        "Text",
        "", // arg_names12
        "Text", // function category
        "", // help file
        "Reverse text",
        "Text ",
    },
};

上記の文字列は、ヌル終了のバイト文字列です。これらを使用して XLOPER を初期化するコードでは、最初に長さカウント付き文字列への変換を行う必要があります。XLOPER12 の初期化に使用する場合には、バイトから Unicode への変換も必要です。別の方法として、先頭のスペース付きでこれらの文字列を初期化し、別のコードでそれを文字列の長さで上書きすることも可能です。ただし、デバッグ モードで動作するいくつかのコンパイラでは、この方法によって問題が生じることがあります。Excel 2007 で実行する場合、Unicode 文字列を Excel に渡すように上記の構造定義を修正することは簡単にできます。その際は、該当する構造を使用するコードも修正する必要があります。

このアプローチでは、同じワークシートを実行した場合に、Excel 2003 と Excel 2007 とで異なる結果が表示される可能性があります。たとえば、Excel 2003 は Excel 2003 ワークシート セル内の Unicode 文字列を ASCII バイト文字列にマッピングし、切り捨ててから XLL 関数呼び出しに渡します。 Excel 2007 は、適切な方法で登録された XLL 関数に、変換されていない Unicode 文字列を渡します。 これにより、異なる結果が返される可能性が生じます。こうした可能性とユーザーへの影響について注意する必要があるのは、Excel 2007 へのアップグレードの場合に限りません。たとえば、Excel 2000 と Excel 2003 との間では、いくつかの組み込みの数値関数で改良が行われています。

共通のコンテナ クラスや構造でのデータ型のラッピング

通常、XLL ワークシート関数は次の動作を実行します。

  • 入力の有効性をチェックする。
  • 入力をそれぞれのコンテキストで解釈する。
  • 入力が無効のとき、個別のエラーを返す。
  • データ構造を設定する。
  • より深い層のコア コードを呼び出す。
  • コア コードからの戻り値を処理し、Excel に対して適切なデータを返す。

ここで説明してきたように、2 つのエクスポートされるインターフェイスを提供する場合、これらのロジックをすべて複製するのは、適切であるとは言えません。しかし、引数のデータ型がすべて異なる場合、どのような選択肢があるでしょうか。 その答えは、Excel のデータ型を共通のコンテナにラッピングすることです。 さまざまなアプローチが考えられますが、ここでは XLOPER および XLOPER12 のみを対象として説明します。以下に説明する解決策では、XLOPER および XLOPER12 を認識するC++ クラスを作成します (この後の例では、C++ クラスには双方のインスタンスが含まれています)。 次の例に示す C++ クラス cpp_xloper については、この記事の最後で紹介している 2 番目の著作に詳しい解説があります。

このクラスには、提供された XLOPERXLOPER12 の初期化子のシャロー コピーを既定で作成するコンストラクタを含めるようにします (ここでは、読み取り専用の UDF 引数の解釈を迅速化するために、シャロー コピーにします)。 また、クラスでは、型と値の抽出および変更を可能にするアクセサ関数も提供する必要があります。 これにより、エクスポートされた関数では XLOPER や XLOPER12 の引数をこの共通の型に変換し、実際のタスクを実行する共通の関数を呼び出し、その関数の戻り値を処理するだけで済むようになります。 次に、クラス cpp_xloper を使用する例を示します。

xloper * __stdcall my_function_xl4(xloper *arg)
{
    cpp_xloper Arg(arg); // constructor makes shallow copy
    cpp_xloper RetVal; // passed by ref to retrieve return value
    my_core_function(RetVal, Arg);
    return RetVal.ExtractXloper();
}

xloper12 * __stdcall my_function_xl12(xloper12 *arg)
{
    cpp_xloper Arg(arg); // constructor makes shallow copy
    cpp_xloper RetVal; // passed by ref to retrieve return value
    my_core_function(RetVal, Arg);
    return RetVal.ExtractXloper12();
}

void my_core_function(cpp_xloper &RetVal, cpp_xloper &Arg)
{
    double d;
    if(Arg.IsMissing() || Arg.IsNil())
        RetVal.SetToError(xlerrValue);
    else if(Arg.IsNum() && (d = (double)Arg) >= 0.0)
        RetVal = sqrt(d); // overloaded assignment operator
    else
        RetVal.SetToError(xlerrNum);
}

ここでは、メソッド ExtractXloperExtractXloper12 が、それぞれスレッドセーフで静的な XLOPERXLOPER12 へのポインタを返し、必要に応じて適切なメモリ フリーのビットを設定していることが前提となっています。

このラッピングによるオーバーヘッドを最小限にするため、コンストラクタでは、シャロー コピーを作成するだけでなく、実行中のバージョンを内部的に認識し、Excel 2007 を実行しているときは、XLOPERXLOPER12 に変換し、Excel4 ではなく Excel12 を呼び出す必要があります。その理由は、Excel 2007 では Excel4 を呼び出すと XLOPER 引数は XLOPER12 に変換され、その戻り値が XLOPER に再変換されるからです。適切な型およびコールバックを使用するようにクラスを設定することにより、呼び出しのたびにこの変換が行われるのを防ぐことができます。

C API 関数のラッピング

前のセクションからの続きとなりますが、my_core_function が C API を介して Excel にコールバックする必要がある場合、cpp_xloperXLOPER または XLOPER12 に戻した後、実行中のバージョンに応じて Excel4 または Excel12 を呼び出さなければなりません。このための解決策の 1 つは、関数 Excel4Excel4vExcel12、および Excel12v を cpp_xloper にクラス メンバ関数としてラッピングすることです。その後、次のように my_core_function を書き直すことができます。

void my_core_function(cpp_xloper &RetVal, cpp_xloper &Arg)
{
    if(!Arg.IsNum() || Arg.Excel(xlfSqrt, 1, &Arg) != xlretSuccess)
        RetVal.SetToError(xlerrValue);
}

ここで、cpp_xloper::Excel は戻り値を直接 Arg に格納します。これを実行し、なおかつこの関数を XLOPERXLOPER12cpp_xloper の各引数で呼び出す柔軟性を実現するには、オーバーロードされるメンバ関数をいくつか作成する必要があります。

int Excel(int xlfn); // not strictly necessary, but simplifies the others
int Excel(int xlfn, int count, const xloper *p_op1, ...);
int Excel(int xlfn, int count, const xloper12 *p_op1, ...);
int Excel(int xlfn, int count, const cpp_xloper *p_op1, ...);
int Excel(int xlfn, int count, const xloper *p_array[]);
int Excel(int xlfn, int count, const xloper12 *p_array[]);
int Excel(int xlfn, int count, const cpp_xloper *p_array[]);

このような関数の可変引数バージョンを呼び出す側では、さまざまな型の引数を混在させないことが前提となっている点に注意してください。さらに、ここでは const が使用されているので、Excel4v および Excel12v の定義に const を追加する必要がある点にも注意してください。

このようなラッパーを実装した後は、該当の C API 関数を直接的に呼び出さないでください。このアプローチにおけるもう 1 つの利点は、戻り値のメモリ管理をクラスに組み込むことができる点です。Excel が文字列を返す場合、そのインスタンスを上書きまたは抹消する前に、xlFree を呼び出すように指示するフラグをクラスで設定できます。また、これらのラッパーに追加のチェック機能を組み込むこともできます。たとえば、カウントが 0 未満ではないかどうか、またはバージョン固有の制限を超えていないかどうかをチェックできます。この場合、次のように追加的なエラーを定義して返すことができます。

#define xlretNotCalled      -1   // C API not called

このような関数の実装例を次に示します。ここで、接頭辞 m_ の付いた変数はクラス メンバ変数です。フラグ m_XLtoFree12 および m_XLtoFree は、xlFree を呼び出してメモリを解放するようにクラスに指示します。m_Op および m_Op12 は、それぞれ XLOPER および XLOPER12 のデータ構造のクラスにおける内部コピーです。

int cpp_xloper::Excel(int xlfn, int count, const cpp_xloper *p_op1, ...)
{
    if(xlfn < 0 || count < 0 || count > (gExcelVersion12plus ?
        MAX_XL12_UDF_ARGS : MAX_XL11_UDF_ARGS))
        return xlretNotCalled;

    if(count == 0 || !p_op1) // default to 0 and NULL if omitted
        return Excel(xlfn); // Call a simpler version of this function

    va_list arg_ptr;
    va_start(arg_ptr, p_op1); // Initialize

    if(gExcelVersion12plus)
    {
        const xloper12 *xloper12_ptr_array[MAX_XL12_UDF_ARGS];
        xloper12_ptr_array[0] = &(p_op1->m_Op12);
        cpp_xloper *p_cpp_op;

        for(int i = 1; i < count; i++) // retrieve as ptrs to cpp_xlopers
        {
            p_cpp_op = va_arg(arg_ptr, cpp_xloper *);
            xloper12_ptr_array[i] = &(p_cpp_op->m_Op12);
        }
        va_end(arg_ptr); // Reset

        xloper12 temp;
        m_ExcelRtnCode = Excel12v(xlfn, &temp, count, xloper12_ptr_array);
        Free();

        if(m_ExcelRtnCode == xlretSuccess)
        {
            m_Op12 = temp; // shallow copy
            m_XLtoFree12 = true;
        }
    }
    else // gExcelVersion < 12
    {
        const xloper *xloper_ptr_array[MAX_XL11_UDF_ARGS];
        xloper_ptr_array[0] = &(p_op1->m_Op);
        cpp_xloper *p_cpp_op;

        for(int i = 1; i < count; i++) // retrieve as ptrs to cpp_xlopers
        {
            p_cpp_op = va_arg(arg_ptr, cpp_xloper *);
            xloper_ptr_array[i] = &(p_cpp_op->m_Op);
        }
        va_end(arg_ptr); // Reset

        xloper temp;
        m_ExcelRtnCode = Excel4v(xlfn, &temp, count, xloper_ptr_array);
        Free();

        if(m_ExcelRtnCode == xlretSuccess)
        {
            m_Op = temp; // shallow copy
            m_XLtoFree = true;
        }
    }
    return m_ExcelRtnCode;
}

新しいワークシート関数と Analysis Toolpak 関数

Excel の以前のバージョンとは異なり、Excel 2007 には Analysis Toolpak (ATP) 関数が組み込まれています。以前は、XLL は次のように xlUDF を使用して ATP 関数を呼び出していました。

double call_ATP_example(void)
{
    xloper settlement, maturity, coupon, yield,
        redepmtion_value, num_coupons, rate_basis, price;

// Initialise the data types to be passed to the ATP function PRICE
    settlement.xltype = maturity.xltype = coupon.xltype =
    yield.xltype = redepmtion_value.xltype =
    num_coupons.xltype = rate_basis.xltype = xltypeNum;

// Set the values to be passed to the ATP function PRICE
    settlement.val.num = 39084.0; // 2-Jan-2007
    maturity.val.num = 46706.0; // 15-Nov-2027
    coupon.val.num = 0.04;
    yield.val.num = 0.05;
    redepmtion_value.val.num = 1.0; // 100% of face value
    num_coupons.val.num = 1.0; // Annual coupons
    rate_basis.val.num = 1.0; // Act/Act

    xloper fn;
    fn.xltype = xltypeStr;
    fn.val.str = "\005" "PRICE";
    if(Excel4(xlUDF, &price, 8, &fn, &settlement, &maturity,
        &coupon, &yield, &redepmtion_value, &num_coupons,
        &rate_basis) != xlretSuccess || price.xltype != xltypeNum)
        return -1.0; // an error value
    return price.val.num;
}

Excel 2007 では、この呼び出しを次のように Excel 内で置き換える必要があります。ここで、gExcelVersion はプロジェクト内でグローバルなスコープを持つ整数変数であり、xlAutoOpen を呼び出すときに初期化されます。

    int xl_ret_val;
    if(gExcelVersion12plus)
    {
        xl_ret_val = Excel4(xlfPrice, &price, 7, &settlement,
            &maturity, &coupon, &yield, &redepmtion_value,
            &num_coupons, &rate_basis);
    }
    else // gExcelVersion < 12
    {
        xloper fn;
        fn.xltype = xltypeStr;
        fn.val.str = "\005" "PRICE";
        xl_ret_val = Excel4(xlUDF, &price, 8, &fn, &settlement,
            &maturity, &coupon, &yield, &redepmtion_value,
            &num_coupons, &rate_basis);
    }
    if(xl_ret_val != xlretSuccess || price.xltype != xltypeNum)
        return -1.0; // an error value
    return price.val.num;

cpp_xloper などのコンテナを使用すると、関数のバージョンへの依存度はさらに低くなり、Excel 2003 と Excel 2007 の両方で有効になります。次に例を示します。

double call_ATP_example_3(void)
{
    cpp_xloper Settlement(39084.0); // 2-Jan-2007
    cpp_xloper Maturity(46706.0); // 15-Nov-2027
    cpp_xloper Price, Coupon(0.04), YTM(0.05);
    cpp_xloper RedepmtionValue(1.0); // 100% of face
    cpp_xloper NumCoupons(1.0); // Annual coupons
    cpp_xloper RateBasis(1.0); // Act/Act
    int xl_ret_val;

    if(gExcelVersion12plus)
    {
        xl_ret_val = Price.Excel(xlfPrice, 7, &Settlement,
            &Maturity, &Coupon, &YTM, &RedepmtionValue,
            &NumCoupons, &RateBasis);
    }
    else
    {
        cpp_xloper Fn("PRICE");
        xl_ret_val = Price.Excel(xlUDF, 8, &Fn, &Settlement,
            &Maturity, &Coupon, &YTM, &RedepmtionValue,
            &NumCoupons, &RateBasis);
    }
    if(xl_ret_val != xlretSuccess || !Price.IsNum())
        return -1.0; // an error value
    return (double)Price;
}

以前のバージョンで新しい C API ワークシート関数の呼び出しを試みると、 xlretInvXlfn エラーが表示されます。

スレッドセーフ XLL とワークシート関数の作成

Excel の以前のバージョンでは、ワークシート計算にはすべて単一のスレッドが使用されていました。 マルチプロセッサ コンピュータ、または複数のスレッドを使用するように明示的に設定されたシングルプロセッサ コンピュータでは、Excel 2007 はメイン スレッドと追加の (単一または複数の) スレッドとの間で、OS がすべてのプロセッサに割り当てる負荷の分散を試みます。 デュアル プロセッサ (またはデュアル コア) コンピュータの場合、ブック内やブック間の依存関係ツリーのトポロジや、スレッドセーフな関数の割合によっては、再計算の時間が最高で半分に短縮化される可能性もあります。

Excel 2007 は 1 つのスレッド (メイン スレッド) を使用して、すべてのコマンド、スレッドセーフでない関数、xlAuto 関数 (xlAutoFree 以外)、COM および VBA 関数を呼び出します。

XLL 開発者がスレッドセーフ関数を作成する場合、次のようなシンプルなルールに従う必要があります。

  • スレッドセーフでない可能性のある他の DLL 内のリソースを呼び出さない。
  • C API または COM を介して、スレッドセーフでない呼び出しを実行しない。
  • 複数のスレッドによって同時に使用される可能性のあるリソースは、クリティカル セクションを使用して保護する。
  • スレッド固有の記憶域にはスレッドローカルのメモリを使用し、関数内の静的変数はスレッドローカルの変数に置き換える。

xlbitDllFree を設定された XLOPER または XLOPER12 が Excel に返されると、同じスレッドで xlAutoFree が呼び出された後、スレッドの他の関数が呼び出されます。 これはスレッドセーフかどうかを問わず、すべての関数に適用され、スレッドローカルの XLOPER が、関連付けられたメモリが解放されないうちに再利用されるリスクを回避します。

スレッドセーフ関数からの C API 関数の呼び出し

VBA および COM のアドイン関数は、スレッドセーフとは見なされません。 C API コマンド (ワークシート関数からの呼び出しが許可されない xlcDefineName など) に加え、スレッドセーフな関数は XLM 情報関数にアクセスできません。 また、スレッドセーフな XLL 関数を、型文字列にナンバー記号 (#) を追加することによってマクロ シートに相当するものとして登録することもできません。 したがって、スレッドセーフな関数では、次の動作を実行できません。

  • 計算されないセル (呼び出し元のセルを含む) の値の読み取り
  • xlfGetCellxlfGetWindowxlfGetWorkbookxlfGetWorkspace などの関数、およびその他の情報関数の呼び出し
  • xlfSetName による XLL 内部名の定義または削除

XLM の例外の 1 つは、スレッドセーフな xlfCaller です。 呼び出し側がワークシート セルまたは範囲である場合、xlfCaller は参照を返します。 ただし、スレッドセーフな関数で xlCoerce を使用して、この参照に値を強制的に設定するのは安全ではありません。結果として、xlretUncalced が返されるためです。 # を使用して関数を登録すれば、この問題が解決されますが、その場合、関数はマクロシートに対応するので、スレッドセーフとは見なされません。 したがって、一定のエラー条件が成立した場合などに前の値を返す関数は、スレッドセーフではありません。

以下に示す C API 専用の関数は、すべてスレッドセーフである点に注意してください。

  • xlCoerce (ただし、計算されないセルの参照に強制的に値を設定しようとするとエラーになります)
  • xlFree
  • xlStack
  • xlSheetId
  • xlSheetNm
  • xlAbort (ただし、ブレーク条件をクリアする目的では使用できません)
  • xlGetInst
  • xlGetHwnd
  • xlGetBinaryName
  • xlDefineBinaryName

唯一の例外が xlSet です。これはコマンドと同等であり、ワークシート関数から呼び出すことはできません。

Excel 2007 の組み込みのワークシート関数およびそれに対応する C API 関数は、次のものを除き、すべてスレッドセーフです。

  • PHONETIC
  • CELL ("format" 引数または "address" 引数を使用する場合)
  • INDIRECT
  • GETPIVOTDATA
  • CUBEMEMBER
  • CUBEVALUE
  • CUBEMEMBERPROPERTY
  • CUBESET
  • CUBERANKEDMEMBER
  • CUBEKPIMEMBER
  • CUBESETCOUNT
  • ADDRESS (5 番目のパラメータ sheet_name が提供されている場合)
  • Excel PivotTable を参照する任意のデータベース関数 (DSUMDAVERAGE など)
  • ERROR.TYPE
  • HYPERLINK

複数のスレッドによって使用されるリソース

複数のスレッドによってアクセスされる可能性のある読み取り/書き込みメモリは、クリティカル セクションを使用して保護する必要があります。保護するメモリ ブロックごとに 1 つの名前付きクリティカル セクションが必要です。これらのメモリは xlAutoOpen を呼び出す時点で初期化し、xlAutoClose を呼び出す時点で解放してヌルに設定することができます。EnterCriticalSection および LeaveCriticalSection の呼び出しによって、保護されたブロックへのアクセスを組み込む必要があります。どのような場合にも、クリティカル セクションには複数のスレッドを含めることはできません。次に、g_csSharedTable というセクションを初期化、初期化解除して、使用する例を示します。

CRITICAL_SECTION g_csSharedTable; // global scope (if required)
bool xll_initialised = false; // module scope

int __stdcall xlAutoOpen(void)
{
    if(xll_initialised)
        return 1;
// Other initialisation omitted
    InitializeCriticalSection(&g_csSharedTable);
    xll_initialised = true;
    return 1;
}

int __stdcall xlAutoClose(void)
{
    if(!xll_initialised)
        return 1;
// Other cleaning up omitted
    DeleteCriticalSection(&g_csSharedTable);
    xll_initialised = false;
    return 1;
}

bool read_shared_table_element(unsigned int index, double &d)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTable);
    d = shared_table[index];
    LeaveCriticalSection(&g_csSharedTable);
    return true;
}
bool set_shared_table_element(unsigned int index, double d)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTable);
    shared_table[index] = d;
    LeaveCriticalSection(&g_csSharedTable);
    return true;
}

メモリ ブロックを保護するためのもう 1 つの (おそらく、より安全な) 方法は、独自の CRITICAL_SECTION を含むクラスを作成し、コンストラクタ、デストラクタ、アクセサの各メソッドでクラスの使用を処理することです。このアプローチを採用した場合、xlAutoOpen を実行する前に初期化されたり、xlAutoClose が呼び出された後も残っていたりする可能性のあるオブジェクトを保護するという点で有益です。ただし、あまりに多くのクリティカル セクションを作成すると、OS の処理が必要以上にスロー ダウンするため注意が必要です。

複数の保護メモリ ブロックに同時にアクセスする必要のあるコードでは、各クリティカル セクションでの関数の出入りの順序を入念に考慮する必要があります。たとえば、次の 2 つの関数ではデッドロックが生じる可能性があります。

bool copy_shared_table_element_A_to_B(unsigned int index)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTableA);
    EnterCriticalSection(&g_csSharedTableB);
    shared_table_B[index] = shared_table_A[index];
    LeaveCriticalSection(&g_csSharedTableA);
    LeaveCriticalSection(&g_csSharedTableB);
    return true;
}
bool copy_shared_table_element_B_to_A(unsigned int index)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTableB);
    EnterCriticalSection(&g_csSharedTableA);
    shared_table_A[index] = shared_table_B[index];
    LeaveCriticalSection(&g_csSharedTableA);
    LeaveCriticalSection(&g_csSharedTableB);
    return true;
}

あるスレッドで最初の関数が g_csSharedTableA に入る場合、同時に別のスレッドで 2 番目の関数が g_csSharedTableB に入ると、両方のスレッドがハングします。次のように、一貫した順序で入り、その逆の順序で出るようにする必要があります。

    EnterCriticalSection(&g_csSharedTableA);
    EnterCriticalSection(&g_csSharedTableB);
    // code that accesses both blocks
    LeaveCriticalSection(&g_csSharedTableB);
    LeaveCriticalSection(&g_csSharedTableA);

可能であれば、次のように個々のブロックへのアクセスを分離します。これは、スレッド間の連携という観点からも、適切な方法であると言えます。

bool copy_shared_table_element_A_to_B(unsigned int index)
{
    if(index >= SHARED_TABLE_SIZE) return false;
    EnterCriticalSection(&g_csSharedTableA);
    double d = shared_table_A[index];
    LeaveCriticalSection(&g_csSharedTableA);
    EnterCriticalSection(&g_csSharedTableB);
    shared_table_B[index] = d;
    LeaveCriticalSection(&g_csSharedTableB);
    return true;
}

短い時間内での頻繁なアクセス要求など、共有リソースへの競合が頻発する場合は、クリティカル セクションのスピン能力を利用することを検討してください。これは、リソースの待機によってプロセッサが集中的に使用されないようにするためのテクニックです。具体的な方法は、セクションを初期化するときに InitializeCriticalSectionAndSpinCount を使用するか、またはクリティカル セクションを初期化した後に SetCriticalSectionSpinCount を使用して、リソースが空くまでの間スレッドがループする回数を設定します。待機動作はプロセッサを消費しますが、リソースが解放されれば、スピンによってそれを回避できます。シングル プロセッサ システムの場合、スピン回数は事実上無視されますが、指定しても悪影響はありません。メモリ ヒープ マネージャでは、スピン回数として 4000 を使用します。クリティカル セクションの使用法についての詳細は、プラットフォーム SDK ドキュメントのトピック「Critical Sections Object」(英語) を参照してください。

スレッドローカル メモリの宣言と使用

たとえば、XLOPER へのポインタを返す次のような関数について考察してみましょう。

xloper * __stdcall mtr_unsafe_example(xloper *arg)
{
    static xloper ret_val; // memory shared by all threads!!!
// code sets ret_val to a function of arg ...
    return &ret_val;
}

この関数は、スレッドセーフではありません。1 つのスレッドで静的な XLOPER が返され、別のスレッドでそれが上書きされる可能性があるからです。 XLOPERxlAutoFree に渡す必要がある場合は、この状況が起こる可能性がさらに大きくなります。 1 つの解決策は、次のように、返される XLOPER にメモリを割り当て、XLOPER のメモリ自体を解放するように xlAutoFree を実装することです。

xloper * __stdcall mtr_safe_example_1(xloper *arg)
{
    xloper *p_ret_val = new xloper; // must be freed by xlAutoFree
// code sets ret_val to a function of arg ...
    p_ret_val.xltype |= xlbitDLLFree; // Always needed regardless of type
    return p_ret_val; // xlAutoFree must free p_ret_val
}

このアプローチは、次の例で説明する TLS API に依存するアプローチよりも簡単ですが、いくつかの問題があります。 まず、返される XLOPER がどのような型であっても、Excel では xlAutoFree を呼び出さなければなりません。 さらに、新しく割り当てられた XLOPERExcel4 の呼び出しに設定される文字列である場合、xlAutoFree に対し、delete を使用して p_ret_val を解放する前に xlFree を使用して文字列を解放する必要があることを通知する簡単な方法がありません。そのため、この関数は必然的に DLL が割り当てられたコピーを作成しなければならないことになります。

このような制約を回避するための解決策は、スレッドローカルの XLOPER を作成して返すことです。このアプローチは、xlAutoFreeXLOPER ポインタ自体が解放されないようにする必要があります。

xloper *get_thread_local_xloper(void);

xloper * __stdcall mtr_safe_example_2(xloper *arg)
{
    xloper *p_ret_val = get_thread_local_xloper();
// code sets ret_val to a function of arg setting xlbitDLLFree or
// xlbitXLFree if required
    return p_ret_val; // xlAutoFree must not free this pointer!
}

別の問題は、スレッドローカル メモリをどのように設定して取得するかという点です。 これに対しては、Thread Local Storage (TLS) の API を使用します。 最初の手順で、TlsAlloc を使用して TLS インデックスを取得します。このインデックスは最終的に TlsFree を使用して解放します。 どちらも、次のように DllMain から実行するのが最適です。

// This implementation just calls a function to set up thread-local storage
BOOL TLS_Action(DWORD Reason);

__declspec(dllexport) BOOL __stdcall DllMain(HINSTANCE hDll, DWORD Reason, void *Reserved)
{
    return TLS_Action(Reason);
}
DWORD TlsIndex; // only needs module scope if all TLS access in this module

BOOL TLS_Action(DWORD DllMainCallReason)
{
    switch (DllMainCallReason)
    {
    case DLL_PROCESS_ATTACH: // The DLL is being loaded
        if((TlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)
            return FALSE;
        break;

    case DLL_PROCESS_DETACH: // The DLL is being unloaded 
        TlsFree(TlsIndex); // Release the TLS index.
        break;
    }
    return TRUE;
}

インデックスを取得した後、次の手順で各スレッド用のメモリ ブロックを割り当てます。ダイナミック リンク ライブラリ リファレンスの「DllMain」(英語) では、DLL_THREAD_ATTACH イベントで DllMain を呼び出すたびにこの割り当てを行い、DLL_THREAD_DETACH のたびにメモリを解放することが推奨されています。しかし、このアドバイスに従った場合、Excel で再計算に使用されないスレッド用に DLL が不必要な割り当てを実行することになります。それよりも、最初に使用するときにメモリを割り当てる方式を採用した方が得策です。この場合、まず、各スレッドに割り当てるための構造を定義します。上記の例の場合、次のような定義で十分です。

struct TLS_data
{
    xloper xloper_shared_ret_val;
// Add other required thread-local data here...
};

次の関数は、スレッドローカル インスタンスへのポインタの取得を実行します (最初の呼び出しの場合には、このポインタの割り当てを実行します)。

TLS_data *get_TLS_data(void)
{
// Get a pointer to this thread's static memory
    void *pTLS = TlsGetValue(TlsIndex);
    if(!pTLS) // No TLS memory for this thread yet
    {
        if((pTLS = calloc(1, sizeof(TLS_data))) == NULL)
        // Display some error message (omitted)
            return NULL;
        TlsSetValue(TlsIndex, pTLS); // Associate this this thread
    }
    return (TLS_data *)pTLS;
}

スレッドローカルの XLOPER メモリを取得する方法は既に説明しましたが、まず、スレッドの TLS_data のインスタンスへのポインタを取得し、次に、その中に含まれる XLOPER へのポインタを返します。

xloper *get_thread_local_xloper(void)
{
    TLS_data *pTLS = get_TLS_data();
    if(pTLS)
        return &(pTLS->xloper_shared_ret_val);
    return NULL;
}

TLS_data および get_thread_local_xloper12 アクセス関数に XLOPER12 を追加することにより、mtr_safe_example の XLOPER12 バージョンを記述できます。

明らかなことですが、mtr_safe_example_1 および mtr_safe_example_2 はスレッドセーフな関数であるため、Excel 2007 を実行するときは "RP$"、Excel 2003 を実行するときは "RP" として登録できます。 XLOPER12 を "UQ$"として使用するこの XLL 関数のバージョンは、Excel 2007 で作成、登録することができますが、Excel 2003 で登録することはできません。

まとめ

Office 2007 のリリースに続いて、新しい XLL SDK が提供される予定です。その時点で、この記事で説明した新しい機能が活用できるようになります。

この記事は、Office Excel 2007 での XLL の開発について説明しました。 この記事で説明した機能は、マイクロソフト ダウンロード センターの「Excel 2007 XLL Software Development Kit」(英語) から入手可能です。

執筆者について

Steve Dalton 氏は英国の Eigensys Ltd. 社の設立者です。同社は財務分析の分野での Excel 開発を専門としています。Dalton 氏は『Excel Add-in Development in C/C++: Applications in Finance』(Wiley、2004) および『Financial Applications Using Excel Add-in Development in C/C++』(Wiley、2007) の著者でもあります。

この記事は、A23 Consulting(英語) と共同で執筆されました。

追加情報

Excel 2007 のアドイン開発についてさらに詳しく知るには、以下の情報を参照してください。