Nimを使用してSteam API呼び出しをWineからGNU / Linuxに、またはその逆に転送する

GNU / Linuxプレーヤーには多くの問題があります。 その1つは、SteamのWindowsゲームごとに個別のSteamクライアントをインストールする必要があることです。 移植されたクロスプラットフォームゲーム用のネイティブSteamクライアントをインストールする必要があるため、状況は悪化しています。



しかし、すべてのゲームに1つのクライアントを使用する方法を見つけたらどうでしょうか? ネイティブクライアントをベースとして、Windowsゲームに、たとえばOpenGLやGNU / Linuxサウンドサブシステムと同じ方法でWineを使用してアクセスさせることができます。 このアプローチの実装については、さらに説明します。







ワインの真実



Wineは、サードパーティ(または英語の用語ではネイティブ)と組み込み(builtin)の2つのモードでWindowsライブラリを使用できます。 サードパーティのライブラリは、 *.dll



拡張子を持つファイルとしてWineによって認識されます。これは、メモリにロードされ、Windowsエンティティと同様に動作する必要があります。 Wineが何も知らないすべてのライブラリで動作するのは、このモードです。 組み込みモードは、Wineがライブラリアクセスを特別な方法で処理し、オペレーティングシステムとそのライブラリにアクセスできる拡張子*.dll.so



で事前に作成されたラッパーにリダイレクトする必要があることを意味します。 詳細についてはこちらをご覧ください







さいわい、Steamクライアントとのやり取りのほとんどは、 steam_api.dll



ライブラリを介してsteam_api.dll



ます。つまり、タスクは、 steam_api.dll.so



ラッパーを実装するsteam_api.dll.so



であり、これはGNU / Linuxのlibsteam_api.so



アクセスします。







このようなラッパーの作成は、よく知られた文書化されたプロセスです。 Windows用のソースライブラリを取得し、 winedump



を使用してその仕様ファイルを取得し、specファイルにすべての関数の実装を記述し、それをwinedump



でコンパイルリンクする必要がwinegcc



ます。 または、 winemaker



にすべてのルーチン作業を依頼してください。







悪魔は細部に宿る



一見、タスクは簡単です。 特に、ソースライブラリにヘッダーファイルがある場合、 winedump



がラッパーを自動的に作成し、ヘッダーファイルがゲーム開発者向けの公式Webサイトで Valveによって公開されることを考慮してください 。 そのため、 winedump



でラッパーを作成し、 winecfg



steam_api.dll



ビルトインモードをオンにしてコンパイルした後、独自のSteamを起動し、ゲーム自体を起動して...ゲームがクラッシュします!







ログを調べます
トレース:steam_api:SteamAPI_RestartAppIfNecessary_((uint32)[非表示])
トレース:steam_api:SteamAPI_RestartAppIfNecessary_()=(bool)0
トレース:steam_api:SteamAPI_Init_()
 Breakpad minidump AppID = [非表示]の設定
 Steam_SetMinidumpSteamID:キャッシュID:[非表示] [APIがロードされていません]
トレース:steam_api:SteamAPI_Init_()=(bool)1
トレース:steam_api:SteamInternal_ContextInit_((void *)0x7ee468)
トレース:steam_api:SteamAPI_GetHSteamPipe_()
トレース:steam_api:SteamAPI_GetHSteamPipe_()=(HSteamPipe)0x1
トレース:steam_api:SteamAPI_GetHSteamUser_()
トレース:steam_api:SteamAPI_GetHSteamUser_()=(HSteamUser)0x1
トレース:steam_api:SteamAPI_GetHSteamPipe_()
トレース:steam_api:SteamAPI_GetHSteamPipe_()=(HSteamPipe)0x1
トレース:steam_api:SteamInternal_CreateInterface_((char *) "SteamClient017")
 wine:アドレス0x7a3a3c92(スレッド0009)の未処理の特権命令、デバッガーの起動...
未処理の例外:32ビットコードの特権命令(0x7a3a3c92)。


注:このログは、上記の方法で生成されたラッパーによって生成されたログよりも有益ですが、これにより問題の本質は変わりません。







ログから判断すると、ラッパーはSteamInternal_CreateInterface



関数がSteamInternal_CreateInterface



まで正確に動作します(!)。 彼女の何が問題なのですか? ドキュメントを読み、ヘッダーファイルと関連付けた後、この関数がSteamClient



