Share via


SAX の喜び : Visual Basic サンプル

Martin Naughton

June 2000

日本語版編集注: この記事は、May 2000 Microsoft XML Parser Technology Preview に基づいて執筆されています。この 原文記事 の公開後にリリースされた September 2000 Microsoft XML Parser Beta Release において、SAX2 の実装に関して変更が行われております。この記事とあわせて、「September 2000 Microsoft XML Parser Beta Release の新機能」 および MSXML SDK Beta Release 収録の文書 「SAX2 Reference」 を参照ください。

要約: この記事では、Microsoft® Visual Basic® で SAX2 インターフェイスのプログラミングを行うアプローチについて説明します。

JoyOfSax.exe については、MSDN Online Code Center のページを参照のうえダウンロードしてください。

はじめに

2000 年 5 月の MSXML テクノロジ プレビューの主な特徴の 1 つは、SAX2 (Simple API for XML Version 2) の実装です。SAX2 の概要として、MSDN XML Developer Center では、「XML 開発者のための SAX2 ジャンプスタート 」 という記事と、ダウンロード可能な Microsoft Visual C++® アプリケーションを提供しています。この記事では、Visual Basic で SAX2 インターフェイスのプログラミングを行うアプローチについて概説しています。このサンプルはサポートされず、SAX/Visual Basic ソリューションのプロトタイプ化の支援を目的としています。さらにこのサンプルのインターフェイスは、今後の SAX に対する Microsoft の Visual Basic のサポートには何も関係がありません。

Hello World?

Visual Basic (VB) において、MSXML3.dll ファイルなどの COM (コンポーネント オブジェクト モデル) コンポーネントを使用した実験の代表的なアプローチでは、新しい "標準 EXE" プロジェクトを作成し、[プロジェクト] メニューの [参照設定] をクリックして MSXML3.dll タイプ ライブラリ (Microsoft XML Version 3.0) への参照を追加します。この時点で、Visual Basic オブジェクト ブラウザを使用して、選択したインターフェイスのプロパティ、メソッド、およびイベントを調べることができます。

これを MSXML3.dll で実行して SAX2 インターフェイスを探しましたが、このタイプ ライブラリにはリモートで SAX-y となるものは見つかりませんでした。次に、適切なバージョンの MSXML3.dll がインストールされているかどうかを確認するためにレジストリを調べました。実際に、HKEY_CLASSES_ROOT ディレクトリの下に有望に見える 2 つの ProgID (具体的には、Msxml2.SAXXMLReader と Msxml2.SAXXMLReader.3.0) がありました。そこで、関連する 2、3 のニュースグループに "何かが欠けているのでしょうか" という内容の質問をポストしました。直ちに、実際に欠けているものがあるという返事を受け取りました。

ソースを使えよ、Luke

SAX2 インターフェイスは、Xmlsax.idl というファイルに定義されています。MSXML3.dll を既定のフォルダにインストールした場合、Xmlsax.idl ファイルは C:\Program Files\Microsoft XML Parser SDK\inc フォルダに置かれます。ファイルを見つけた後、Xmlsax.idl を適切な Xmlsax.tlb というタイプ ライブラリ ファイルにコンパイルしました。コンパイルに使用したツールは MIDL IDL コンパイラです。

Visual Basic 統合開発環境 (IDE) で、再び [プロジェクト] メニューの [参照設定] をクリックし、[参照設定] ダイアログ ボックスの [参照] ボタンをクリックして、新しい Xmlsax.tlb ファイルを探し出しました。この方法でタイプ ライブラリを選択すると、VB IDE は最初にこのタイプ ライブラリ ファイルを登録します。これはかなり期待が持てます。SAX2 インターフェイスは、オブジェクト ブラウザ上に表示されるようになりました。

キャラクタの構成

いくつかのメソッドのパラメータを調べると、ニュースグループからの指摘された事柄が確認できました。このインターフェイスは、あまり Visual Basic に適していませんでした。Microsoft XML SDK 3.0 のドキュメントには、SAX2 インターフェイスが "Microsoft の SAX2 のCOM/C++ 実装" として記載されています。要約すると、Visual Basic 開発者が文字列パラメータがあると予測する場所には、実際には 2 つのパラメータがあります。最初のパラメータには、"pwch" というハンガリー記法のプレフィックス ("pointer to an array of wide char (ワイド文字の配列を指すポインタ)"?) が付いていて、2 番目のパラメータには "cch" プレフィックス ("count of char (文字のカウント)"?) が付いています。

