2025, Nov 14 21:03
Как передавать пути в Windows‑DLL из Python через ctypes: char* и wchar_t
ctypes: почему возникает ArgumentError при str для char* и как вызывать Windows‑DLL из Python. c_char_p и c_wchar_p, bytes/str, кодировки ANSI и Unicode.
Связка Python с Windows‑DLL кажется простым делом, пока не всплывает кодировка текста. Достаточно небольшого несоответствия между bytes и str, чтобы получить ctypes.ArgumentError и сорвать вызов. Ниже — как распознать проблему, почему она возникает и как аккуратно исправить её, не переписывая логику.
Как воспроизвести проблему
Рассмотрим минимальную экспортируемую функцию из Windows‑DLL, которая передаёт путь к файлу во внутреннюю процедуру и возвращает bool:
extern "C" {
bool __declspec(dllexport) ApiOpenPath(const char* pPath)
{
return InnerInvoke(pPath);
}
}
И вызывающий код на Python 3.12.9 через ctypes:
import ctypes
mod = ctypes.cdll.LoadLibrary('The.dll')
open_entry = mod.ApiOpenPath
open_entry.argtypes = [ctypes.c_char_p]
open_entry.restype = ctypes.c_bool
path_name = "TheOutput.txt"
res = open_entry(path_name)
print(res)
Запуск этого кода приводит к ошибке вида:
ctypes.ArgumentError: argument 1: TypeError: wrong type
Что именно идёт не так
ctypes.c_char_p ожидает объект bytes. Строковый литерал "TheOutput.txt" — это Python‑овский str. В Windows C‑API, принимающие char* для путей, трактуют байты как ANSI‑кодировку. С чистым ASCII байтовый литерал сработает. Но в имени файла с символами вроде ü байтовый литерал напрямую это не выразит, и даже запись b'pingüino.jpg' — синтаксическая ошибка. Напротив, Python‑овский str соотносится с широкими строками C — wchar_t* и ctypes.c_wchar_p.
Два способа решения
Если сигнатура DLL остаётся const char*, передавайте из Python именно bytes. Для имён, состоящих только из ASCII, используйте байтовый литерал. Если в имени есть не‑ASCII символы, передавайте str, закодированный в bytes через ANSI.
import ctypes
libh = ctypes.cdll.LoadLibrary('The.dll')
call_open = libh.ApiOpenPath
call_open.argtypes = [ctypes.c_char_p]
call_open.restype = ctypes.c_bool
# ASCII подойдёт в виде bytes
ascii_ok = b"TheOutput.txt"
print(call_open(ascii_ok))
# Для не‑ASCII закодируйте в ANSI
non_ascii = 'pingüino.jpg'.encode('ansi')
print(call_open(non_ascii))
Правило с ANSI легко увидеть на примере стандартной библиотеки. Демонстрация с C FILE *fopen(const char *filename, const char *mode) и Python 3.13:
>>> import ctypes as cx
>>> crt = cx.CDLL('msvcrt')
>>> crt.fopen.argtypes = cx.c_char_p, cx.c_char_p
>>> crt.fopen.restype = cx.c_void_p
>>> crt.fopen(b'pingüino.jpg', b'r')
File "<python-input>", line 1
crt.fopen(b'pingüino.jpg', b'r')
^^^^^^^^^^^^^^^
SyntaxError: bytes can only contain ASCII literal characters
>>> crt.fopen('pingüino.jpg'.encode('ansi'), b'r')
140704303274784
Если вы можете изменить DLL, переключите интерфейс на широкие строки. Использование wchar_t* на стороне C и ctypes.c_wchar_p на стороне Python позволит передавать любое поддерживаемое Unicode‑имя файла напрямую как Python‑строку str.
extern "C" {
bool __declspec(dllexport) ApiOpenPathW(const wchar_t* pWidePath)
{
return InnerInvokeW(pWidePath);
}
}
import ctypes as cx
dllh = cx.CDLL('The.dll')
wide_open = dllh.ApiOpenPathW
wide_open.argtypes = [cx.c_wchar_p]
wide_open.restype = cx.c_bool
print(wide_open('pingüino.jpg'))
Тот же принцип относится к широким I/O стандартной библиотеки C. Пример с C FILE *_wfopen(const wchar_t *filename, const wchar_t *mode):
>>> import ctypes as cx
>>> msv = cx.CDLL('msvcrt')
>>> msv._wfopen.argtypes = cx.c_wchar_p, cx.c_wchar_p
>>> msv._wfopen.restype = cx.c_void_p
>>> msv._wfopen('pingüino.jpg', 'r')
140704303274928
Почему это важно
Граница между Python и нативной Windows‑DLL строго типизирована. Передача str туда, где на стороне C ожидается char*, сразу приводит к ошибке типов, а несоответствие кодировок путей — к тонким багам, проявляющимся лишь на реальных именах файлов. Понимание того, как ctypes сопоставляет Python‑типы с C‑указателями, помогает избежать потери времени и хрупких обходных решений.
Вывод
Соотносите тип указателя с правильным Python‑типом. Если DLL ожидает const char*, передавайте bytes и при необходимости кодируйте имя файла методом .encode('ansi'). Если вы контролируете нативный API, предпочитайте wchar_t* и используйте ctypes.c_wchar_p, чтобы Python‑строки str без лишних усилий работали с Unicode‑путями. Чёткий контракт на границе FFI делает интеграцию с Windows‑DLL предсказуемой и надёжной.