Why only the first letter of the string is printed when using DLL with ctypes in Python 3?

问题描述

C++代码:

#include <stdio.h>
extern "C" {
    __declspec(dllexport) void printString(char*);
}
void printString(char* str)
{
    printf(str);
}

Python代码:

import ctypes

DLL_PATH = 'C:./Test2.dll'

lib = ctypes.cdll.LoadLibrary(DLL_PATH)
lib.printString('abc')

上述C++代码中printString()将接收到的字符串直接打印出来;Python通过ctypes加载此C++代码的动态链接库调用printString()

Python 2下运行输出

abc

而Python 3下运行则仅输出第一个字母

a

原因分析

想必很多人都知道这是Python 2和3内部不同的字符串编码方式造成的,这里深入分析一下。

当执行lib.printString('abc')时,函数调用传递的参数实际是一个内存地址(或者说是指针),而这个内存地址就是'abc'在内存中的起始位置。

printf(str)则是接收到一个内存地址,然后以此内存地址开始,逐byte地以char格式输出内存中的值,直到碰到值为char格式的\0,即内存值为0x00为止。

那么'abc'在Python 2和Python 3下内存值为多少呢?这里'abc'一定是以某种方式被编码为数值写入内存的,那编码方式是什么呢?在Python官方文档中就有描述。

Python 2在howto-unicode中介绍道

For example, Python’s default encoding is the ‘ascii’ encoding.

即Python 2下'abc'被编码为97 98 99 0

Python 2在howto-unicode中介绍道

Since Python 3.0, the language features a str type that contain Unicode characters, meaning any string created using "unicode rocks!", 'unicode rocks!', or the triple-quoted string syntax is stored as Unicode.

就是Unicode了,这样'abc'就被编码为97 0 98 0 99 0 0。(Unicode编码每个字符占2个byte)

如果你还对此有疑惑的话,不妨跑个示例代码看看。

示例

C++代码:

#include <stdio.h>

extern "C"
{
    __declspec(dllexport) void readString(char*, int);
}

void readString(char* str, int length)
{
    for (int i = 0; i <= length; i++)
    {
        printf("%d ", *(str + i));
    }
}

Python代码:

import ctypes

DLL_PATH = './Test.dll'

lib = ctypes.cdll.LoadLibrary(DLL_PATH)
lib.readString('abc', 4)
# lib.readString('abc', 7)

上述C++代码中readString()接收字符串起始地址,以及需要打印的内存byte数据个数,按顺序输出内存值;Python中readString()则传递字符串'abc'的起始地址,分别指定长度为4(Python 2)和长度为7(Python 3)。

Python 2下输出

97 98 99 0

'abc'的ASCII编码。

Python 3下输出

97 0 98 0 99 0 0

'abc'的Unicode编码。

原因:Unicode中'a'由两个字节97 0表示,C++读取字符串时以遇到的第一个0作为字符串结尾,导致当'abc'传入时,C++认为字符串为97 0(即'a'),而不是97 0 98 0 99 0 0

解决方法

知道原理了就很简单了,就是让'abc'的内存值为ASCII编码值,而不是Unicode编码值;所以只需要在Python中调用C++函数时将字符串手动编码为ASCII。

修改后的Python代码为:

import ctypes

DLL_PATH = 'C:./Test2.dll'

lib = ctypes.cdll.LoadLibrary(DLL_PATH)
lib.printString('abc'.encode('ascii'))

这样Python 3下也能得到正确结果

abc

另一种解决方法思路:上述解决方法是在Python端消灭Unicode,当然还有一种方法就是在C++端消灭ASCII,统一使用Unicode,限于时间关系,我这里就不作出尝试了。

参考