Как я могу вернуть тот же объект (передать по ссылке) при вызове кода C++ из Python (используя ctypes)?

Я пытаюсь взаимодействовать между Python и C++.

Это мой код C++ для тестового метода DLL:

extern "C" __declspec(dllexport) PEParserNamespace::PEParserBase& _cdecl test(PEParserNamespace::PEParserBase* base) {
    printf("the C++ function was called\n");
    base->bytes = 12345;
    return *base;
}

Я пытаюсь использовать его из Python следующим образом:

import ctypes
#DataStructures.py
class PEParserBase(ctypes.Structure):
    _fields_ = [("hFile", ctypes.c_void_p),
        ("dwFileSize", ctypes.c_ulong),
        ("bytes", ctypes.c_ulong),
        ("fileBuffer",ctypes.c_void_p)]
class PEHEADER(ctypes.Structure):
    xc = 0
#FunctionWrapper.py
def testWrapper(peParserBase, _instanceDLL):
    _instanceDLL.test.argtypes = [ctypes.POINTER(PEParserBase)]
    _instanceDLL.test.restype = PEParserBase
    return _instanceDLL.test(ctypes.byref(pEParserBase))

pEParserBase = PEParserBase()
print("hallo welt")
_test = ctypes.CDLL('PeParserPythonWrapper.dll')

print(id(testWrapper(pEParserBase, _test)))
print(id(pEParserBase))

Я ожидал, что testWrapper вернет исходный экземпляр PEParserBase, но это не так — сообщаемые значения id отличаются. Код C++ не создает никаких новых экземпляров PEParserBase или чего-то еще, поэтому я уверен, что проблема должна быть в коде Python.

Почему это происходит и как это исправить?

Вы можете заменить id на ctypes.adressof для сравнения. Если вам действительно нужна здесь идентификация, вы должны сохранить словарь, сопоставляющий адрес с объектом Python, и написать функции для управления этим словарем.

Michael Butscher 30.03.2023 09:16

Добро пожаловать в Stack Overflow. Ничего страшного, если ваш английский не является родным, но, пожалуйста, все же старайтесь писать о проблеме, а не о себе — и старайтесь задавать четкий вопрос, прямо и без разговора. Я отредактировал пост, чтобы исправить текст и соответствовать стандартам сайта. Для получения дополнительной информации, пожалуйста, прочитайте Как задать вопрос и Следует ли удалить слова «Привет», «Спасибо», слоганы и приветствия из сообщений?.

Karl Knechtel 30.03.2023 09:22

@MichaelButscher, кажется, стоит написать в качестве ответа (включая объяснение, почему он не работает как есть; я предполагаю, что существует какая-то неявная копия, связанная с пересечением границы DLL или что-то в этом роде?), если есть нет применимого дубликата (определенно не моя область знаний, поэтому я не могу легко найти его).

Karl Knechtel 30.03.2023 09:29
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
3
88
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Листинг [Python.Docs]: ctypes — внешняя библиотека функций для Python.

У вас есть Undefined Behavior (на самом деле их куча):

  • CTypes (как следует из названия) работает с C. Ссылка — это специфичная для C++ концепция (C ничего о ней не знает).

  • Из-за того, что ссылка на самом деле является адресом памяти (точно так же, как указатель, но с некоторыми ключевыми отличиями), ваш прототип функции Python (retype) неверен. Проверьте [SO]: функция C, вызванная из Python через ctypes, возвращает неверное значение (@ответ CristiFati) для получения более подробной информации.
    Примечание: указание указателя вместо этого помогло бы, но технически это все еще неверно (если кто-то хочет быть строгим)

Объекты CTypes — это оболочки Python ([Python.Docs]: Common Object Structures — type PyObject) над фактическими объектами C.
[Python.Docs]: встроенные функции — id(object) возвращает адрес объекта-оболочки Python (который отличается от адреса обернутого объекта).
Вместо этого вы должны использовать ctypes.addressof.

