EternalWindows
LSP / 関数テーブル

ここからは、LSPの開発について説明します。 既に述べてきたようにLSPの正体はDLLであり、 このDLLが実装する関数はws2_32.dllによって適切なタイミングで呼び出されることになっています。 LSPが実装する関数の中で最初に呼び出されるのは、WSPStartupです。

int WSPStartup(
  WORD wVersionRequested,
  LPWSPDATA lpWSPData,
  LPWSAPROTOCOL_INFOW lpProtocolInfo,
  WSPUPCALLTABLE UpcallTable,
  LPWSPPROC_TABLE lpProcTable
);

wVersionRequestedは、Winsock SPIのバージョンが格納されます。 lpWSPDataは、LSPに関する情報を表すWSPDATA構造体のアドレスが格納されます。 これは、LSP自身が初期化する必要があります。 lpProtocolInfoは、WSAPROTOCOL_INFOW構造体のアドレスが格納されます。 この構造体は、ダミーエントリではない、実際のエントリを表しています。 UpcallTableは、WSPUPCALLTABLE構造体が格納されます。 この構造体には、LSPからws2_32.dllの機能を利用するための関数アドレスが設定されています。 lpProcTableは、WSPPROC_TABLE構造体のアドレスが格納されます。 これは、LSP自身が初期化する必要があります。 関数が成功した場合は、0を返します。

WSPStartupで最も重要な引数は、lpProcTableです。 これは、LSPが実装している各関数のアドレスを維持するテーブルであり、 ws2_32.dllはこのテーブルを基に、LSPが実装している関数を呼び出すことになっています。 WSPPROC_TABLEは、次のように定義されています。

typedef struct _WSPPROC_TABLE {
  LPWSPACCEPT              lpWSPAccept;
  LPWSPADDRESSTOSTRING     lpWSPAddressToString;
  LPWSPASYNCSELECT         lpWSPAsyncSelect;
  LPWSPBIND                lpWSPBind;
  LPWSPCANCELBLOCKINGCALL  lpWSPCancelBlockingCall;
  LPWSPCLEANUP             lpWSPCleanup;
  LPWSPCLOSESOCKET         lpWSPCloseSocket;
  LPWSPCONNECT             lpWSPConnect;
  LPWSPDUPLICATESOCKET     lpWSPDuplicateSocket;
  LPWSPENUMNETWORKEVENTS   lpWSPEnumNetworkEvents;
  LPWSPEVENTSELECT         lpWSPEventSelect;
  LPWSPGETOVERLAPPEDRESULT lpWSPGetOverlappedResult;
  LPWSPGETPEERNAME         lpWSPGetPeerName;
  LPWSPGETSOCKNAME         lpWSPGetSockName;
  LPWSPGETSOCKOPT          lpWSPGetSockOpt;
  LPWSPGETQOSBYNAME        lpWSPGetQOSByName;
  LPWSPIOCTL               lpWSPIoctl;
  LPWSPJOINLEAF            lpWSPJoinLeaf;
  LPWSPLISTEN              lpWSPListen;
  LPWSPRECV                lpWSPRecv;
  LPWSPRECVDISCONNECT      lpWSPRecvDisconnect;
  LPWSPRECVFROM            lpWSPRecvFrom;
  LPWSPSELECT              lpWSPSelect;
  LPWSPSEND                lpWSPSend;
  LPWSPSENDDISCONNECT      lpWSPSendDisconnect;
  LPWSPSENDTO              lpWSPSendTo;
  LPWSPSETSOCKOPT          lpWSPSetSockOpt;
  LPWSPSHUTDOWN            lpWSPShutdown;
  LPWSPSOCKET              lpWSPSocket;
  LPWSPSTRINGTOADDRESS     lpWSPStringToAddress;
} WSPPROC_TABLE, FAR * LPWSPPROC_TABLE;

この構造体に格納されているメンバは、ws2_32.dllから呼び出される可能性のある関数の全リストです。 そして、各メンバが表す関数がどのようなタイミングで呼ばれるかは、 メンバ名から想像できるようになっています。 たとえば、lpWSPAcceptに設定された関数は、 Winsockアプリケーションがacceptを呼び出した場合に呼ばれることになります。 ただし、LSPがacceptの呼び出しを検出したいからといって、 lpWSPAcceptのみを初期化するわけにはいきません。 ws2_32.dllからすれば、その他のメンバにも適切な関数アドレスが設定されていると思っていますから、 全てのメンバを初期化しておく必要があります。