クラスのオブジェクトへのポインターを返すことがわかります。







ABI C ++に精通している人は、キャッチが何であるかをすでに理解していると思います。 問題の根本は呼び出し規約です。 C ++標準は、さまざまなコンパイラでコンパイルされたプログラムのバイナリ互換性を意味するものではありません。この場合、Windows用のゲームはMSVCでコンパイルされ、ネイティブのSteamはGCCでコンパイルされました。 すべてのsteam_api.dll



関数steam_api.dll



はC言語の呼び出し規則に従うため、この問題は発生しません。 ゲームがネイティブSteamからSteamClient



クラスのインスタンスを受信し、そのメソッド(thiscallのC ++規約に従う)を呼び出そうとすると、エラーが発生します。 問題を解決するには、まず、使用されているコンパイラの規則間の重要な違いを特定する必要があります。







Msvc Gcc
ECXレジスタにオブジェクトへのポインターを置きます。 最上位のスタック上のオブジェクトへのポインターを見つけることを期待しています。
呼び出されたメソッドによってスタックがクリアされるのを待ちます。 呼び出しコードによってスタックがクリアされるのを待ちます。


[ ソース ]







この段階では、少し余談し、タイトルに示されている問題を解決しようとする試みがすでに行われており、非常に成功していることさえ言及する価値があります。 Windows用とGNU / Linux用の2つの個別のライブラリを使用するSteamBridgeプロジェクトがあります。 WindowsライブラリはMSVCを使用してコンパイルされ、GNU / Linuxライブラリを呼び出します。これはWineに置き換えられ、GCCを使用して同様の方法でコンパイルされます。 メソッドの問題は、Windowsライブラリの側でアセンブラーの挿入を使用し、MSVCコードの側に渡すときに各オブジェクトをラップすることで解決されます。 このソリューションは、アセンブリ用の追加の非クロスプラットフォームコンパイラを必要とし、追加のエンティティを導入するため、いくぶん冗長です。ただし、返されるオブジェクトをラップするという考えは適切です。 借ります!







幸いなことに、Wineは呼び出し規約を使用する方法をすでに知っています。 thiscall