Я подготовил небольшой пример (я определил структуру C на основе Python и имен членов - также сделал вывод, что вы находитесь на Win (но все идеи в ответе не зависят от ОС)).

  • dl00.cpp:

    #include <stdio.h>
    
    #if defined(_WIN32)
    #  include <Windows.h>
    #  define DLL00_EXPORT_API __declspec(dllexport)
    #else
    #  define DLL00_EXPORT_API
    #endif
    
    
    struct PEParserBase {
        HANDLE hFile;
        DWORD dwFileSize;
        ULONG bytes;
        LPVOID fileBuffer;
    };
    
    typedef PEParserBase *PPEParserBase;
    
    
    #if defined(__cplusplus)
    extern "C" {
    #endif
    
    DLL00_EXPORT_API PPEParserBase func00ptr(PPEParserBase parg);
    
    #if defined(__cplusplus)
    }
    #endif
    
    
    PPEParserBase func00ptr(PPEParserBase parg)
    {
        printf("  C:\n    Address: 0x%0zX\n", reinterpret_cast<size_t>(parg));
        if (parg) {
            printf("    dwFileSize: %u\n    bytes: %u\n", parg->dwFileSize, parg->bytes);
            parg->dwFileSize = 123;
        }
        return parg;
    }
    
  • code00.py:

    #!/usr/bin/env python
    
    import ctypes as cts
    import ctypes.wintypes as wts
    import sys
    
    
    DLL_NAME = "./dll00.{:s}".format("dll" if sys.platform[:3].lower() == "win" else "so")
    
    
    class PEParserBase(cts.Structure):
        _fields_ = (
            ("hFile", wts.HANDLE),
            ("dwFileSize", wts.DWORD),
            ("bytes", wts.ULONG),
            ("fileBuffer", wts.LPVOID),
        )
    
        def __str__(self):
            ret = [self.__repr__()]
            for field, _ in self._fields_:
                ret.append("  {:s}: {:}".format(field, getattr(self, field)))
            return "\n".join(ret)
    
    PEParserBasePtr = cts.POINTER(PEParserBase)
    
    
    def print_ctypes_obj(obj):
        _id = id(obj)
        try:
            _addrof = cts.addressof(obj)
        except TypeError:
            _addrof = 0
        print("Id: 0x{:016X}\nAddressOf: 0x{:016X}\n{:s}\n".format(_id, _addrof, str(obj)))
    
    
    def main(*argv):
        dll = cts.CDLL(DLL_NAME)
        func00ptr = dll.func00ptr
        func00ptr.argtypes = (PEParserBasePtr,)
        func00ptr.restype = PEParserBasePtr
    
        use_ptr = 1
        if use_ptr:
            print("Test pointer export\n")
            pb0 = PEParserBase(None, 3141593, 2718282, None)
            print_ctypes_obj(pb0)
            ppb0 = cts.byref(pb0)
            print_ctypes_obj(ppb0)
            ppb1 = func00ptr(ppb0)
            print()
            print_ctypes_obj(ppb1)
            pb1 = ppb1.contents
            print_ctypes_obj(pb1)
    
    
    if __name__ == "__main__":
        print("Python {:s} {:03d}bit on {:s}\n".format(" ".join(elem.strip() for elem in sys.version.split("\n")),
                                                       64 if sys.maxsize > 0x100000000 else 32, sys.platform))
        rc = main(*sys.argv[1:])
        print("\nDone.\n")
        sys.exit(rc)
    

Выход:

[cfati@CFATI-5510-0:e:\Work\Dev\StackExchange\StackOverflow\q075885080]> sopr.bat
### Set shorter prompt to better fit when pasted in StackOverflow (or other) pages ###

[prompt]>
[prompt]> "c:\Install\pc032\Microsoft\VisualStudioCommunity\2019\VC\Auxiliary\Build\vcvarsall.bat" x64 > nul

[prompt]> cl /nologo /MD /DDLL dll00.cpp  /link /NOLOGO /DLL /OUT:dll00.dll
dll00.cpp
   Creating library dll00.lib and object dll00.exp

[prompt]>
[prompt]> "e:\Work\Dev\VEnvs\py_pc064_03.10_test0\Scripts\python.exe" ./code00.py
Python 3.10.9 (tags/v3.10.9:1dd9be6, Dec  6 2022, 20:01:21) [MSC v.1934 64 bit (AMD64)] 064bit on win32

Test pointer export

Id: 0x0000013F2CB60740
AddressOf: 0x0000013F2C30FE30
<__main__.PEParserBase object at 0x0000013F2CB60740>
  hFile: None
  dwFileSize: 3141593
  bytes: 2718282
  fileBuffer: None

Id: 0x0000013F2CBA72B0
AddressOf: 0x0000000000000000
<cparam 'P' (0x0000013F2C30FE30)>

  C:
    Address: 0x13F2C30FE30
    dwFileSize: 3141593
    bytes: 2718282

Id: 0x0000013F2CB607C0
AddressOf: 0x0000013F2CB60808
<__main__.LP_PEParserBase object at 0x0000013F2CB607C0>

Id: 0x0000013F2CB60AC0
AddressOf: 0x0000013F2C30FE30
<__main__.PEParserBase object at 0x0000013F2CB60AC0>
  hFile: None
  dwFileSize: 123
  bytes: 2718282
  fileBuffer: None


Done.
Ответ принят как подходящий

id() в CPython возвращает адрес объекта-оболочки Python ctypes, а не адрес обернутого объекта. Для этого используйте ctypes.addressof().

ctypes также понимает только типы и структуры простых старых данных C (POD). Он не знает пространств имен или ссылок C++, но поскольку ссылки на самом деле являются синтаксическим сахаром C++ для указателей, вы можете использовать указатель в .argtypes и .restype в качестве замены.

Вот минимальный пример:

test.cpp

#include <stdio.h>

namespace PEParserNamespace {
struct PEParserBase {
    void* hFile;
    unsigned long dwFileSize;
    unsigned long bytes;
    void* fileBuffer;
};
}

extern "C" __declspec(dllexport)
PEParserNamespace::PEParserBase& _cdecl test(PEParserNamespace::PEParserBase* base) {
    printf("the C++ function was called\n");
    base->bytes = 12345;
    return *base;
}

test.py

import ctypes as ct

class PEParserBase(ct.Structure):
    _fields_ = (("hFile", ct.c_void_p),
                ("dwFileSize", ct.c_ulong),
                ("bytes", ct.c_ulong),
                ("fileBuffer",ct.c_void_p))

dll = ct.CDLL('./test')
dll.test.argtypes = ct.POINTER(PEParserBase),
dll.test.restype = ct.POINTER(PEParserBase)    # Use a pointer here for the reference

base = PEParserBase()
pbase = dll.test(ct.byref(base))

print(hex(ct.addressof(base)), base.bytes)
print(hex(ct.addressof(pbase.contents)), base.bytes)  # .contents dereferences the pointer
                                                  # so we get the address of the structure
                                                  # not the address of the pointer itself.

Выход:

the C++ function was called, base=00000169712ADAF0
0x169712adaf0 12345
0x169712adaf0 12345

Другие вопросы по теме