Search code examples
pythonc++windowsctypesfile-mapping

Getting c_char_p value from a file map causes a python program to crash


I'm trying to create a program that will take a string (const char[]) from a c++ application, create a map file object using windows.h while a separately launched python script will read the map file object and take the pointer to the string and print it.

I wrote the c++ code that writes to a file map and it works fine, since another c++ program that's supposed to read the map data does show the correct string. When I tried to rewrite the 'reading' code to python using ctypes, I encountered a problem, where python crashes each time I try to access the ctypes.c_char_p data's value. When I remove the .value part, the program succesfully executes and prints, I'm guessing, a pointer address. See the code below. (*if there's a better approach that does not use other libraries or you notice some mistakes or improvements I could do here, please do tell)

write.cpp - C++ code that creates a file map and writes "something".

#include<Windows.h>
#include<memoryapi.h>
#include<iostream>

int main(){
    //Create a file map.
    HANDLE fileMappingObject = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(char)*256, L"testingFileMapping");
    //Creating a pointer to the data.
    char *mapData = (char*)MapViewOfFile(fileMappingObject, FILE_MAP_WRITE, 0, 0, 0);
    //Writing to the pointer 'something'
    strcpy_s(mapData, 256, "something"); //write to

    //Just making sure the program doesn't quit.
    int tmp;
    while(true){
        std::cin >> tmp;
        if(tmp == 0) break; 
    }
    //Close - cleaning up
    UnmapViewOfFile(mapData);
    CloseHandle(fileMappingObject);
    return 0;
}

read.cpp - C++ code that reads from a file map and prints "something" to the console.

#include<Windows.h>
#include<memoryapi.h>
#include<iostream>

int main(){
    //create, or in this case, access an existing file map
    HANDLE fileMappingObject = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, sizeof(char)*256, L"testingFileMapping");
    //get the pointer
    char *mapData = (char*)MapViewOfFile(fileMappingObject, FILE_MAP_READ, 0, 0, 0);
    //print the value;
    std::cout << mapData;
//PRINTS: something

    //Just making sure the program doesn't quit.
    int tmp = 1;
    while(true){
        std::cin >> tmp;
        if(tmp == 0) break; 
    }
    //Close - cleaning up
    UnmapViewOfFile(mapData);
    CloseHandle(mapData);
    return 0;
}

read.py - read.cpp version in python.

from ctypes import wintypes
from ctypes import windll
import ctypes
#define some values
INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value  #get the value of the arg
PAGE_READWRITE = 0x04 #hex numb of the arg
buffer_size = 256 * ctypes.sizeof(ctypes.c_char) #256 is the max numb of characters

#Python version of creating - accessing a file map
fileMappingObject = windll.kernel32.CreateFileMappingW(wintypes.HANDLE(INVALID_HANDLE_VALUE), None, PAGE_READWRITE, 0, buffer_size, wintypes.LPWSTR("testingFileMapping"))
#Getting the pointer
mapData = windll.kernel32.MapViewOfFile(fileMappingObject, 0x04, 0, 0, 256)

##BELOW THE CODE EXPLANATION OF WHAT IS HAPPENING FROM HERE
if mapData == 0:
    print("Couldn't access or open data")
else:
    data = ctypes.c_char_p(mapData).value.decode("utf-8")
    print("data: ", data)
##TO HERE

#cleaning up
windll.kernel32.UnmapViewOfFile(mapData)
windll.kernel32.CloseHandle(fileMappingObject)  