WSPPROC_TABLE構造体の全てのメンバを初期化するとなると、 LSPは全ての関数を適切に実装しなければならないように思えますが、 そのようなことはありません。 LSPは何らかのエントリの上にインストールされているため、 下に存在するエントリが関数を適切に実装していれば、 その関数のアドレスをWSPPROC_TABLE構造体に指定する方法が成立します。 具体的には、下に存在するエントリのDLLを明示的にロードしてWSPStartupを呼び出し、 初期化されたWSPPROC_TABLE構造体をlpProcTableにコピーするようにします。 そして、LSPが独自に処理したい関数のアドレスをlpProcTableの適切なメンバに指定します。 このようにすれば、必要な関数の呼び出しのみを検出できるようになり、 その他の関数は下のエントリのDLLに処理を任すことができます。

下に存在するエントリを取得するには、WSPStartupのlpProtocolInfoを利用することになります。 これはLSP自身を表す実際のエントリですが、このエントリのChainEntries[1]には、 下に存在するエントリのIDが格納されているはずですから、 これを頼りにすれば下のエントリのWSAPROTOCOL_INFOW構造体を取得することができます。 ただし、WSAPROTOCOL_INFOW構造体にはDLLのパスを格納するメンバが存在しないため、 WSCGetProviderPathを呼び出してパスを取得することになります。

int WSPAPI WSCGetProviderPath(
  LPGUID lpProviderId,
  LPWSTR lpszProviderDllPath,
  LPINT lpProviderDllPathLen,
  LPINT lpErrno
);

lpProviderIdは、エントリのGUIDを指定します。 lpszProviderDllPathは、DLLのパスを受け取るバッファを指定します。 lpProviderDllPathLenは、lpszProviderDllPathのサイズを指定します。 lpErrnoは、エラー情報を表す変数のアドレスを指定します。

下に存在するエントリのDLLのパスを取得すれば、 それを基にDLLをロードすることができるようになります。 次に、WSPStartupの実装例を示します。

int WSPAPI WSPStartup(WORD wVersionRequested, LPWSPDATA lpWSPData, LPWSAPROTOCOL_INFOW lpProtocolInfo, WSPUPCALLTABLE UpcallTable, LPWSPPROC_TABLE lpProcTable)
{
	int               nError;
	WCHAR             szDllPath[256];
	WCHAR             szDllPathEnv[256];
	DWORD             dwSize;
	WSPDATA           wspData;
	LPWSPSTARTUP      lpfnWSPStartup;
	WSAPROTOCOL_INFOW nextEntry;
	
	if (!FindNextEntry(lpProtocolInfo, &nextEntry))
		return WSAEPROVIDERFAILEDINIT;

	dwSize = sizeof(szDllPathEnv) / sizeof(WCHAR);
	WSCGetProviderPath(&nextEntry.ProviderId, szDllPathEnv, (LPINT)&dwSize, &nError);

	dwSize = sizeof(szDllPath) / sizeof(WCHAR);
	ExpandEnvironmentStringsW(szDllPathEnv, szDllPath, dwSize);

	g_hmod = LoadLibraryW(szDllPath);
	if (g_hmod == NULL)
		return WSAEPROVIDERFAILEDINIT;

	lpfnWSPStartup = (LPWSPSTARTUP)GetProcAddress(g_hmod, "WSPStartup");
	if (lpfnWSPStartup == NULL) {
		FreeLibrary(g_hmod);
		return WSAEPROVIDERFAILEDINIT;
	}
	
	if (lpfnWSPStartup(wVersionRequested, &wspData, &nextEntry, UpcallTable, &g_procTable) != 0) {
		FreeLibrary(g_hmod);
		return WSAEPROVIDERFAILEDINIT;
	}

	CopyMemory(lpProcTable, &g_procTable, sizeof(WSPPROC_TABLE));
	lpProcTable->lpWSPCleanup = WSPCleanup;
	lpProcTable->lpWSPConnect = WSPConnect;
	lpProcTable->lpWSPRecv    = WSPRecv;
	lpProcTable->lpWSPSend    = WSPSend;
	lpProcTable->lpWSPSocket  = WSPSocket;

	CopyMemory(lpWSPData, &wspData, sizeof(WSPDATA));
	CopyMemory(&g_upcallTable, &UpcallTable, sizeof(WSPUPCALLTABLE));

	return 0;
}

FindNextEntryという自作関数は、第1引数に指定したエントリの下に存在するエントリを第2引数に返します。 LSPが正しくインストールされている場合は、この処理に失敗することはないはずですが、 失敗した場合はWSPStartupの引数を正しく初期化できなかったことを示すWSAEPROVIDERFAILEDINITを返します。 DLLをロードする部分は、次のようになっています。

dwSize = sizeof(szDllPathEnv) / sizeof(WCHAR);
WSCGetProviderPath(&nextEntry.ProviderId, szDllPathEnv, (LPINT)&dwSize, &nError);

dwSize = sizeof(szDllPath) / sizeof(WCHAR);
ExpandEnvironmentStringsW(szDllPathEnv, szDllPath, dwSize);

