Friends of Total Commander(64ビット)およびプラグイン(32ビット)

Total Commander (以降TC)の64ビットバージョンで32ビットバージョンの拡張機能を使用できるようにする小さなプロジェクトについて説明します。 プロジェクトはデモ段階にあり、WCXモジュールを最小限の機能セットで使用できます(アーカイブの内容またはアーカイブとして表現できるものを表示および抽出する)。 最後に、そのようなソリューションの関連性に関する調査と、プロジェクトを特定のレベルに引き上げ、APIのすべての可能な部分とモジュールのすべての可能なカテゴリをカバーします。



問題のステートメントとその解決策


TCのモジュールは、拡張子がWCX、WFX、WLX、WDXで、エクスポートされた関数の特定のセットを含むDLLモジュールです(モジュールのカテゴリに応じて)。 すべては問題ありませんが、すべての著者が64ビットバージョンの面倒を見ているわけではありません。 そして、原則としてソースコードは利用できません...



質問-既存の32ビットバージョンを使用できますか?

回答-はい、しかしそれほど簡単ではありません。



動的な32ビットライブラリを64ビットプロセスにロードする前に一般化すると、タスクは新しいものではなく、インターネット上のソリューションの検索で待機することはありません。 すべては、ライブラリをロードし、IPCを介してこのプロセスと対話する(プロセス間対話)ことができる代理プロセスを作成することです。 TCのソースにアクセスできず、サロゲートプロセスを操作するメカニズムを追加できません。 しかし、ライブラリを作成することはできます。 ライブラリはモジュールを偽装し、サロゲートプロセスと通信します。サロゲートプロセスはモジュール関数をプルし、結果を返します。 そして、次のようになります。











可能なIPCオプションは、 MSDN-Interprocess Communicationsで入手できます。 私のプロジェクトでは、パイプを選択しました。 これは最速の方法ではないかもしれませんが、サロゲートプロセスの状態を暗黙的に監視できます。 代理プロセスがクラッシュすると、パイプチャネルが壊れ、ライブラリがそれについて学習します。 以下は、進行中のプロセスの説明です。



ライブラリを接続するとき



一意の名前を生成するには、UuidCreate()関数を使用します。 UUID(GUID)を生成します。 それを文字列(UuidToString)に変換し、パイプのパスを入力します。 ブロックモードで動作し、メッセージを送信するパイプ(CreateNamedPipe)を作成します。 代理プロセス(CreateProcess)を実行します。 パイプ名は、コマンドラインパラメーターとして渡されます。 そして、クライアント(ConnectNamedPipe)を待ちます。



ライブラリをオフにするとき



クライアントを切断し(DisconnectNamedPipe)、サロゲートプロセスを終了し(TerminateProcess)、パイプを閉じてリソースをクリーニングします(CloseHandle)



代理プロセスを開始するとき



パイプ(CreateFile)に接続し、ブロッキングモードで動作してメッセージを送信するように構成します。 モジュール(LoadLibrary)をロードし、エクスポートされた関数のアドレス(GetProcAddress)を保存します。 メッセージ待機サイクルに入ります。 必要に応じて、プロセスを完了してサイクルを終了します。



代理プロセスの終了時



パイプから切断し(CloseHandle)、モジュールをアンロードします(FreeLibrary)。



ライブラリから関数を呼び出すとき



例を使用して関数呼び出しを呼び出します