StartElement メソッドに対する ISAXContentHandler IDL は以下のとおりです。

HRESULT StartElement(
        [in] const wchar_t * pwchNamespaceUri, 
        [in] int cchNamespaceUri, 
        [in] const wchar_t * pwchLocalName, 
        [in] int cchLocalName, 
        [in] const wchar_t * pwchQName, 
        [in] int cchQName, 
        [in] ISAXAttributes * pAttributes);

幸運なことに、ニュースグループからのリプライにこれらのパラメータを処理する Visual Basic コードが含まれていました。この関数のシンプルなバージョンを次に示します。

Public Function UnicodeArrayToString(pwchArray As Integer, ByVal lCharCount As Long) As String

Dim sText As String

' 文字列のサイズを適切な文字数にします。
sText = String$(lCharCount, 0)

' 値をコピーします。文字列は Unicode (ダブル バイト) なので
' サイズは文字数の 2 倍になることに注意してください。
Call CopyMemory(ByVal StrPtr(sText), iUnicodeCharArrayFirstElt, 
  lCharCount * 2)

UnicodeArrayToString = sText

End Function

XMLSAX インターフェイスは、Visual Basic 整数の配列と配列内の項目数を返します。UnicodeArrayToString 関数は、文書化されていない Visual Basic StrPtr 関数と CopyMemory Windows API に対する呼び出しを使用して、これらの 2 つの値を Visual Basic 文字列に変換します。CopyMemory は Windows API RtlMoveMemory の別名です。

Visual Basic の SAX2 "ジャンプスタート"

Visual Basic で "SAX2 ジャンプスタート" のバージョンを作成する準備が整いました。C++ "ジャンプスタート" アプリケーションに類似した結果を得るには、以下の指示に従ってください。MSXML への参照は必要ないことに注意してください。

  1. Visual Basic の標準 EXE プロジェクトを作成します。

  2. XMLSAX.tlb ファイルへの参照を追加します (ダウンロードに含まれています)。

  3. 以下に示すコードをコピーして、Visual Basic フォームのコード ウィンドウに貼り付けます (コードを貼り付けた後、実行する前にコードからキャリッジ リターンを削除する必要があります)。

  4. フォームに Command1 コマンド ボタン (CommandButton) を追加します。

  5. プロジェクトを保存します。

  6. プロジェクトを保存した同じフォルダ内に Test.xml という有効な XML ファイルがない場合は、そのファイルをこのフォルダに配置します。

  7. デバッグ モードでプロジェクトを実行します。

  8. Command1 ボタンをクリックします。

  9. [イミディエイト] ウィンドウを表示します。

Option Explicit

Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (pDest
  As Any, pSource As Any, ByVal ByteLen As Long)

Implements XMLSAX.ISAXContentHandler

Private Sub Command1_Click()

Call Parse

End Sub

Private Function Parse() As Boolean

Dim i() As Integer
Dim str As String

Dim sax As SAXXMLReader30

Set sax = New SAXXMLReader30

Call sax.PutContentHandler(Me)

str = App.Path & "\" & "test.xml"

' 配列を文字数のサイズにします。
ReDim i(Len(str))

' メモリをコピーします。Unicode 文字列 (ダブル バイト) なので、
' サイズが文字数の 2 倍になることに注意してください。
CopyMemory i(0), ByVal StrPtr(str), Len(str) * 2

' 最初の配列のエントリを渡します。
sax.ParseURL i(0), Len(str)

End Function

Private Sub ISAXContentHandler_Characters(pwchChars As Integer, ByVal
  cchChars As Long)

'何もしません。

End Sub

Private Sub ISAXContentHandler_EndDocument()

'何もしません。


End Sub

Private Sub ISAXContentHandler_EndElement(pwchNamespaceUri As Integer,
  ByVal cchNamespaceUri As Long, pwchLocalName As Integer, ByVal
  cchLocalName As Long, pwchQName As Integer, ByVal cchQName As Long)

'何もしません。

End Sub

Private Sub ISAXContentHandler_EndPrefixMapping(pwchPrefix As Integer,
  ByVal cchPrefix As Long)

'何もしません。


End Sub

Private Sub ISAXContentHandler_IgnorableWhitespace(pwchChars As Integer,
  ByVal cchChars As Long)

'何もしません。


End Sub

Private Sub ISAXContentHandler_ProcessingInstruction(pwchTarget As
  Integer, ByVal cchTarget As Long, pwchData As Integer, ByVal cchData
  As Long)

'何もしません。


End Sub

Private Sub ISAXContentHandler_PutDocumentLocator(ByVal pLocator As
  XMLSAX.ISAXLocator)

'何もしません。

End Sub

Private Sub ISAXContentHandler_SkippedEntity(pwchName As Integer, ByVal
  cchName As Long)

'何もしません。


End Sub

Private Sub ISAXContentHandler_StartDocument()

End Sub

Private Sub ISAXContentHandler_StartElement(pwchNamespaceUri As Integer,
  ByVal cchNamespaceUri As Long, pwchLocalName As Integer, ByVal
  cchLocalName As Long, pwchQName As Integer, ByVal cchQName As Long,
  ByVal pAttributes As XMLSAX.ISAXAttributes)

Dim str As String

' 文字列のサイズを適切な文字数にします。
str = String$(cchLocalName, 0)

' 値をコピーします。文字列は Unicode (ダブル バイト) なので
' サイズが文字数の 2 倍になることに注意してください。
CopyMemory ByVal StrPtr(str), pwchLocalName, cchLocalName * 2

Debug.Print str

End Sub

Private Sub ISAXContentHandler_StartPrefixMapping(pwchPrefix As Integer,
  ByVal cchPrefix As Long, pwchUri As Integer, ByVal cchUri As Long)

'何もしません。


End Sub

ラップの実行

Visual Basic で SAX2 をプログラミングする最初のハードルを越えた後、長期的に見た場合の最善の方法は、すべての SAX2 インターフェイスに対して Visual Basic ラッパー クラスの内側に、取り扱いに注意を要するものをカプセル化することであると判断しました。

この作業の結果は、この記事でダウンロードできるコードを見ればわかります。完全ではありませんが、重要な点がすべて示されています。サンプル コードは、ActiveX® コンポーネント (VBXMLSAX) にコンパイルでき、実装の詳細の知識がなくても (Visual Basic、VBScript、JScript®、さらに Visual C++ でも) 再利用できます。これらの実装の問題点について知りたい方は続けてお読みください。

パーサーを終了する

SAX2 のようなイベント ドリブンのパーサーにおける優れた機能の 1 つは、ユーザーが定義した条件が満たされた場合に解析を中止する機能です。たとえば、XML ドキュメントで "UNINTERESTING" という要素が検出された場合に解析を中止する場合があります。SAX2 ハンドラ インターフェイスによってユーザーは解析を中止することができます。イベント シンク (たとえば VB Form オブジェクト) が解析の終了を制御できるように、ハンドラ インターフェイスのメソッドをラッパー クラスの Visual Basic イベントとして提供しようと計画しています。

インターフェイスのハンドラ ファミリのドキュメントを読んでみると、ハンドラ メソッドは、戻り値 (HRESULT) をシンボリック ERR_FAIL 値に設定することによって解析を中止する必要性を示すと記載されています。ここに 1 つだけ問題があります。VB では HRESULT が隠されるため、カスタム ハンドラ コードにこの値を設定することはできません。

実際にはこの値を設定できますが、そのためにはいくつかのハードルを越えなければなりません。この手法は、"vtable 修正" と呼ばれ、Bruce McKinney の著書 『Hardcore Visual Basic』 に記載されています。実行時に Visual Basic が提供するイベント ハンドラからの呼び出しを、自身の関数にリダイレクトすることは可能です。この手法の鍵は、AddressOf 演算子を使用し、オーバーライド関数のアドレスを入手して、CopyMemory を使用して該当するインターフェイスの vtable エントリを上書きすることです。オーバーライド関数は (Visual Basic ではなく) ユーザーが定義するため、戻り値を制御することができます。

