📜 ⬆️ ⬇️

We forward Steam API calls from Wine to GNU / Linux and back with Nim

GNU / Linux players have a lot of problems. One of them is the need to install a separate Steam client for each Windows game from Steam. The situation is aggravated by the need to install a native Steam client for ported and cross-platform games.

But what if you find a way to use one client for all games? You can take the native client as a basis, and let Windows games access it just like, for example, OpenGL or the GNU / Linux sound subsystem — Wine. On the implementation of this approach and will be discussed further.


Truth in wine


Wine can work with Windows libraries in two modes: third-party (or native in English terminology) and built-in (builtin). The third-party library is perceived by Wine as a file with the *.dll extension, which needs to be loaded into memory and to work with it, as with the Windows entity. It is in this mode that Wine works with all libraries that it knows nothing about. The built-in mode implies that Wine should process the call to the library in a special way and redirect it to a previously created wrapper with the extension *.dll.so , which can access the operating system and its libraries. Read more about it here .


Fortunately, most of the interaction with the Steam client occurs just through the steam_api.dll library, which means the task is reduced to the implementation of the steam_api.dll.so wrapper, which will refer to the GNU / Linux analogue - libsteam_api.so .


Creating such a wrapper process is well-known and documented . You need to take the source library for Windows, get a spec file for it using winedump , write the implementations of all the functions in the spec file and compile-link it all with the help of winegcc . Or ask the winemaker to do all the routine work.


The devil is in the details


At first glance, the task is simple. Especially considering that winedump can create wrappers automatically in the presence of the header files of the source library, and the header files are published by Valve for game developers on the official website . So, after creating a wrapper through winedump , turning on the built-in steam_api.dll mode in winecfg and compiling, we launched our native Steam, then the game itself and ... The game crashes!


Look in the log
 trace: steam_api: SteamAPI_RestartAppIfNecessary_ ((uint32) [hidden])
 trace: steam_api: SteamAPI_RestartAppIfNecessary_ () = (bool) 0
 trace: steam_api: SteamAPI_Init_ ()
 Setting breakpad minidump AppID = [hidden]
 Steam_SetMinidumpSteamID: Caching Steam ID: [hidden] [API loaded no]
 trace: steam_api: SteamAPI_Init_ () = (bool) 1
 trace: steam_api: SteamInternal_ContextInit_ ((void *) 0x7ee468)
 trace: steam_api: SteamAPI_GetHSteamPipe_ ()
 trace: steam_api: SteamAPI_GetHSteamPipe_ () = (HSteamPipe) 0x1
 trace: steam_api: SteamAPI_GetHSteamUser_ ()
 trace: steam_api: SteamAPI_GetHSteamUser_ () = (HSteamUser) 0x1
 trace: steam_api: SteamAPI_GetHSteamPipe_ ()
 trace: steam_api: SteamAPI_GetHSteamPipe_ () = (HSteamPipe) 0x1
 trace: steam_api: SteamInternal_CreateInterface_ ((char *) "SteamClient017")
 wine: Unhandled privileged instruction at address 0x7a3a3c92 (thread 0009), starting debugger ...
 Unhandled exception: privileged instruction in 32-bit code (0x7a3a3c92).

Note: this log is more informative than that generated by the wrapper generated by the method described above, but this does not change the essence of the problem.


Judging by the log, our wrapper works (!) Exactly until the moment when the SteamInternal_CreateInterface function is SteamInternal_CreateInterface . What is wrong with her? After reading the documentation and correlating it with the header files, we find that this function returns a pointer to an object of the SteamClient class.


I think those who are familiar with ABI C ++ have already understood what the catch is. The root of the problem in calling conventions. The C ++ standard does not imply binary compatibility of programs compiled by different compilers, and in our case the game for windows is compiled into MSVC, while native Steam is in GCC. Since all calls to the steam_api.dll function follow the C language calling conventions, this problem is not observed. As soon as the game receives an instance of the SteamClient class from its native Steam and tries to call its method (which follows the C ++ agreement of thiscall), an error occurs. To fix the problem, you should first identify the key differences in conventions for the used compilers.


MSVCGcc
Places a pointer to an object in the ECX register.Waits to find a pointer to an object in the stack at the top position.
Waiting for the stack to be cleared by the called method.Waiting for the stack to clear the calling code.

[ source ]


At this stage, it is worth making a small digression and mentioning that attempts to solve the problem indicated in the title have already been made, and even quite successfully. There is a SteamBridge project that uses two separate libraries - for Windows and for GNU / Linux. The library for Windows is compiled using MSVC and calls the library for GNU / Linux, which is replaced by Wine and compiled using GCC in a similar pattern. The problem of the methods is solved with the help of assembler inserts on the side of the Windows library and wrapping each object while transferring it in the direction of the MSVC code. This solution is somewhat redundant, since it requires an additional non-platform compiler to build and introduces an extra entity, but the idea of ​​wrapping the returned objects is robust. We will borrow it!


Luckily for us, Wine already has the ability to work with call agreements. It is enough to declare a method with thiscall attribute. Thus, it is necessary to create wrappers for all methods of all classes, and in the implementation of methods simply call methods from the original class (the link to which is stored in a wrapper). The wrapper will look like this:


 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; } 

A similar operation, only in the opposite direction, should be carried out for classes transferred from MSVC code to GCC, namely CCallback and CCallResult . The task is routine and uninteresting, because the best solution would be to delegate it to the script for code generation. After several attempts to put everything together, the game begins to work.


Log fragment
 trace: steam_api: SteamAPI_RestartAppIfNecessary_ ((uint32) [hidden])
 trace: steam_api: SteamAPI_RestartAppIfNecessary_ () = (bool) 0
 trace: steam_api: SteamAPI_Init_ ()
 Setting breakpad minidump AppID = [hidden]
 Steam_SetMinidumpSteamID: Caching Steam ID: [hidden] [API loaded no]
 trace: steam_api: SteamAPI_Init_ () = (bool) 1
 trace: steam_api: SteamInternal_ContextInit_ ((void *) 0x7ee468)
 trace: steam_api: SteamAPI_GetHSteamPipe_ ()
 trace: steam_api: SteamAPI_GetHSteamPipe_ () = (HSteamPipe) 0x1
 trace: steam_api: SteamAPI_GetHSteamUser_ ()
 trace: steam_api: SteamAPI_GetHSteamUser_ () = (HSteamUser) 0x1
 trace: steam_api: SteamAPI_GetHSteamPipe_ ()
 trace: steam_api: SteamAPI_GetHSteamPipe_ () = (HSteamPipe) 0x1
 trace: steam_api: SteamInternal_CreateInterface_ ((char *) "SteamClient017")
 trace: steam_api: SteamInternal_CreateInterface_ (): (ISteamClient *) 0x7a7a04c8 wrapped as (ISteamClient_ *) 0x7c49bc70
 trace: steam_api: SteamInternal_CreateInterface_ () = (ISteamClient_ *) 0x7c49bc70
 trace: steam_api: GetISteamUser ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "SteamUser019")
 trace: steam_api: GetISteamUser () = (ISteamUser *) 0x7c4bcc40
 trace: steam_api: GetISteamFriends ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "SteamFriends015")
 trace: steam_api: GetISteamFriends () = (ISteamFriends *) 0x7c4b8650
 trace: steam_api: GetISteamUtils ((ISteamClient *) 0x7c49bc70, (HSteamPipe) 0x1, (char *) "SteamUtils008")
 trace: steam_api: GetISteamUtils () = (ISteamUtils *) 0x7c4b7930
 trace: steam_api: GetISteamMatchmaking ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "SteamMatchMaking009")
 trace: steam_api: GetISteamMatchmaking () = (ISteamMatchmaking *) 0x7c4c03c0
 trace: steam_api: GetISteamMatchmakingServers ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "SteamMatchMakingServers002")
 trace: steam_api: GetISteamMatchmakingServers () = (ISteamMatchmakingServers *) 0x7c4b5450
 trace: steam_api: GetISteamUserStats ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "STEAMUSERSTATS_INTERFACE_VERSION011")
 trace: steam_api: GetISteamUserStats () = (ISteamUserStats *) 0x7c4b5e10
 trace: steam_api: GetISteamApps ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "STEAMAPPS_INTERFACE_VERSION008")
 trace: steam_api: GetISteamApps () = (ISteamApps *) 0x7c4b73a0
 trace: steam_api: GetISteamNetworking ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "SteamNetworking005")
 trace: steam_api: GetISteamNetworking () = (ISteamNetworking *) 0x7c49cd40
 trace: steam_api: GetISteamRemoteStorage ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "STEAMREMOTESTORAGE_INTERFACE_VERSION014")
 trace: steam_api: GetISteamRemoteStorage () = (ISteamRemoteStorage *) 0x7c4c1610
 trace: steam_api: GetISteamScreenshots ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "STEAMSCREENSHOTS_INTERFACE_VERSION003")
 trace: steam_api: GetISteamScreenshots () = (ISteamScreenshots *) 0x7c4b70b0
 trace: steam_api: GetISteamHTTP ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "STEAMHTTP_INTERFACE_VERSION002")
 trace: steam_api: GetISteamHTTP () = (ISteamHTTP *) 0x7c4b5c50
 trace: steam_api: GetISteamUnifiedMessages ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "STEAMUNIFIEDMESSAGES_INTERFACE_VERSION001")
 trace: steam_api: GetISteamUnifiedMessages () = (ISteamUnifiedMessages *) 0x7c49e680
 trace: steam_api: GetISteamController ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "SteamController005")
 trace: steam_api: GetISteamController () = (ISteamController *) 0x7c49bfd0
 trace: steam_api: GetISteamUGC ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "STEAMUGC_INTERFACE_VERSION009")
 trace: steam_api: GetISteamUGC () = (ISteamUGC *) 0x7c49cad0
 trace: steam_api: GetISteamAppList ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "STEAMAPPLIST_INTERFACE_VERSION001")
 trace: steam_api: GetISteamAppList () = (ISteamAppList *) 0x7c49c450
 trace: steam_api: GetISteamMusic ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "STEAMMUSIC_INTERFACE_VERSION001")
 trace: steam_api: GetISteamMusic () = (ISteamMusic *) 0x7c49cbf0
 trace: steam_api: GetISteamMusicRemote ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "STEAMMUSICREMOTE_INTERFACE_VERSION001")
 trace: steam_api: GetISteamMusicRemote () = (ISteamMusicRemote *) 0x7c49e710
 trace: steam_api: GetISteamHTMLSurface ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "STEAMHTMLSURFACE_INTERFACE_VERSION_003")
 trace: steam_api: GetISteamHTMLSurface () = (ISteamHTMLSurface *) 0x7c49ccb0
 trace: steam_api: GetISteamInventory ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "STEAMINVENTORY_INTERFACE_V001")
 trace: steam_api: GetISteamInventory () = (ISteamInventory *) 0x7c49d0c0
 trace: steam_api: GetISteamVideo ((ISteamClient *) 0x7c49bc70, (HSteamUser) 0x1, (HSteamPipe) 0x1, (char *) "STEAMVIDEO_INTERFACE_V001")
 trace: steam_api: GetISteamVideo () = (ISteamVideo *) 0x7c49cb60
 trace: steam_api: SetOverlayNotificationPosition ((ISteamUtils *) 0x7c4b7930, (ENotificationPosition) 0x2)
 trace: steam_api: SteamInternal_ContextInit_ ((void *) 0x7ee468)
 trace: steam_api: SetWarningMessageHook ((ISteamUtils *) 0x7c4b7930, (SteamAPIWarningMessageHook_t) 0x52ebb0)