__declspec(dllexport) HANDLE __stdcall OpenArchive(tOpenArchiveData *ArchiveData) { if (s_init && ArchiveData) { // serialize uint8_t *p = s_buff; SET_FUNC(p, OPENARCHIVE) SET_CALLTYPE(p, CALL_QUERY) SET_STR_A(p, ArchiveData->ArcName) SET_INT(p, ArchiveData->OpenMode) // send DWORD writeSize = p - s_buff; DWORD writedSize; while (WriteFile(s_pipe, s_buff, writeSize, &writedSize, NULL)) { assert(writeSize == writedSize); // recv DWORD readedSize; if (ReadFile(s_pipe, s_buff, PIPE_BUFF_SIZE, &readedSize, NULL)) { // deserialize uint8_t *p = s_buff; uint8_t func; GET_FUNC(p, func) uint8_t callType; GET_CALLTYPE(p, callType) if (callType == CALL_ANSWER) { assert(func == OPENARCHIVE); GET_INT(p, ArchiveData->OpenResult) HANDLE r; GET_HANDLE(p, r) // result return r; } else if (callType == CALL_QUERY) { CALL_PROC } assert(0); } } ArchiveData->OpenResult = E_NOT_SUPPORTED; } return NULL; }
      
      





OpenArchiveは、モジュールをロードした後にTCが呼び出す最初の関数です。 タイプtOpenArchiveDataのポインター構造が渡されます。

 typedef struct { char* ArcName; int OpenMode; int OpenResult; char* CmtBuf; int CmtBufSize; int CmtSize; int CmtState; } tOpenArchiveData;
      
      





構造体へのポインタを渡すことはできません。プロセスは分離されており、互いのメモリは表示されません。 また、行(ArcName)へのポインターとフィールドの配置のため、単純にメッセージに構造をコピーするだけでは構造を転送できません。 さらに、一部のフィールドはデータを関数に転送するように設計されており(ArcName、OpenMode)、一部は結果を返すバッファーとして機能し(OpenResult)、後者はまったく使用されません(Cmt *)。 マーシャリング、つまり、データを送信に適した形式に変換する必要があります。 このために、書かれた一連のマクロSET_ *が使用されます。 SET_INTは、intを32ビット数としてバッファーに書き込みます。 SET_STR_Aは、バッファに文字列へのポインタの有効性のサインを書き込み、有効性の場合、文字列のサイズを終端ゼロとポインタが指す文字配列で書き込みます。 バッファの先頭に2つの値が配置されます。それは、どのような機能であるか、それは要求です。 次に、バッファに書き込まれるデータのサイズを計算し、パイプに書き込む必要があります。 反対側からの回答を待ちます。 応答を受信するとき、2つの値を読み取ります。それがどのような関数であるか、それがフィードバック関数の実行に対する応答または要求であるかです。 これが答えであれば、結果を取得し、パーツを構造に書き込み、関数を終了します。 これがフィードバック関数を呼び出す要求である場合、そのパラメータを取得して実行し、結果を返し、次の応答を待ちます(これはすべてCALL_PROCマクロに隠されています)。 それとは別に、考慮された関数の結果のタイプに言及する価値があります。 それはハンドルですが、実際にはポインターです。 TC自体が関数の残りを呼び出すためのパラメーターとして必要になります。 しかし、その重要性はモジュール内でのみ役割を果たします。 32ビットプロセスでは、ポインターは32ビットで、それぞれ64ビットでは64ビットです。 また、32ビットプロセスで作成されます。 したがって、64に変換してから32に変換しても、データが失われることはありません。

2つの関数(SetChangeVolProc、SetProcessDataProc)は、モジュールのフィードバック関数を登録します。 私たちの側では、単にそれらを覚えて、登録の事実を伝えます。 CALL_PROCで必要になります。



メッセージを受信したとき



メッセージ受信サイクル

  while (s_loop) { DWORD readedSize; if (ReadFile(s_pipe, s_buff, PIPE_BUFF_SIZE, &readedSize, NULL)) { // deserialize, process, serialize uint8_t *p = s_buff; uint8_t func; GET_FUNC(p, func) uint8_t callType; GET_CALLTYPE(p, callType) assert(callType == CALL_QUERY); if (func == OPENARCHIVE) { tOpenArchiveData openArchiveData = {0}; GET_STR_A(p, openArchiveData.ArcName) GET_INT(p, openArchiveData.OpenMode) HANDLE r = OpenArchive(&openArchiveData); p = s_buff; SET_FUNC(p, OPENARCHIVE) SET_CALLTYPE(p, CALL_ANSWER) SET_INT(p, openArchiveData.OpenResult) SET_HANDLE(p, r) } else ... ... { assert(0); } DWORD writeSize = p - s_buff; DWORD writedSize; if (!WriteFile(s_pipe, s_buff, writeSize, &writedSize, NULL) || writeSize != writedSize) { return -6; } } else if (GetLastError() != ERROR_TIMEOUT) { break; } }
      
      





すべてがほぼ同じです。 メッセージを受信し、呼び出しを求めている関数を見つけ、マーシャリングの逆プロセス(GET_ *)を実行し、関数を呼び出し、結果を取得してライブラリに送信します。 関数呼び出し中に、コールバック関数が発生する場合があります。

 int __stdcall ChangeVolProc(char *ArcName, int Mode) { uint8_t *p = s_buff; SET_FUNC(p, CHANGEVOLPROC) SET_CALLTYPE(p, CALL_QUERY) SET_STR_A(p, ArcName) SET_INT(p, Mode) DWORD writeSize = p - s_buff; DWORD writedSize; assert(WriteFile(s_pipe, s_buff, writeSize, &writedSize, NULL) || writeSize == writedSize); DWORD readedSize; assert(ReadFile(s_pipe, s_buff, PIPE_BUFF_SIZE, &readedSize, NULL)); p = s_buff; uint8_t func; GET_FUNC(p, func) uint8_t callType; GET_CALLTYPE(p, callType) assert(func == CHANGEVOLPROC && callType == CALL_ANSWER); int r; GET_INT(p, r) return r; }
      
      





シェル関数が呼び出され、これにより(ライブラリとの)接続が行われます。



これにはすべて、代理プロセスの異常終了、スタブ関数の置換、デフォルト値の戻りという形でのエラー処理が伴います。



ソリューションのマイナス面:これはすべてモジュールの速度を低下させます。



おそらくそれだけです...



残っているもの


現実には、解決策を選択する必要がある多くの問題がまだあります。 最小限のデモのみが実装されています。 拡張モジュール内の一連の機能はわずかに大きく、エクスポートテーブルには利用可能なモジュール機能が記載されています。 これに動的に適応することは不可能です。 WLXモジュール、特にウィンドウとの相互作用では、すべてが明確ではありません。 等



完全なソースコードはsourceにあります。 Pelles C for Windowsを使用してビルドできます 。 結果のアプリケーションとライブラリは、モジュール(例:msi.wcxモジュール、msi.exeプログラム、msi.wcx64ライブラリ)に従って名前を変更し、モジュールの横に配置する必要があります。



そして、あなたの意見を知りたい



All Articles