例を見てみましょう。ISAXContentHandler インターフェイスは StartDocument というメソッドを提供します。ISAXContentHandler (CVBSAXContentHandler) を実装する Visual Basic のクラス ラッパーを作成する場合、Visual Basic によって以下のようなハンドラ ルーチン プロトタイプが提供されます。

Private Sub ISAXContentHandler_StartDocument()

裏側では、関数プロトタイプは実際は以下のようになっています。

Public Function ISAXContentHandler_StartDocument(ByVal This As
  ISAXContentHandler) As Long

実際の戻り値の型は HRESULT ですが、HRESULT 値を保存するために Visual Basic の長整数型 (Long) を使用することができます。したがって、StartDocument メソッドをオーバーライドするには、BAS ファイルの後の方のプロトタイプで関数を作成します。実行時に呼び出されるのは Visual Basic クラス ラッパーのルーチンではなく、このオーバーライド関数です。"This" というパラメータの追加に注意してください。呼び出されるのは ISAXContentHandler ハンドラのインスタンスです。StartDocument のオーバーライド関数は以下のとおりです。

Public Function ISAXContentHandler_StartDocument(ByVal This As ISAXContentHandler) As Long
  
#If iDebug = -1 Then
  Debug.Print "VTable Replacement for ISAXContentHandler_StartDocument"
#End If
  
  Dim booAbort As Boolean

  Dim objVBSAXContentHandler As CVBSAXContentHandler
  Set objVBSAXContentHandler = This
  
  Call objVBSAXContentHandler.StartDocument(booAbort)
  
  If booAbort Then
    ISAXContentHandler_StartDocument = E_FAIL
  Else
    ISAXContentHandler_StartDocument = S_OK
  End If

End Function

この手法の使用には問題が 1 つあります。AddressOf 演算子を適用できるのは標準モジュール (BAS ファイル) に定義された関数のみです。したがって、特定のハンドラ インターフェイスのすべてのインスタンスに対して同じ関数が呼び出されます。これにより、イベントを発生するのはラッパーであるため、ハンドラ ラッパーのどのインスタンスが関連しているのかを判別しなければならないという問題が起きます。

幸運なことに、vtable オーバーライド関数の必要なフォーマットには、"This" パラメータが含まれていて (前のコード サンプルを参照)、その型は呼び出されるインターフェイスです。このパラメータから、CVBSAXContentHandler 型の変数を宣言し、以下の割り当てを行うことによって、QueryInterface に相当することを実行できます。

Dim objCVBSAXContentHandler As CVBSAXContentHandler
Set objCVBSAXContentHandler = This

この呼び出しで、実際に Visual Basic ラッパー インターフェイスが取得されます。その後で、予測したとおりにラッパーがイベントを発生するように、ラッパーを呼び出すことができます。

この効果を得るために、"Friend" スコープで CVBSAXContentHandler のメソッドを定義します。これにより、VBSAXXML コンポーネントの内部コードはこの関数にアクセスできますが、外部のクライアントはそれを見ることもできません。この Friend メソッドの実装は単にイベントを発生させ、元のパラメータすべてと Abort フラグを渡し (ByRef で渡し)、このイベントを処理するコードがフラグを設定できるようにします。コードは以下のとおりです。

Friend Sub StartDocument(Abort As Boolean)
 
  RaiseEvent StartDocument(Abort)

End Sub

クライアント アプリケーション (たとえばフォーム) の StartDocument イベント ハンドラから返されると、CVBSAXContentHandler の Friend StartDocument メソッドは、単に "Abort" パラメータの値を vtable オーバーライド関数に戻します。Abort フラグの値に基づいて、vtable オーバーライド関数は基礎となっている ISAXContentHandler.StartElement メソッドの HRESULT 戻り値を適切に設定することができます。

そのほかのハンドラ メソッドも、同様の形式でオーバーライドされます。以下は、独自のパラメータを持つ StartElement メソッドをオーバーライドする関数のプロトタイプです。