So, if my write.cpp compiled file write.exe isn't open at the moment, mapData is indeed 0. However, when the c++ application is open, the python crashes and I can't (well, couldn't) get the error with try. If I remove the .value.decode("utf-8") part, the code executes and prints this: data: c_char_p(18446744073353297920), data: c_char_p(598343680), etc.. The numbers are never the same*

What is the problem and what is the solution? An in-depth answer is appreciated, even if I don't understand it.


Solution

  • The main problem is not defining .argtypes and .restype for your functions to ctypes makes no assumptions about types and can type check. Particularly in your case, the LPVOID return value wasn't declared, so ctypes defaults to ctype.c_int for return values which is typically 32-bit on a 64-bit OS. The returned pointer on a 64-bit OS is 64 bits and if the actual value is larger than can fit in 32 bits, it is truncated.

    Below is a fully declared set of functions to map and unmap file views, with a Python example. .errcheck is also set for each function, so they cannot fail silently:

    import ctypes as ct
    import ctypes.wintypes as w
    
    NO_ERROR = 0
    ERROR_INVALID_HANDLE = 6
    ERROR_ALREADY_EXISTS = 183
    
    INVALID_HANDLE_VALUE = w.HANDLE(-1).value
    
    PAGE_EXECUTE_READ = 0x20
    PAGE_EXECUTE_READWRITE = 0x40
    PAGE_EXECUTE_WRITECOPY = 0x80
    PAGE_READONLY = 0x02
    PAGE_READWRITE = 0x04
    PAGE_WRITECOPY = 0x08
    
    SEC_COMMIT = 0x8000000
    SEC_IMAGE = 0x1000000
    SEC_IMAGE_NO_EXECUTE = 0x11000000
    SEC_LARGE_PAGES = 0x80000000
    SEC_NOCACHE = 0x10000000
    SEC_RESERVE = 0x4000000
    SEC_WRITECOMBINE = 0x40000000
    
    FILE_MAP_READ = 0x04
    FILE_MAP_WRITE = 0x02
    
    # Overkill declaration since we pass NULL, but just to be complete...
    class SECURITY_ATTRIBUTES(ct.Structure):
        _fields_ = [('nLength', w.DWORD),
                    ('lpSecurityDescriptor', w.LPVOID),
                    ('bInheritHandle', w.BOOL)]
    
    def nullcheck(result, func, args):
        if result is None:
            raise ct.WinError(ct.get_last_error())
        return result
    
    def zeroerrorcheck(result, func, args):
        if not result:
            err = ct.get_last_error()
            if err != NO_ERROR:
                raise ct.WinError(err)
        return result
    
    # Not declared by ctypes.wintypes
    LPSECURITY_ATTRIBUTES = ct.POINTER(SECURITY_ATTRIBUTES)
    SIZE_T = ct.c_size_t
    
    kernel32 = ct.WinDLL('kernel32', use_last_error=True)
    # Arguments and return types strictly match MSDN function descriptions.
    CreateFileMapping = kernel32.CreateFileMappingW
    CreateFileMapping.argtypes = w.HANDLE, LPSECURITY_ATTRIBUTES, w.DWORD, w.DWORD, w.DWORD, w.LPCWSTR
    CreateFileMapping.restype = w.HANDLE
    CreateFileMapping.errcheck = nullcheck
    MapViewOfFile = kernel32.MapViewOfFile
    MapViewOfFile.argtypes = w.HANDLE, w.DWORD, w.DWORD, w.DWORD, SIZE_T
    MapViewOfFile.restype = w.LPVOID
    MapViewOfFile.errcheck = nullcheck
    UnmapViewOfFile = kernel32.UnmapViewOfFile
    UnmapViewOfFile.argtypes = w.LPCVOID,
    UnmapViewOfFile.restype = w.BOOL
    UnmapViewOfFile.errcheck = zeroerrorcheck
    CloseHandle = kernel32.CloseHandle
    CloseHandle.argtypes = w.HANDLE,
    CloseHandle.restype = w.BOOL
    CloseHandle.errcheck = zeroerrorcheck
    
    # Create a mapping and write a value.
    # (This could be in a different Python instance as long as it isn't closed
    #  before being read)
    h1 = CreateFileMapping(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, 256, 'testingFileMapping')
    p1 = MapViewOfFile(h1, FILE_MAP_WRITE, 0, 0, 256)
    s = '你好, 马克!'.encode()
    ct.memmove(p1, s, len(s))
    
    # Create a different handle to the same mapping and read it.
    h2 = CreateFileMapping(INVALID_HANDLE_VALUE, None, PAGE_READWRITE, 0, 256, 'testingFileMapping')
    p2 = MapViewOfFile(h2, FILE_MAP_READ, 0, 0, 256)
    print(hex(p2))
    print(ct.string_at(p2).decode())
    UnmapViewOfFile(p2)
    CloseHandle(h2)
    UnmapViewOfFile(p1)
    CloseHandle(h1)
    

    Output (Note the returned LPVOID value is larger than a 32-bit value can hold):

    0x28ba2630000
    你好, 马克!