It would seem: is the end of a fairy tale? And no!


Welcome to versioned hell!


It soon becomes clear that our design is fully viable only for games compiled using the same header files that we have available. And we only have the latest version of the Steam API, other versions of Valve do not publish (and this was given under a closed license). On the other hand, we also have the latest version of Steam, but this does not prevent it from working with old versions of the Steam API. How does he do it?


The answer is hidden in this line of the log: trace:steam_api:SteamInternal_CreateInterface_ ((char *)"SteamClient017") . It turns out that the client stores information about all the classes of all versions of SteamAPI, and steam_api.dll only requests the client with an instance of the desired class of the desired version. It remains only to find exactly where it is stored. To begin with, we will try the “forehead” approach: we will try to find the string "SteamClient016" in libsteam_api.so . Why not "SteamClient017"? Because we need to find the location of all versions of the Steam API classes, and not just the version to which libsteam_api.so belongs.


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

It looks like libsteam_api.so has nothing like that. Then we will try to go through all the libraries of the Steam client.


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

And here is what we need! Hang the Gabe Newell icon, if any, and open steamclient.so in IDA. A quick keyword search yields a curious set of strings: CAdapterSteamClient0XX , where XX is the version number. What is even more curious is that the file contains the lines CAdapterSteamYYYY0XX , where XX is still the version number, and YYYY is the name of the Steam API for all other interfaces. Analysis of cross-references allows you to effortlessly find a table of virtual methods for each of the classes with such names. Thus, the total scheme for each class will look like this:

The method table is found, only we have absolutely no information about the signatures of these methods. But this problem was solved by calculating the maximum depth of the stack to which the method is trying to gain access. So you can make a utility that will receive steamclient.so as input, and create a list of classes of all versions, as well as their methods, as output. It remains only on the basis of this list to generate the class wrapper code for the method conversion. The task does not look simple, especially considering that the method signatures themselves are still not known to us, we only know the stack depth at which the method arguments end. The situation is aggravated by the peculiarities of the return of some structures by value, namely the presence of a hidden argument to the memory where the structure should be written. This pointer, in all calling conventions, is retrieved from the stack by the called function, because it can be easily calculated using the ret $4 instruction in methods from steamclient.so . But even so, the amount of non-trivial code generation is huge.


The appearance of the hero


For any new or just not very popular programming language, the first question is about its niche. Nim is no exception. He is often criticized for trying to “sit on all chairs at once,” implying fullness with a large number of features in the absence of one clear direction of development. Among these features, two can be particularly highlighted:



It is this combination that will make the process of writing a wrapper painless as a result.
First, create the main steam_api.nim file and the file with the steam_api.nims compilation 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`  `--` ? 

It does not look very simple, but this is only due to the fact that we swung at many things at once. Here, both cross-compilation, and importing functions from C header files, and compilation features for Wine ... Despite seeming complexity, nothing complicated happened, we just directly implemented some parts of C source code that Nim doesn’t know anything about and cannot , and at the same time described for Nim how to call the TRACE macro from the header files of Wine (these files were also told about themselves).


We now turn to the most delicious - macros and code generation. Since we do not have complete information on method signatures, we will emulate instances of classes from C code, since we only need to emulate a virtual method table. So, let us have a file that describes the methods and classes of the Steam API as follows:


 ! CAdapterSteamYYY0XX
 [+] <method 1 stack depth>
 [+] <method 2 stack depth>
 ...

The + sign is optional and will serve as an indicator of a hidden argument.
This file can be obtained by analyzing steamclient.so . It should make a table. The keys to it will be strings of the form CAdapterSteamYYYY0XX , and the values ​​will be an array of references to functions that call the corresponding methods in an object that is implicitly passed to the structure field passed to them through the ECX . Writing all of this in assembly language is not very convenient, especially considering that it would be nice to add some kind of journaling, so select the minimum assembler fragment:


Stack to the execution of the fragment
 [...]
 [...]
 [...]
 [return address] <= ESP
 [argument 1]
 [argument 2]
 [???]

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

Stack after calling Nim function
 [assembly return address] <= ESP
 [method number]
 [object pointer =% ecx]
 [return address]
 [argument 1]
 [argument 2]
 [???]

Stack after returning from fragment
 [return address to assembler fragment]
 [method number]
 [object pointer =% ecx]
 [return address]
 [argument 1]
 [argument 2]
 [???] <= ESP

It remains to generate the designated functions Nim. It is necessary to generate one function for each depth of the stack encountered in the file and one more for calls with a hidden argument. We will call these functions pseudo-methods for brevity.


Here is an example of such a function.
 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) #   -   ,      . 

Let’s leave behind the implementation of the wrapIfNecessary function and proceed to the description of the code that generates the fragments described above. , . , 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" """ 

Conclusion


, , Steam API . , , , . Nim . - : « ?». . — echo ( print Nim). Nim repr treeRepr , , .


Nim. , , , .


, , , , . , , :



, , . , , .


github:



, . , , .


, Nim , , echo "Hello, world!" .


')

Source: https://habr.com/ru/post/349388/


All Articles