2025, Nov 15 15:02
Вызов методов C++ DLL из Python через ctypes и C++‑шим
Пошаговое руководство: как вызывать методы C++ DLL из Python через ctypes, работая с перемангленными именами, std::wstring и GetProcAddress, добавив C++‑шим.
Вызывать функции из C++ DLL в Python быстро становится непросто, когда вы не управляете библиотекой и у вас нет её заголовков. Особенно деликатной ситуация становится, как только в игру вступают методы классов и специфичные для C++ типы вроде std::wstring. Ниже — практическое руководство, как связать такую DLL с Python с помощью ctypes и небольшого C++‑шима, опираясь только на экспортируемые перемангленные имена.
С чем придётся иметь дело
DLL экспортирует перемангленные символы примерно такого вида:
2980 BA3 005A3060 ?getFoo@FooLib@@YAAEAVFoo@1@XZ
2638 A4D 005A3020 ?getApplicationData@Foo@FooLib@@QEAAAEAVApplicationData@2@XZ
2639 A4E 005A3030 ?getApplicationData@Foo@FooLib@@QEBAAEBVApplicationData@2@XZ
2738 AB1 000F8A30 ?getDataRootPath@ApplicationData@FooLib@@QEBA?AV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@XZ
Расшифровав их, получаем такие сигнатуры:
Foo __cdecl FooLib::getFoo()
ApplicationData& __thiscall FooLib::Foo::getApplicationData()
const ApplicationData& __thiscall FooLib::Foo::getApplicationData() const
std::wstring __thiscall FooLib::ApplicationData::getDataRootPath() const
Из Python легко найти и вызвать первые две через ctypes, трактуя всё как непрозрачные указатели:
from ctypes import *
lib = WinDLL(r"c:\\path\\to\\Foo.dll")
sym_getFoo = getattr(lib, "?getFoo@FooLib@@YAAEAVFoo@1@XZ")
sym_getFoo.argtypes = []
sym_getFoo.restype = c_void_p
sym_getApp = getattr(lib, "?getApplicationData@Foo@FooLib@@QEAAAEAVApplicationData@2@XZ")
sym_getApp.argtypes = [c_void_p]
sym_getApp.restype = c_void_p
sym_getApp(sym_getFoo()) # стабильное значение указателя возвращается многократно
Однако экспорт, возвращающий std::wstring, таким способом не вызывается — это приводит к сбоям или нарушениям доступа:
sym_getPath = getattr(lib, "?getDataRootPath@ApplicationData@FooLib@@QEBA?AV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@XZ")
Здесь мешают две вещи. Во‑первых, ctypes не умеет маршалить типы, доступные только в C++, такие как std::wstring. Во‑вторых, методы усложняют форму вызова, а __thiscall ctypes не поддерживает. Нужен небольшой C++‑шим, который на одной стороне говорит на ABI DLL, а на другой — на обычном C‑ABI.
Ключевая идея: как вызвать метод по адресу
В MSVC x86‑64 вызов метода класса эквивалентен вызову свободной функции с неявным первым параметром — указателем this. Концептуально это равносильно следующему:
struct S {
int f(int arg);
};
int f(S *self, int arg);
Значит, получив адрес метода и приведя его к указателю на функцию, принимающую объект первым аргументом, можно вызывать его как обычную функцию. На x64 это избавляет от необходимости отдельно обрабатывать __thiscall.
Соберите шим, который превращает C++‑результат в C‑строку
Самый чистый путь — загрузить перемангленные функции через GetProcAddress, вызвать их, передав скрытый указатель this первым параметром, получить std::wstring на стороне C++, затем преобразовать его в размещённый в куче char*, который Python воспримет как C‑строку. Отдельная функция‑освободитель проясняет владение.
namespace {
class AppCtx {
public:
std::wstring getDataRootPath() const;
};
class FooHost {
public:
AppCtx& getApplicationData();
const AppCtx& getApplicationData() const;
};
FooHost& fetchFoo();
}
char *emitFlatString() {
using FnGetFoo = FooHost&();
auto *pGetFoo = (FnGetFoo*)GetProcAddress("?getFoo@FooLib@@YAAEAVFoo@1@XZ");
using FnGetApp = AppCtx&(FooHost&);
auto *pGetApp = (FnGetApp*)GetProcAddress("?getApplicationData@Foo@FooLib@@QEAAAEAVApplicationData@2@XZ");
using FnGetRoot = std::wstring(AppCtx&);
auto *pGetRoot = (FnGetRoot*)GetProcAddress("?getDataRootPath@ApplicationData@FooLib@@QEBA?AV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@XZ");
std::wstring ws = pGetRoot(pGetApp(pGetFoo()));
char *buf = (char*)std::malloc(ws.size() + 1);
for (size_t i = 0; i <= ws.size(); ++i) {
buf[i] = ws[i]; // предполагается отсутствие не-ASCII символов
}
return buf;
}
void releaseFlatString(char *p) {
std::free(p);
}
В этом шиме фиктивные C++‑классы нужны лишь для локального выражения типов; совместимость по макетам данных не предполагается. Более того, можно вовсе убрать эти определения и приводить всё к void*, сохраняя корректную семантику вызова.
Если предпочитаете полностью непрозрачные указатели
Ту же идею можно выразить, используя только void* там, где фигурируют ссылки и указатели. Шим получится ещё компактнее, без вводимых названий классов.
using FGetFoo = void*();
auto *fnGetFoo = (FGetFoo*)GetProcAddress("?getFoo@FooLib@@YAAEAVFoo@1@XZ");
using FGetApp = void*(void*);
auto *fnGetApp = (FGetApp*)GetProcAddress("?getApplicationData@Foo@FooLib@@QEAAAEAVApplicationData@2@XZ");
using FGetRoot = std::wstring(void*);
auto *fnGetRoot = (FGetRoot*)GetProcAddress("?getDataRootPath@ApplicationData@FooLib@@QEBA?AV?$basic_string@_WU?$char_traits@_W@std@@V?$allocator@_W@2@@std@@XZ");
Дальше вызываете fnGetRoot(fnGetApp(fnGetFoo())) тем же способом, копируете символы в заново выделенный буфер C и возвращаете его в Python вместе с парной функцией освобождения.
Альтернативный вариант: передавать указатели на функции из Python в шим
Поскольку ctypes умеет получать указатель на функцию, можно передать этот адрес в шим и вызвать его в C++, приведя к нужной сигнатуре.
char *emitFlatString2(
void *app_from_python,
void *getRoot_from_python
) {
using RootCall = std::wstring(void*);
auto *pRoot = (RootCall*)getRoot_from_python;
std::wstring ws = pRoot(app_from_python);
char *out = (char*)std::malloc(ws.size() + 1);
for (size_t i = 0; i <= ws.size(); ++i) {
out[i] = ws[i]; // предполагается, что содержимое только ASCII
}
return out;
}
void releaseFlatString2(char *p) {
std::free(p);
}
На стороне Python вы получите и указатель на объект, и указатель на функцию через getattr по перемангленному имени, после чего передадите эти непрозрачные значения в шим.
Почему это работает?
Ключ — относиться к вызову метода как к вызову свободной функции, принимающей объект первым параметром. Именно так ABI MSVC на x86‑64 понижает такие вызовы, поэтому, зная адрес символа, можно вызвать его, приведя к типу указателя на функцию, которая явно принимает скрытый указатель this. Это обходит проблемы с __thiscall и оставляет ctypes в комфортной зоне — вы не просите его маршалить C++‑типы.
Вторая составляющая — изоляция типов, существующих только в C++. std::wstring через ctypes не проходит. Вместо этого потребляйте его внутри шима и возвращайте обычный char* в Python плюс парную функцию освобождения. Тонкая прослойка экспорта extern "C" вокруг API шима на базе char* — это нормально; не экспортируйте функции со std::wstring в C‑линковке. Перенос преобразования внутрь обычной C++‑функции избавляет от предупреждений о C‑линковке с C++‑типами и от падений при прямом экспорте таких функций.
Что важно помнить
Такие детали, как способ загрузки DLL и получение GetProcAddress в вашем процессе, — отдельные вопросы, которые здесь не разбираются. Кроме того, в примерах предполагается только ASCII при «уплощении» std::wstring в char*. Если возвращаемый текст содержит не‑ASCII символы, такой наивный копирующий код не даст корректного результата.
Итоги
Когда нужно обратиться к чисто C++ DLL из Python без заголовков, опирайтесь на правило понижения вызова методов на x64 и рассматривайте методы как свободные функции с явным указателем this. Разрешайте символы по перемангленным именам, вызывайте их из C++‑шима и конвертируйте любые C++‑специфичные возвращаемые типы в плоское C‑представление до перехода через границу. Экспортируйте только C‑интерфейс и сопровождайте любые выделенные буферы явным освободителем. Такой подход минимален, надёжен в заданных рамках и избегает неопределённого поведения, возникающего при попытке заставить ctypes работать с C++‑типами или неподдерживаемыми соглашениями о вызовах.