属性を持つメソッドを宣言するだけで十分です。 したがって、すべてのクラスのすべてのメソッドのラッパーを作成する必要があり、メソッドの実装では、元のクラス(ラッパーに格納されているリンク)からメソッドを呼び出すだけです。 ラッパーは次のようになります。







 class ISteamClient_ { public: virtual HSteamPipe CreateSteamPipe() __attribute__((thiscall)); ... // -  private: ISteamClient * internal; }
      
      





 HSteamPipe ISteamClient_::CreateSteamPipe() { TRACE("((ISteamClient *)%p)\n", this); HSteamPipe result = this->internal->CreateSteamPipe(); TRACE("() = (HSteamPipe)%p\n", result); return result; }
      
      





MSVCコードからGCCに転送されたクラス、つまりCCallback



およびCCallResult



に対して、同様の操作を逆方向でのみ実行する必要がありCCallResult



。 最適な解決策は、コード生成のためにスクリプトに委任することであるため、タスクは日常的で面白くありません。 すべてをまとめようと何度か試みた後、ゲームは動き始めます。







ログフラグメント
トレース:steam_api:SteamAPI_RestartAppIfNecessary_((uint32)[非表示])
トレース:steam_api:SteamAPI_RestartAppIfNecessary_()=(bool)0
トレース:steam_api:SteamAPI_Init_()
 Breakpad minidump AppID = [非表示]の設定
 Steam_SetMinidumpSteamID:キャッシュID:[非表示] [APIがロードされていません]
トレース:steam_api:SteamAPI_Init_()=(bool)1
トレース:steam_api:SteamInternal_ContextInit_((void *)0x7ee468)
トレース:steam_api:SteamAPI_GetHSteamPipe_()
トレース:steam_api:SteamAPI_GetHSteamPipe_()=(HSteamPipe)0x1
トレース:steam_api:SteamAPI_GetHSteamUser_()
トレース:steam_api:SteamAPI_GetHSteamUser_()=(HSteamUser)0x1
トレース:steam_api:SteamAPI_GetHSteamPipe_()
トレース:steam_api:SteamAPI_GetHSteamPipe_()=(HSteamPipe)0x1
トレース:steam_api:SteamInternal_CreateInterface_((char *) "SteamClient017")
トレース:steam_api:SteamInternal_CreateInterface_():(ISteamClient *)0x7a7a04c8は(ISteamClient_ *)0x7c49bc70としてラップ
トレース:steam_api:SteamInternal_CreateInterface_()=(ISteamClient_ *)0x7c49bc70
トレース:steam_api:GetISteamUser((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "SteamUser019")
トレース:steam_api:GetISteamUser()=(ISteamUser *)0x7c4bcc40
トレース:steam_api:GetISteamFriends((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "SteamFriends015")
トレース:steam_api:GetISteamFriends()=(ISteamFriends *)0x7c4b8650
トレース:steam_api:GetISteamUtils((ISteamClient *)0x7c49bc70、(HSteamPipe)0x1、(char *) "SteamUtils008")
トレース:steam_api:GetISteamUtils()=(ISteamUtils *)0x7c4b7930
 trace:steam_api:GetISteamMatchmaking((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "SteamMatchMaking009")
トレース:steam_api:GetISteamMatchmaking()=(ISteamMatchmaking *)0x7c4c03c0
トレース:steam_api:GetISteamMatchmakingServers((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "SteamMatchMakingServers002")
トレース:steam_api:GetISteamMatchmakingServers()=(ISteamMatchmakingServers *)0x7c4b5450
トレース:steam_api:GetISteamUserStats((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "STEAMUSERSTATS_INTERFACE_VERSION011")
トレース:steam_api:GetISteamUserStats()=(ISteamUserStats *)0x7c4b5e10
トレース:steam_api:GetISteamApps((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "STEAMAPPS_INTERFACE_VERSION008")
トレース:steam_api:GetISteamApps()=(ISteamApps *)0x7c4b73a0
トレース:steam_api:GetISteamNetworking((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "SteamNetworking005")
トレース:steam_api:GetISteamNetworking()=(ISteamNetworking *)0x7c49cd40
トレース:steam_api:GetISteamRemoteStorage((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "STEAMREMOTESTORAGE_INTERFACE_VERSION014")
トレース:steam_api:GetISteamRemoteStorage()=(ISteamRemoteStorage *)0x7c4c1610
トレース:steam_api:GetISteamScreenshots((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "STEAMSCREENSHOTS_INTERFACE_VERSION003")
トレース:steam_api:GetISteamScreenshots()=(ISteamScreenshots *)0x7c4b70b0
トレース:steam_api:GetISteamHTTP((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "STEAMHTTP_INTERFACE_VERSION002")
トレース:steam_api:GetISteamHTTP()=(ISteamHTTP *)0x7c4b5c50
トレース:steam_api:GetISteamUnifiedMessages((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "STEAMUNIFIEDMESSAGES_INTERFACE_VERSION001")
トレース:steam_api:GetISteamUnifiedMessages()=(ISteamUnifiedMessages *)0x7c49e680
トレース:steam_api:GetISteamController((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "SteamController005")
トレース:steam_api:GetISteamController()=(ISteamController *)0x7c49bfd0
トレース:steam_api:GetISteamUGC((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "STEAMUGC_INTERFACE_VERSION009")
トレース:steam_api:GetISteamUGC()=(ISteamUGC *)0x7c49cad0
トレース:steam_api:GetISteamAppList((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "STEAMAPPLIST_INTERFACE_VERSION001")
トレース:steam_api:GetISteamAppList()=(ISteamAppList *)0x7c49c450
トレース:steam_api:GetISteamMusic((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "STEAMMUSIC_INTERFACE_VERSION001")
トレース:steam_api:GetISteamMusic()=(ISteamMusic *)0x7c49cbf0
トレース:steam_api:GetISteamMusicRemote((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "STEAMMUSICREMOTE_INTERFACE_VERSION001")
トレース:steam_api:GetISteamMusicRemote()=(ISteamMusicRemote *)0x7c49e710
トレース:steam_api:GetISteamHTMLSurface((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "STEAMHTMLSURFACE_INTERFACE_VERSION_003")
トレース:steam_api:GetISteamHTMLSurface()=(ISteamHTMLSurface *)0x7c49ccb0
トレース:steam_api:GetISteamInventory((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "STEAMINVENTORY_INTERFACE_V001")
トレース:steam_api:GetISteamInventory()=(ISteamInventory *)0x7c49d0c0
トレース:steam_api:GetISteamVideo((ISteamClient *)0x7c49bc70、(HSteamUser)0x1、(HSteamPipe)0x1、(char *) "STEAMVIDEO_INTERFACE_V001")
トレース:steam_api:GetISteamVideo()=(ISteamVideo *)0x7c49cb60
トレース:steam_api:SetOverlayNotificationPosition((ISteamUtils *)0x7c4b7930、(ENotificationPosition)0x2)
トレース:steam_api:SteamInternal_ContextInit_((void *)0x7ee468)
トレース:steam_api:SetWarningMessageHook((ISteamUtils *)0x7c4b7930、(SteamAPIWarningMessageHook_t)0x52ebb0)


どうやら、これはおとぎ話の終わりですか? そしていや!







バージョン管理された地獄へようこそ!



私たちのデザインは、利用可能な同じヘッダーファイルを使用してコンパイルされたゲームに対してのみ完全に実行可能であることがすぐに明らかになります。 また、利用可能なSteam APIの最新バージョンのみがあり、Valveは他のバージョンを公開しません(そして、このバージョンはプライベートライセンスで提供されました)。 一方、Steamも最新バージョンですが、これは古いバージョンのSteam APIで動作することを妨げるものではありません。 彼はどのように成功しますか?







答えはログのこの行に隠されています: trace:steam_api:SteamInternal_CreateInterface_ ((char *)"SteamClient017")



。 クライアントはSteamAPIのすべてのバージョンのすべてのクラスに関する情報を保存し、steam_api.dllは目的のバージョンの目的のクラスのインスタンスのみをクライアントに要求することがわかりました。 それが保存されている場所を正確に見つけるためにのみ残っています。 まず、「額」アプローチを試してみましょう。libsteam_api.soで「SteamClient016」の行を見つけてみてください。 なぜ「SteamClient017」ではないのですか? libsteam_api.so



属するバージョンだけでなく、Steam APIクラスのすべてのバージョンの場所を見つける必要があるためです。







 $ grep "SteamClient017" libsteam_api.so   libsteam_api.so  $ grep "SteamClient016" libsteam_api.so $
      
      





libsteam_api.so



類似するlibsteam_api.so



ないようlibsteam_api.so



。 次に、Steamクライアントのすべてのライブラリを試してみてください。







 $ grep "SteamClient017" *.so   steamclient.so    steamui.so  $ grep "SteamClient016" *.so   steamclient.so  $
      
      





そして、ここに必要なものがあります! Gabe Newellのアイコンがある場合はそれをsteamclient.so



、IDAでsteamclient.so



を開きます。 キーワードで簡単に検索すると、興味深い行セットCAdapterSteamClient0XX



が得られますCAdapterSteamClient0XX



はバージョン番号です。 さらに不思議なことに、このファイルにはCAdapterSteamYYYY0XX



という行が含まれていますCAdapterSteamYYYY0XX



は依然としてバージョン番号であり、YYYYは他のすべてのインターフェースのSteam APIの名前です。 相互参照分析を使用すると、このような名前を持つ各クラスの仮想メソッドのテーブルを簡単に見つけることができます。 したがって、各クラスの概要図は次のようになります。



メソッドのテーブルが見つかりましたが、これらのメソッドのシグネチャに関する情報はありません。 しかし、この問題は、メソッドがアクセスしようとしている最大スタック深度を計算することで解決されることが判明しました。 したがって、 steamclient.so



を入力として受け取り、出力ですべてのバージョンのクラスのリストとそのメソッドを受け取るユーティリティを作成できます。 メソッドの変換用のクラスラッパーコードを生成するのは、このリストに基づいたままです。 タスクは単純に見えません。特に、メソッドシグネチャ自体はまだ知られていないことを考えると、メソッドの引数が終了するスタックの深さしかわかりません。 状況は、値による構造体の戻りの特性、つまり、構造体が書き込まれるメモリへの隠された引数ポインタの存在によって悪化します。 すべての呼び出し規約のこのポインターは、呼び出された関数によってスタックから取得されるため、 steamclient.so



のメソッドでret $4



命令を使用して簡単に計算できます。 しかし、そうであっても、重要なコード生成の量は膨大です。







ヒーローの登場



新しいプログラミング言語、または単にあまり人気のないプログラミング言語については、そのニッチについて最初の質問が生じます。 ニムも例外ではありません。 彼は多くの場合、「一度にすべての椅子に座ろう」と批判され、1つの明確な開発方向がない場合に多数の機能が充実していることを暗示しています。 これらの機能のうち、2つを強調表示できます。









この組み合わせにより、ラッパーの作成プロセスが簡単になります。

まず、メインのsteam_api.nim



ファイルと、 steam_api.nims



コンパイルsteam_api.nims



steam_api.nims



ファイルをsteam_api.nims









steam_api.nim
 const specname {.strdefine.} = "steam_api.spec" # spec     ,        `-d:specname=/path/to/steam_api.spec`    {.strdefine.}     `specname`. #    ,       — "steam_api.spec". {.passL: "'" & specname & "'".} #     spec     . #   TRACE    wine,      proc trace*(format: cstring) {.varargs, importc: "TRACE", header: """#include <stdarg.h> #include "wine/debug.h" WINE_DEFAULT_DEBUG_CHANNEL(steam_api);""".} #  varargs ,       ,  importc —         ,  header —        ,   . #  , Nim      TRACE.    ,    TRACE    . #    winedump',           . {.emit:[""" BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, void *reserved) { """, trace, """("(%p, %u, %p)\n", instance, reason, reserved); //     ,        switch (reason) { case DLL_WINE_PREATTACH: return FALSE; /* prefer native version */ case DLL_PROCESS_ATTACH: DisableThreadLibraryCalls(instance); NimMain(); //      Nim break; } return TRUE; } """].}
      
      





steam_api.nims
 --app:lib #    steam_api.dll.so,     --passL:"-mno-cygwin" #     winegcc  --passC:"-mno-cygwin" #       ,   `--`,      --passC:"-D__WINESRC__" #        Nim --os:windows #     linux, wine    WinAPI --noMain #     `DllMain`,   ,  Nim    --cc:gcc #     C #    `switch`,    `--`       switch("gcc.exe", "/usr/bin/winegcc") #         switch("gcc.linkerexe", "/usr/bin/winegcc") #     `switch`  `--` ?
      
      





それほど単純に見えませんが、これは一度に多くのことを振り回したからです。 ここでは、クロスコンパイル、Cヘッダーファイルからの関数のインポート、およびWineのコンパイル機能...明らかな複雑さにもかかわらず、複雑なことは何も起こりませんでしたが、Nimは何も知らず、わからないCソースコードの一部を直接実装しました、同時に、Nimに対してWineヘッダーファイルからTRACEマクロを呼び出す方法を説明しました(これらのファイル自体についても話しました)。







それでは、最もおいしいマクロ、コード生成に移りましょう。 メソッドシグネチャに関する完全な情報はないため、仮想メソッドテーブルをエミュレートするだけでよいため、Cコードからクラスインスタンスをエミュレートします。 そのため、Steam APIのメソッドとクラスを次のように説明するファイルを用意します。







 !CAdapterSteamYYY0XX
 [+] <メソッド1のスタック深度>
 [+] <メソッド2のスタック深度>
 ...


+



記号はオプションであり、隠された引数のインジケータとして機能します。

このファイルは、 steamclient.so



を分析することで取得できます。 テーブルを作成する必要があります。 キーはCAdapterSteamYYYY0XX



形式の文字列で、値はオブジェクトの対応するメソッドを呼び出す関数参照の配列です。これは、 ECX



介して暗黙的に渡される構造体のフィールドです。 これらすべてをアセンブラーで記述することは、特に何らかのロギングを追加するのが良いことを考えると、あまり便利ではないため、最小限のアセンブラーフラグメントを選択します。







フラグメント実行前のスタック
 [...]
 [...]
 [...]
 [返信先アドレス] <= ESP
 [引数1]
 [引数2]
 [???]


 push %ecx #       (   ) push $<    > #      (    ) #     3 (      ) call < Nim> #  ,   Nim add $0x4, %esp #      pop %ecx #     ret $< > #      
      
      





Nim関数を呼び出した後のスタック
 [アセンブラフラグメントでアドレスを返す] <= ESP
 [メソッド番号]
 [オブジェクトポインター=%ecx]
 [返信先]
 [引数1]
 [引数2]
 [???]


フラグメントから戻った後のスタック
 [アセンブラーフラグメントでアドレスを返す]
 [メソッド番号]
 [オブジェクトポインター=%ecx]
 [返信先]
 [引数1]
 [引数2]
 [???] <= ESP


指定されたNim関数を生成するために残ります。 ファイルで見つかったスタックの深さごとに1つの関数を生成し、隠し引数を持つ呼び出しに対してもう1つ生成する必要があります。 以下では、簡潔にするためにこれらの関数を擬似メソッドと呼びます。







このような関数の例を次に示します
 proc pseudoMethod4(methodNo: uint32, obj: ptr WrappedObject, retAddress: pointer, argument1: pointer) : uint64 {.cdecl.} = #   pseudoMethod< > # methodNo -         0 # obj -     # retAddress -      ( ) # argument1 - ,    #  uint64,    ,    64     EAX  EDX  32   EAX. #  cdecl  ,         trace("Method No %d was called for obj=%p and return to %p\n", methodNo, obj, retAddress) trace("(%p)\n", argument1) trace("Origin = %p\n", obj.origin) let vtableaddr = obj.origin.vtable trace("Origins VTable = %p\n", vtableaddr) #         let maddr = cast[ptr proc(obj: pointer argument1: pointer): uint64](cast[uint32](vtableaddr) + methodNo*4) #      trace("Method address to call: %p\n", maddr) let themethod = maddr[] #     trace("Method to call: %p\n", themethod) let res = themethod(obj.origin, argument1) #    (   GCC) trace("Result = %p\n", res) return wrapIfNecessary(res) #   -   ,      .
      
      





wrapIfNecessary



関数の実装をwrapIfNecessary



、上記のフラグメントを生成するコードの説明にwrapIfNecessary



ましょう。 , . , spec- — .







 from strutils import splitLines, split, parseInt from tables import initTable, `[]`, `[]=`, pairs, Table type StackState* = tuple #       depth: int #   swap: bool #     Classes* = Table[string, seq[StackState]] ## ,    :  —   (CAdapterSteamYYY0XX),  —      const cdfile {.strdefine.} = "" #     ,        proc readClasses(): Classes {.compileTime.} = #  compileTime   ,         result = initTable[string, seq[StackState]]() # result —  ,       let filedata = slurp(cdfile) #       `slurp`,           for line in filedata.splitLines(): if line.len == 0: continue elif line[0] == '!': let curstr = line[1..^1] #       result[curstr] = newSeq[StackState]() else: let depth = parseInt(line) let swap = line[0] == '+' #        "+"    #           result[curstr].add((depth: depth, swap: swap)) #          #  ,    result     
      
      





. readClasses



, , : const classes = readClasses()



. -, , .







-
 static: #   static ,        . var declared: set[uint8] = {} #  ,       var swpdeclared: set[uint8] = {} #  ,          proc eachMethod(k: string, methods: seq[StackState], sink: NimNode): NimNode {.compileTime.} = #       `k`      `sink` # NimNode -   .            . result = newStmtList() #     let kString = newStrLitNode k #     ,   # Unified Call Syntax       ,    newStrLitNode(k), k.newStrLitNode()  k.newStrLitNode (   ) result.add quote do: # quote -  ,     ,     ,  `do`        `sink`[`kString`] = newSeq[MethodProc](2) # ,          for i, v in methods.pairs(): if v.swap: #  ,    swpdeclared.incl(v.depth.uint8) #      else: declared.incl(v.depth.uint8) #          . #        `&`. #        . let asmcode = """ push %ecx #       push $0x""" & i.toHex & """ #       call `pseudoMethod""" & $v.depth & (if v.swap: "S" else: "") & # if-elif-else  case-of-else      """` #   add $0x4, %esp #      pop %ecx #       ECX      ret $""" & $(v.depth-4) & """ #        """ var tstr = newNimNode(nnkTripleStrLit) # nnkTripleStrLit          tstr.strVal = asmcode #         let asmstmt = newTree(nnkAsmStmt, newEmptyNode(), tstr) #        `asm """<>"""` let methodname = newIdentNode("m" & k & $i) #     `m< >< >` result.add quote do: #             proc `methodname` () {.asmNoStackFrame, noReturn.} = #   #  asmNoStackFrame   ,       #  noReturn  ,            `asmstmt` #  add(`sink`[`kString`], `methodname`) #  quote          ,       UCS   
      
      





. . , , — Nim, ( ). .







 proc makePseudoMethod(stack: uint8, swp: bool): NimNode {.compileTime.} = ##     . result = newProc(newIdentNode("pseudoMethod" & $stack & (if swp:"S" else: ""))) #       "pseudoMethod< >[S]" #   `quote`   ,      result.addPragma(newIdentNode("cdecl")) #  {.cdecl.} let nargs = max(int(stack div 4) - 1 - int(swp), 0) #          ,    let justargs = genArgs(nargs) #   ,   -      "argument1: uint32"  "argument<nargs>: uint32" let origin = newIdentNode("origin") let rmethod = newIdentNode("rmethod") var mcall = genCall("rmethod", nargs) #    ,   -   "rmethod(argument1, ... , argument<nargs>)" mcall.insert(1, origin) #       var argseq = @[ #    newIdentNode("uint64"), #   newIdentDefs(newIdentNode("methodNo"), newIdentNode("uint32")), #    newIdentDefs(newIdentNode("obj"), newIdentNode("uint32")), #    (   uint32   ) newIdentDefs(newIdentNode("retAddress"), newIdentNode("uint32")), #   ] if swp: #     -   argseq.add(newIdentDefs(newIdentNode("hidden"), newIdentNode("pointer"))) #      argseq &= justargs[1..^1] var originargs = @[ #      newIdentNode("uint64"), newIdentDefs(newIdentNode("obj"), newIdentNode("uint32")), ] & justargs[1..^1] let procty = newTree(nnkProcTy, newTree(nnkFormalParams, originargs), newTree(nnkPragma, newIdentNode("cdecl"))) #     let args = newTree(nnkFormalParams, argseq) result[3] = args #      let tracecall = genTraceCall(nargs) #    ,  -  trace   ,    result.body = quote do: #    trace("Method No %d was called for obj=%p and return to %p\n", methodNo, obj, retAddress) `tracecall` let wclass = cast[ptr WrappedClass](obj) #     -   `uint32`  `ptr WrappedClass` let `origin` = cast[uint32](wclass.origin) trace("Origin = %p\n", `origin`) let vtableaddr = wclass.origin.vtable trace("Origins VTable = %p\n", vtableaddr) let maddr = cast[ptr `procty`](cast[uint32](vtableaddr) + shift*4) trace("Method address to call: %p\n", maddr) let `rmethod` = maddr[] trace("Method to call: %p\n", `rmethod`) if swp: #         ,      let asmcall = genAsmHiddenCall("rmethod", "origin", nargs) #         ,     ,       result.body.add quote do: trace("Hidden before = %p (%p) \n", hidden, cast[ptr cint](hidden)[]) `asmcall` #     trace("Hidden result = %p (%p) \n", hidden, cast[ptr cint](hidden)[]) return cast[uint64](hidden) #           ,  ,         else: #         result.body.add quote do: let res = `mcall` trace("Result = %p\n", res) return wrapIfNecessary(res) #  `wrapIfNecessary`     
      
      





. . quote, , . , .







 macro makeTableOfVTables(sink: untyped): untyped = #         # `sink` - -,    . result = newStmtList() #    result.add quote do: # `sink`      untyped,           ,     NimNode `sink` = initTable[string, seq[MethodProc]]() #    let classes = readClasses() #    readClasses,        for k, v in classes.pairs: result.add(eachMethod(k, v, sink)) #   - for i in declared: # ,  `declared`     ,   ,       eachMethod . result.insert(0, makePseudoMethod(i, false)) #     ,  Nim,   ,      for i in swpdeclared: result.insert(0, makePseudoMethod(i, true)) when declared(debug): #     `-d:debug`,       stdout    , echo(result.repr) #      ,     #     `result`  NimNode   `untyped`,     #   . var vtables: Table[string, seq[MethodProc]] makeTableOfVTables(vtables)
      
      





steam_api.dll



. GNU/Linux Steam API, . , :







CCallback
 proc run(obj: ptr WrappedCallback, p: pointer) {.cdecl.} = #     CCallback. trace("[%p](%p)\n", obj, p) let originRun = (obj.origin.vtable + 0)[] # `+`      ,       let originObj = obj.origin asm """ mov %[obj], %%ecx #          ECX mov %%esp, %%edi # ESP   EDI, ..      push %[p] #     call %[mcall] #   mov %%edi, %%esp #   ::[obj]"g"(`originObj`), [p]"g"(`p`), [mcall]"g"(`originRun`) :"eax", "edi", "ecx", "cc" """
      
      





おわりに



, , Steam API . , , , . Nim . - : « ?». . — echo



( print



Nim). Nim repr



treeRepr



, , .







Nim. , , , .







, , , , . , , :









, , . , , .







github:









, . , , .







, Nim , , echo "Hello, world!"










All Articles