Public Function ISAXContentHandler_StartElement(ByVal This As
  ISAXContentHandler, pwchNamespaceUri As Integer, ByVal cchNamespaceUri
  As Long, pwchLocalName As Integer, ByVal cchLocalName As Long, pwchQName
  As Integer, ByVal cchQName As Long, ByVal pAttributes As
  XMLSAX.ISAXAttributes) As Long

Friend に相当するものは以下のようになります。

Friend Sub StartElement(ByVal NamespaceUri As String, ByVal LocalName As
  String, ByVal QName As String, ByVal Attributes As CVBSAXAttributes,
  Abort As Boolean)
  
  RaiseEvent StartElement(NamespaceUri, LocalName, QName, Attributes,
  Abort)

End Sub

メソッドが独自のパラメータを持つ場合、C++ スタイルのパラメータから Visual Basic に適した文字列へのマッピングを実行するのは vtable オーバーライド関数です。

タイプ ライブラリ ハッキング

場合により、提供された Xmlsax.idl ファイルのコンパイルによって作成される Xmlsax.tlb タイプ ライブラリは Visual Basic に適していないだけでなく、実際は Visual Basic で使用できないことが判明しました。特に "out" として IDL で定義されたメソッドに対するパラメータは、Visual Basic からアクセスできません。パラメータが "in, out" (ByRef パラメータ) または "out, retval" (戻り値) として定義されている場合は、Visual Basic では使用できません。これは、SAX2 インターフェイスのメソッドのいくつかに該当します。

さらに、ISAXAttributes インターフェイスの大部分のメソッドの呼び出しで問題が発生しました。たとえば、GetLocalName メソッドは、Visual Basic のサブルーチンを作成します。呼び出し元からは、Attributes コレクションのインデックスが渡されます。ルーチンは、既におなじみの整数配列と配列長パラメータを返します。IDL は以下のようになっています。

HRESULT GetLocalName(
        [in] int nIndex,
        [out] const wchar_t ** ppwchLocalName,
        [out] int * pcchLocalName);

"out" パラメータは Visual Basic では受け入れられないことがわかっています。したがって、"out" パラメータの IDL を "in, out" に変更することにしました。ただし、const wchar_t ** データ型をデコードする方法がわかりませんでした。『Hardcore Visual Basic』 によると、有望そうな UPointerToString という関数を提供していることがわかりました。しかし、UPointerToString ではポインタ (VB 用語では As Long) を予期しています。そのために、データ型を const wchar_t ** から int * に変更することにしました。修正した IDL は以下のようになります。

HRESULT GetLocalName(
        [in] int nIndex,
        [in, out] int * ppwchLocalName,
        [in, out] int * pcchLocalName);

このフォーマットは、Visual Basic で使用可能であることが判明しました。文字列値は UPointerToStringEx (UPointerToString と同じですが、パラメータとして文字カウントをとります) で正常にデコードされました。

ISAXAttributes のメソッドを、サブルーチンとして呼び出せるようになりました。

  Call pAttributes.GetValue(0, pwchValue, cchValue)

上記の呼び出しは、pwchValue の値を指すポインタと cchValue の値の長さを割り当てて、インデックス位置 0 の属性値を取得します。その後、VB 文字列は以下のコードから返されます (以前に作成されたものと同様です)。

  sAttValue = String$(cchValue, 0)
  CopyMemory ByVal StrPtr(sAttValue), ByVal pwchValue, cchValue * 2

pwchValue の前に ByVal が必要なことに注意してください。

Xmlsax.idl ファイルをコピーし、XmlsaxVB.idl という名前を付け、修正してからコンパイルして XmlsaxVB.tlb にしました。ダウンロードに含まれている ActiveX コンポーネント プロジェクトで参照されているのは、この修正されたタイプ ライブラリです。

IDL ファイルの修正は一般的にはお勧めできません。ただし、修正したタイプ ライブラリを開発に使用したコンピュータから移動しないことをお勧めします。ディストリビューション ウィザードを使用してセットアップ プログラムを作成する場合、このようなタイプ ライブラリをセットアップ パッケージに組み込まないでください。ただし、この記事で取り上げた MSXML コンポーネントはテクノロジ プレビューであるため、だれかがこのサンプルを実稼動環境に統合しようとする危険性はありません。