Python 调用 DLL 动态库入门:Windows 下调用 C++ 与 C# 动态库完整示例
很多 Windows 项目里都会遇到这样的需求:底层算法已经用 C/C++ 写好了,或者公司内部有一个 C# 组件,现在希望在 Python 里直接调用,而不是重写一遍。
这篇文章面向新手,重点讲清楚三件事:
- Python 如何调用 C/C++ 编译出来的 DLL。
- Python 调用 C# 动态库时有哪些可行方案。
- DLL 里的常见数据类型如何对应到 Python 里的类型。
示例环境:
- Windows 10/11
- Python 3.10+,64 位
- Visual Studio 2022 Build Tools 或 Visual Studio 2022
- .NET 8 SDK
重要前提:Python、DLL、依赖库必须是同一位数。64 位 Python 调 64 位 DLL,32 位 Python 调 32 位 DLL。位数不一致会加载失败。
一、先理解 Python 调 DLL 的基本思路
Python 标准库里自带 ctypes,它可以加载 Windows 下的 .dll 文件,并调用 DLL 中导出的 C 风格函数。
核心步骤通常是:
python
from ctypes import CDLL
dll = CDLL(r".\native_demo.dll")
result = dll.AddInt(1, 2)
print(result)
但真实项目中不能只写这几行,因为 Python 不知道 DLL 函数的参数和返回值类型。如果不声明类型,遇到字符串、浮点数、结构体、指针时很容易出错。
更规范的写法是:
python
import ctypes as C
dll = C.CDLL(r".\native_demo.dll")
dll.AddInt.argtypes = [C.c_int, C.c_int]
dll.AddInt.restype = C.c_int
print(dll.AddInt(10, 20))
argtypes 表示参数类型,restype 表示返回值类型。
二、准备一个 C++ DLL 示例
先写一个最简单但覆盖多种类型的 C++ 动态库:整数、浮点数、字符串、结构体、指针都包含。
新建文件 native_demo.cpp:
cpp
#include <cmath>
#include <cwchar>
#define DLL_EXPORT extern "C" __declspec(dllexport)
struct Point
{
int x;
int y;
};
struct Student
{
int id;
double score;
wchar_t name[32];
};
DLL_EXPORT int AddInt(int a, int b)
{
return a + b;
}
DLL_EXPORT double CircleArea(double radius)
{
return 3.141592653589793 * radius * radius;
}
DLL_EXPORT int IsEven(int value)
{
return value % 2 == 0 ? 1 : 0;
}
DLL_EXPORT void MovePoint(Point* point, int dx, int dy)
{
if (point == nullptr)
{
return;
}
point->x += dx;
point->y += dy;
}
DLL_EXPORT int BuildStudent(int id, const wchar_t* name, double score, Student* output)
{
if (name == nullptr || output == nullptr)
{
return 0;
}
output->id = id;
output->score = score;
wcsncpy_s(output->name, name, _TRUNCATE);
return 1;
}
DLL_EXPORT void WriteMessage(wchar_t* buffer, int bufferLength)
{
if (buffer == nullptr || bufferLength <= 0)
{
return;
}
wcsncpy_s(buffer, bufferLength, L"Hello from C++ DLL", _TRUNCATE);
}
用 Visual Studio 的 "x64 Native Tools Command Prompt for VS 2022" 进入该文件所在目录,执行:
bat
cl /LD /EHsc native_demo.cpp /Fe:native_demo.dll
编译成功后会得到:
text
native_demo.dll
native_demo.lib
native_demo.obj
Python 只需要加载 native_demo.dll。
三、Python 调用 C++ DLL
新建 call_cpp_dll.py:
python
import ctypes as C
from pathlib import Path
dll_path = Path(__file__).with_name("native_demo.dll")
dll = C.CDLL(str(dll_path))
class Point(C.Structure):
_fields_ = [
("x", C.c_int),
("y", C.c_int),
]
class Student(C.Structure):
_fields_ = [
("id", C.c_int),
("score", C.c_double),
("name", C.c_wchar * 32),
]
dll.AddInt.argtypes = [C.c_int, C.c_int]
dll.AddInt.restype = C.c_int
dll.CircleArea.argtypes = [C.c_double]
dll.CircleArea.restype = C.c_double
dll.IsEven.argtypes = [C.c_int]
dll.IsEven.restype = C.c_int
dll.MovePoint.argtypes = [C.POINTER(Point), C.c_int, C.c_int]
dll.MovePoint.restype = None
dll.BuildStudent.argtypes = [C.c_int, C.c_wchar_p, C.c_double, C.POINTER(Student)]
dll.BuildStudent.restype = C.c_int
dll.WriteMessage.argtypes = [C.c_wchar_p, C.c_int]
dll.WriteMessage.restype = None
print("AddInt:", dll.AddInt(10, 20))
print("CircleArea:", dll.CircleArea(3.0))
print("IsEven:", bool(dll.IsEven(8)))
point = Point(3, 5)
dll.MovePoint(C.byref(point), 10, -2)
print("Point:", point.x, point.y)
student = Student()
ok = dll.BuildStudent(1001, "Alice", 95.5, C.byref(student))
if ok:
print("Student:", student.id, student.name, student.score)
buffer = C.create_unicode_buffer(128)
dll.WriteMessage(buffer, len(buffer))
print("Message:", buffer.value)
运行:
bat
python call_cpp_dll.py
可能输出:
text
AddInt: 30
CircleArea: 28.274333882308137
IsEven: True
Point: 13 3
Student: 1001 Alice 95.5
Message: Hello from C++ DLL
四、为什么 C++ DLL 要写 extern "C"
C++ 支持函数重载,所以编译器会对函数名做 name mangling,也就是把函数名改造成带参数信息的内部符号。
例如你写的是:
cpp
int AddInt(int a, int b);
编译后的导出名可能不是简单的 AddInt。Python 用 ctypes 找函数时会按导出名查找,如果导出名变了,就会找不到。
所以给 DLL 导出的函数建议写成:
cpp
extern "C" __declspec(dllexport)
它的含义是:
extern "C":按 C 语言方式导出函数名,减少 C++ 名字改写问题。__declspec(dllexport):告诉 MSVC 把函数导出到 DLL。
如果要在 32 位环境稳定导出函数名,建议再配合 .def 文件控制导出名。新手学习阶段优先使用 64 位 Python 和 64 位 DLL,会少很多麻烦。
五、DLL 类型与 Python ctypes 类型对应表
下面是 Windows DLL 开发中最常见的类型映射。
| C/C++ 类型 | Windows 常见类型 | Python ctypes 类型 |
说明 |
|---|---|---|---|
char |
CHAR |
c_char |
单个字节字符 |
unsigned char |
BYTE |
c_ubyte |
无符号 8 位整数 |
short |
SHORT |
c_short |
16 位整数 |
unsigned short |
WORD |
c_ushort |
无符号 16 位整数 |
int |
INT |
c_int |
通常是 32 位整数 |
unsigned int |
UINT |
c_uint |
无符号 32 位整数 |
long |
LONG |
c_long |
Windows 下通常是 32 位 |
unsigned long |
DWORD |
c_ulong |
Windows 下常用 32 位无符号整数 |
long long |
LONGLONG |
c_longlong |
64 位整数 |
float |
FLOAT |
c_float |
单精度浮点数 |
double |
DOUBLE |
c_double |
双精度浮点数 |
bool |
BOOL |
c_bool 或 c_int |
Win32 BOOL 一般用 c_int 更稳 |
char* |
LPSTR |
c_char_p |
ANSI 字符串指针 |
wchar_t* |
LPWSTR |
c_wchar_p |
Unicode 宽字符字符串指针 |
const char* |
LPCSTR |
c_char_p |
输入用 ANSI 字符串 |
const wchar_t* |
LPCWSTR |
c_wchar_p |
输入用 Unicode 字符串 |
void* |
LPVOID |
c_void_p |
通用指针 |
int* |
int* |
POINTER(c_int) |
指向整数的指针 |
struct |
STRUCT |
ctypes.Structure |
字段顺序必须一致 |
1. 整数和浮点数
C++:
cpp
DLL_EXPORT int AddInt(int a, int b);
DLL_EXPORT double CircleArea(double radius);
Python:
python
dll.AddInt.argtypes = [C.c_int, C.c_int]
dll.AddInt.restype = C.c_int
dll.CircleArea.argtypes = [C.c_double]
dll.CircleArea.restype = C.c_double
2. 布尔值
Windows API 里很多函数的布尔值不是 C++ 的 bool,而是 BOOL,本质上是 32 位整数。
C++ 示例里这样写:
cpp
DLL_EXPORT int IsEven(int value)
{
return value % 2 == 0 ? 1 : 0;
}
Python:
python
dll.IsEven.argtypes = [C.c_int]
dll.IsEven.restype = C.c_int
is_even = bool(dll.IsEven(8))
新手建议:跨语言 DLL 接口里尽量用 int 表示成功失败,少直接暴露 C++ bool。
3. 字符串输入
C++:
cpp
DLL_EXPORT int BuildStudent(int id, const wchar_t* name, double score, Student* output);
Python:
python
dll.BuildStudent.argtypes = [C.c_int, C.c_wchar_p, C.c_double, C.POINTER(Student)]
dll.BuildStudent.restype = C.c_int
调用时直接传 Python 字符串:
python
dll.BuildStudent(1001, "Alice", 95.5, C.byref(student))
这里用的是 wchar_t*,对应 Python 的 c_wchar_p。在 Windows 上建议优先用宽字符,中文路径和中文内容会更省事。
4. 字符串输出
DLL 如果要返回字符串,不建议直接返回内部临时指针。更稳的方式是:Python 创建缓冲区,把缓冲区指针传给 DLL,DLL 往缓冲区里写。
C++:
cpp
DLL_EXPORT void WriteMessage(wchar_t* buffer, int bufferLength);
Python:
python
buffer = C.create_unicode_buffer(128)
dll.WriteMessage(buffer, len(buffer))
print(buffer.value)
这种方式的优点是内存由 Python 分配和释放,不容易出现"谁申请、谁释放"的问题。
5. 结构体
C++:
cpp
struct Point
{
int x;
int y;
};
Python 必须按相同字段顺序定义:
python
class Point(C.Structure):
_fields_ = [
("x", C.c_int),
("y", C.c_int),
]
传给 DLL 时:
python
point = Point(3, 5)
dll.MovePoint(C.byref(point), 10, -2)
C.byref(point) 表示把结构体地址传过去,C++ 侧收到的是 Point*。
6. 数组
C/C++ 里的数组可以用 ctypes 的数组类型表示。
python
IntArray10 = C.c_int * 10
arr = IntArray10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
如果 DLL 函数参数是:
cpp
void SumArray(const int* values, int count);
Python 可以声明为:
python
dll.SumArray.argtypes = [C.POINTER(C.c_int), C.c_int]
调用时把数组对象传进去即可。
六、调用约定:CDLL 和 WinDLL 怎么选
Windows DLL 里常见两种调用约定:
cdeclstdcall
Python 里对应:
python
C.CDLL("xxx.dll") # cdecl
C.WinDLL("xxx.dll") # stdcall
如果 C++ 函数没有特别声明,MSVC 默认通常是 cdecl,所以使用 CDLL。
如果 DLL 函数写成:
cpp
extern "C" __declspec(dllexport) int __stdcall AddInt(int a, int b);
Python 侧就应该用:
python
dll = C.WinDLL(r".\xxx.dll")
调用约定不匹配可能导致参数读取错误、程序崩溃,或者函数调用结束后栈异常。
七、Python 调用 C# DLL 的两种常见方式
C# 编译出来的普通 .dll 是 .NET 程序集,不是传统 Win32 DLL。它里面的 C# 方法不能直接像 C 函数那样被 ctypes.CDLL 调用。
常见方案有两个:
- 使用
pythonnet,在 Python 中加载 .NET 程序集并调用 C# 类。 - 把 C# 方法导出成原生函数,再用
ctypes调用。
新手优先推荐第一种,简单、直观、适合调用普通 C# 类库。第二种更接近"Python 调 DLL"的传统方式,但构建复杂一些。
八、方案一:用 pythonnet 调用普通 C# 类库
先创建一个 C# 类库:
bat
dotnet new classlib -n CsLibraryDemo
修改 CsLibraryDemo/Class1.cs:
csharp
namespace CsLibraryDemo;
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public double CircleArea(double radius)
{
return Math.PI * radius * radius;
}
public string Hello(string name)
{
return $"Hello, {name}";
}
}
public class Student
{
public int Id { get; set; }
public string Name { get; set; } = "";
public double Score { get; set; }
}
编译:
bat
dotnet build -c Release
安装 Python 包:
bat
pip install pythonnet
Python 调用:
python
import clr
from pathlib import Path
dll_path = Path(r".\CsLibraryDemo\bin\Release\net8.0\CsLibraryDemo.dll").resolve()
clr.AddReference(str(dll_path))
from CsLibraryDemo import Calculator, Student
calc = Calculator()
print(calc.Add(10, 20))
print(calc.CircleArea(3.0))
print(calc.Hello("Python"))
student = Student()
student.Id = 1001
student.Name = "Alice"
student.Score = 95.5
print(student.Id, student.Name, student.Score)
这种方式的类型映射由 pythonnet 帮你处理,调用体验更像"在 Python 中使用 C# 类"。
常见映射大致如下:
| C# 类型 | Python 侧表现 |
|---|---|
int |
int |
long |
int |
float |
float |
double |
float |
bool |
bool |
string |
str |
class |
.NET 对象 |
List<T> |
.NET 集合对象,可遍历 |
byte[] |
.NET 字节数组,必要时转 bytes |
九、方案二:把 C# 方法导出成原生函数给 ctypes 调
如果你希望 Python 像调用 C++ DLL 一样调用 C#,可以使用 .NET NativeAOT,把 C# 方法导出为原生函数。
这种方式适合:
- Python 端必须使用
ctypes。 - 希望 C# DLL 暴露 C 风格函数。
- 不想在 Python 进程里直接操作 C# 类。
创建项目:
bat
dotnet new classlib -n CsNativeExportDemo
修改 CsNativeExportDemo.csproj:
xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot>
<NativeLib>Shared</NativeLib>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>
修改 Class1.cs:
csharp
using System.Runtime.InteropServices;
public static class NativeExports
{
[UnmanagedCallersOnly(EntryPoint = "AddInt")]
public static int AddInt(int a, int b)
{
return a + b;
}
[UnmanagedCallersOnly(EntryPoint = "CircleArea")]
public static double CircleArea(double radius)
{
return Math.PI * radius * radius;
}
}
发布:
bat
dotnet publish -c Release -r win-x64
发布目录中会生成原生 DLL。Python 可以像调用 C++ DLL 一样调用它:
python
import ctypes as C
from pathlib import Path
dll_path = Path(r".\CsNativeExportDemo\bin\Release\net8.0\win-x64\publish\CsNativeExportDemo.dll")
dll = C.CDLL(str(dll_path))
dll.AddInt.argtypes = [C.c_int, C.c_int]
dll.AddInt.restype = C.c_int
dll.CircleArea.argtypes = [C.c_double]
dll.CircleArea.restype = C.c_double
print(dll.AddInt(10, 20))
print(dll.CircleArea(3.0))
注意:UnmanagedCallersOnly 导出的函数更适合使用简单类型,例如 int、double。字符串、数组、复杂对象需要额外处理内存和编码,新手不建议一开始就这样做。
十、实战中最容易踩的坑
1. 32 位和 64 位不一致
报错类似:
text
OSError: [WinError 193] %1 不是有效的 Win32 应用程序
通常是 Python 和 DLL 位数不一致。
检查 Python 位数:
python
import platform
print(platform.architecture())
2. DLL 依赖项找不到
报错类似:
text
OSError: [WinError 126] 找不到指定的模块
不一定是目标 DLL 不存在,也可能是目标 DLL 依赖的其他 DLL 找不到。
解决思路:
- 把依赖 DLL 放到同一目录。
- 把依赖目录加入
PATH。 - Python 3.8+ 可以使用
os.add_dll_directory()。
示例:
python
import os
os.add_dll_directory(r"C:\path\to\dll_folder")
3. 没写 argtypes 和 restype
ctypes 默认返回 int。如果 DLL 实际返回 double、指针、结构体,结果就会错。
建议每个函数都显式声明:
python
dll.SomeFunction.argtypes = [...]
dll.SomeFunction.restype = ...
4. 字符串编码混乱
Windows 下建议优先使用 Unicode 宽字符接口:
- C/C++:
wchar_t* - Python:
c_wchar_p - 缓冲区:
create_unicode_buffer
如果使用 char*,就需要明确编码,比如 UTF-8 或 GBK。
5. 内存释放责任不清楚
跨语言调用时一定要明确:内存是谁申请的,就尽量由谁释放。
新手推荐:
- Python 传入缓冲区,DLL 写入内容。
- DLL 不要返回临时字符串指针。
- 如果 DLL 必须分配内存,就同时提供
FreeMemory之类的释放函数。
十一、推荐的 DLL 接口设计习惯
为了让 Python 调用更稳定,建议 DLL 对外接口尽量保持 C 风格:
cpp
extern "C" __declspec(dllexport) int FunctionName(...);
接口设计上尽量:
- 用
int返回成功或失败。 - 复杂数据通过结构体指针输出。
- 字符串输出采用"调用方传缓冲区"的方式。
- 避免直接暴露 C++ 类、模板、
std::string、std::vector。 - 保证 Python 和 DLL 使用相同位数。
- 每个 Python 调用都写清楚
argtypes和restype。
十二、完整文件清单
学习 C++ DLL 调用时,建议把下面文件放在同一目录:
text
demo/
native_demo.cpp
native_demo.dll
call_cpp_dll.py
学习 C# 普通类库调用时:
text
demo/
CsLibraryDemo/
call_csharp_by_pythonnet.py
学习 C# NativeAOT 导出时:
text
demo/
CsNativeExportDemo/
call_csharp_native_export.py
总结
Python 在 Windows 下调用 DLL,最常用的是标准库 ctypes。调用 C/C++ DLL 时,关键点是导出 C 风格函数、声明参数类型、处理好字符串和结构体。
调用 C# 动态库要先区分 DLL 类型:普通 C# 类库是 .NET 程序集,推荐用 pythonnet;如果要像 C++ DLL 一样用 ctypes 调用,则需要把 C# 方法导出成原生函数,例如使用 NativeAOT。
掌握这几个规则后,Python 就可以很自然地接入已有的 C++ 算法库、Windows SDK 封装库、工业设备 SDK,或者公司内部的 C# 组件。