g_hmod = LoadLibraryW(szDllPath);
if (g_hmod == NULL)
	return WSAEPROVIDERFAILEDINIT;

下に存在するエントリのGUIDをWSCGetProviderPathに指定して、 下に存在するエントリのDLLパスを取得します。 エントリがTCPやUDPを実装するベースプロトコルである場合は、 取得したパスが環境変数で構成されていることがあるため、 この点を考慮してExpandEnvironmentStringsを呼び出すようにしています。 これにより、szDllPathには正規のDLLパスが格納されます。 WSCGetProviderPathはDLLパスをWCHAR型で返すため、 ExpandEnvironmentStringsやLoadLibraryは、UNICODE版の関数を呼び出しています。 WSPStartupの呼び出しは、次のようになっています。

lpfnWSPStartup = (LPWSPSTARTUP)GetProcAddress(g_hmod, "WSPStartup");
if (lpfnWSPStartup == NULL) {
	FreeLibrary(g_hmod);
	return WSAEPROVIDERFAILEDINIT;
}

if (lpfnWSPStartup(wVersionRequested, &wspData, &nextEntry, UpcallTable, &g_procTable) != 0) {
	FreeLibrary(g_hmod);
	return WSAEPROVIDERFAILEDINIT;
}

各エントリのDLLは、ws2_32.dllとの接点として必ずWSPStartupをエクスポートしています。 しかし、下のエントリのWSPStartupを呼び出すのは、ws2_32.dllではなくLSP自身です。 理由は既に述べたように、下のエントリの関数テーブルを取得するためです。 第3引数はlpProtocolInfoではなく、下のエントリを示すnextEntryを指定するようにします。 WSPStartupの呼び出しが成功すれば、lpProcTableの初期化を行います。

CopyMemory(lpProcTable, &g_procTable, sizeof(WSPPROC_TABLE));
lpProcTable->lpWSPCleanup = WSPCleanup;
lpProcTable->lpWSPConnect = WSPConnect;
lpProcTable->lpWSPRecv    = WSPRecv;
lpProcTable->lpWSPSend    = WSPSend;
lpProcTable->lpWSPSocket  = WSPSocket;

先のWSPStartupの呼び出しで、g_procTableには下のエントリの関数テーブルが格納されています。 よって、これをlpProcTableにコピーし、その後にLSPが実装する独自の関数のアドレスを指定します。 この例では、lpWSPSocketを書き換えていますから、 Winsockアプリケーションがsocketを呼び出した場合に、 LSPが実装しているWSPSocketが呼ばれることになります。 こうした関数内では、独自の処理を終えた後に、下のエントリに処理を任せることになるため、 g_procTableはグローバルに定義しています。 残りの処理は、次のようになっています。

CopyMemory(lpWSPData, &wspData, sizeof(WSPDATA));
CopyMemory(&g_upcallTable, &UpcallTable, sizeof(WSPUPCALLTABLE));

下のエントリのWSPStartupを呼び出したことで、WSPDATA構造体も初期化されたことになるため、 これをそのままlpWSPDataにコピーするようにしています。 当然ながら、LSP独自の情報を指定しても問題ありません。 WSPUPCALLTABLE構造体は、後で必要になるためグローバル変数として保存しておきます。

WSPStartupで呼び出しているFindNextEntryは、次のような実装になります。

BOOL FindNextEntry(LPWSAPROTOCOL_INFOW lpOverEntry, LPWSAPROTOCOL_INFOW lpNextEntry)
{
	int                 i;
	int                 nError;
	int                 nTotalEntryCount;
	DWORD               dwSize;
	LPWSAPROTOCOL_INFOW lpEntryList;

	WSCEnumProtocols(NULL, NULL, &dwSize, &nError);
	lpEntryList = (LPWSAPROTOCOL_INFOW)HeapAlloc(GetProcessHeap(), 0, dwSize);
	nTotalEntryCount = WSCEnumProtocols(NULL, lpEntryList, &dwSize, &nError);

	for (i = 0; i < nTotalEntryCount; i++) {
		if (lpEntryList[i].dwCatalogEntryId == lpOverEntry->ProtocolChain.ChainEntries[1]) {
			CopyMemory(lpNextEntry, &lpEntryList[i], sizeof(WSAPROTOCOL_INFOW));
			HeapFree(GetProcessHeap(), 0, lpEntryList);
			return TRUE;
		}
	}
	
	HeapFree(GetProcessHeap(), 0, lpEntryList);

	return FALSE;
}

まず、WSCEnumProtocolsで全てのエントリのリストを取得し、 その中からChainEntries[1]と等しいエントリを検索します。 LSPのエントリのChainEntries[1]には、下のエントリのIDが格納されているため、 検索で一致した場合は下のエントリを発見できたことになります。 よって、lpNextEntryに目的のエントリをコピーします。


戻る