Friends of Total Commander (64 bits) and plugins (32 bits)

    I will tell you about a small project that allows you to use 32 bit versions of extensions in the 64 bit version of Total Commander (hereinafter TC). The project is at the demo stage and allows you to use WCX modules with the minimum necessary set of functions (viewing and extracting the contents of archives or anything that can be represented as archives). Well, at the end, a survey about the relevance of such a solution and bringing the project to a certain level, covering all the possible part of the API and all possible categories of modules.

    Statement of the problem and its solution

    Modules for TC are DLL files with extensions WCX, WFX, WLX, WDX and containing a specific set of exported functions (according to the category of the module). Everything would be fine, but not all authors have taken care of 64 bit versions. And the source code is not available, as a rule ...

    Question - Can I use the existing 32 bit versions?
    Answer - Yes, but not so simple.

    If you generalize before loading a dynamic 32-bit library into a 64-bit process, it turns out that the task is not new and the search for a solution on the Internet will not keep you waiting. It all comes down to creating a surrogate process that can load a library and interact with this process through IPC (inter-process interaction). We do not have access to the TC sources and we cannot add a mechanism for working with a surrogate process. But we can create a library. The library will impersonate the module and communicate with the surrogate process, which, in turn, will pull the module functions and return the result. And it will look like this:



    Possible IPC options are available on MSDN - Interprocess Communications . For my project, I chose Pipes. This may not be the fastest way, but it allows you to implicitly monitor the health of the surrogate process. If the surrogate process crashes, the pipe channel breaks and our library learns about it. The following is a description of the ongoing processes.

    When connecting the library
    • unique name generation for pipe
    • pipe creation
    • creation of a surrogate process
    • passing the pipe name to the surrogate process
    • waiting and connecting a client through pipe

    To generate a unique name, we will use the UuidCreate () function. It will generate a UUID (GUID). Convert it to a string (UuidToString) and fill in the path for pipe. Create a pipe (CreateNamedPipe) working in blocking mode and sending messages. Run the surrogate process (CreateProcess). The pipe name is passed as a command line parameter. And we will wait for the client (ConnectNamedPipe).

    When turning off the library
    • disconnect the client from the pipe
    • complete the surrogate process
    • close pipe (generally free allocated resources)

    Disconnect the client (DisconnectNamedPipe), terminate the surrogate process (TerminateProcess), close the pipe and clean the resources (CloseHandle)

    When the surrogate process starts
    • get the name pipe
    • connect to pipe as a client
    • load module
    • expect message

    Connect to the pipe (CreateFile) and configure it to work in blocking mode and send messages. Load the module (LoadLibrary) and save the addresses of the exported functions (GetProcAddress). We enter the message waiting cycle. If necessary, complete the process and exit the cycle.

    At the end of the surrogate process
    • disconnect from pipe
    • unload module

    Disconnect from the pipe (CloseHandle) and unload the module (FreeLibrary).

    When calling a function from the library
    • pack parameters in a message
    • pass request through pipe
    • get an answer
    • unzip the result and exit the function

    We call a function call using an example
    __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 is the first function that TC calls after loading the module. A pointer structure of type tOpenArchiveData is passed to it.
    typedefstruct {char* ArcName;
        int OpenMode;
        int OpenResult;
        char* CmtBuf;
        int CmtBufSize;
        int CmtSize;
        int CmtState;
      } tOpenArchiveData;
    

    We cannot pass a pointer to the structure, processes are isolated and do not see each other's memory. We also cannot transfer the structure by simply copying it into the message, because of the pointer to the line (ArcName) and alignment of the fields. Plus, some fields are designed to transfer data to a function (ArcName, OpenMode), and some serve as a buffer to return the result (OpenResult), the latter are not used at all (Cmt *). We must marshal, that is, convert the data to a format suitable for transmission. For this, a series of written macros SET_ * are used. SET_INT writes an int as a 32 bit number to the buffer. SET_STR_A writes to the buffer the sign of validity of the pointer to the string, and in the case of validity, writes the size of the string with terminal zero and the array of characters pointed to by the pointer. Two values ​​are placed at the beginning of the buffer: what is this function and what is this request. Next, you need to calculate the size of the data written to the buffer and write them to pipe. Wait for an answer from the other side. When receiving a response, read two values: what kind of function it is and what it is a response or request for the execution of the feedback function. If this is the answer, we get the result, write the part to the structure and exit the function. If this is a request to call a feedback function, we get the parameters for it, execute it, return the result and wait for the next response (all this is hidden in the CALL_PROC macro). We should also mention the type of result of the considered function. It is HANDLE, but in reality a pointer. It will be needed as a parameter for calling the rest of the function by TC itself. But its significance plays a role only within the module. In 32 bit processes, the pointer is 32 bit, in 64, respectively, 64 bit. And it is created in a 32 bit process. Therefore, converting it to 64 and then to 32 will not lead to data loss.
    Two functions (SetChangeVolProc, SetProcessDataProc) register feedback functions in the module. For our part, we simply remember them, and convey the fact of registration. They will be needed in CALL_PROC.

    Upon receipt of a message
    • get a message
    • unzip options
    • call function from extension
    • pack the result
    • send a message with the result

    Message Receive Cycle
    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;
                            }
                    } elseif (GetLastError() != ERROR_TIMEOUT)
                    {
                            break;
                    }
            }
    

    Everything is about the same. We will receive a message, find out what function they are asking to call, perform the reverse process to marshaling (GET_ *), call the function, get the result and send it to the library. During a function call, a callback function may occur.
    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;
    }
    

    Our shell functions are called, which will conduct the connection (with the library).

    All this is accompanied by error handling in the form of abnormal termination of the surrogate process, substitution of stub functions and return of default values.

    The negative side of the solution: all this slows down the speed of the module.

    Perhaps that's all ...

    What's left

    In reality, there are still a number of issues for which solutions must be chosen. Only a minimum of demo is implemented. The set of functions within the extension module is slightly larger, and the export table speaks of the available module features. Dynamically adapt to this is impossible. Not everything is clear with WLX modules, in particular, interaction with a window. Etc.

    The full source code can be found at source . You can build using Pelles C for Windows . The resulting application and library must be renamed in accordance with the module (example: msi.wcx module, msi.exe program, msi.wcx64 library) and put next to the module.

    And I would like to know your opinion

    Only registered users can participate in the survey. Please come in.

    Bring the project to mind


    Also popular now: