Python 调用 DLL 动态库入门:Windows 下调用 C++ 与 C# 动态库完整示例

Python 调用 DLL 动态库入门:Windows 下调用 C++ 与 C# 动态库完整示例

很多 Windows 项目里都会遇到这样的需求:底层算法已经用 C/C++ 写好了,或者公司内部有一个 C# 组件,现在希望在 Python 里直接调用,而不是重写一遍。

这篇文章面向新手,重点讲清楚三件事:

  1. Python 如何调用 C/C++ 编译出来的 DLL。
  2. Python 调用 C# 动态库时有哪些可行方案。
  3. 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_boolc_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]

调用时把数组对象传进去即可。

六、调用约定:CDLLWinDLL 怎么选

Windows DLL 里常见两种调用约定:

  • cdecl
  • stdcall

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 调用。

常见方案有两个:

  1. 使用 pythonnet,在 Python 中加载 .NET 程序集并调用 C# 类。
  2. 把 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 导出的函数更适合使用简单类型,例如 intdouble。字符串、数组、复杂对象需要额外处理内存和编码,新手不建议一开始就这样做。

十、实战中最容易踩的坑

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. 没写 argtypesrestype

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::stringstd::vector
  • 保证 Python 和 DLL 使用相同位数。
  • 每个 Python 调用都写清楚 argtypesrestype

十二、完整文件清单

学习 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# 组件。

相关推荐
2301_796588502 小时前
Python中PyTorch如何处理NaN损失值_添加梯度裁剪与检查输入数据
jvm·数据库·python
InfinteJustice2 小时前
Golang怎么做代码热更新_Golang热更新教程【精通】
jvm·数据库·python
2401_887724502 小时前
c++如何利用C++23的std--expected重构传统的文件IO报错代码【进阶】
jvm·数据库·python
2301_777599372 小时前
Go语言怎么做DNS查询_Go语言DNS域名解析教程【完整】
jvm·数据库·python
tjc199010052 小时前
HTML5音频通过OscillatorNode产生基础波形测试
jvm·数据库·python
YuanDaima20482 小时前
大语言模型生命周期全链路解析:从架构基石到高效推理
开发语言·人工智能·python·语言模型·架构·transformer
kronos.荒2 小时前
回溯(python)
python·回溯
weixin_580614002 小时前
golang如何使用sync.WaitGroup_golang sync.WaitGroup并发等待使用方法
jvm·数据库·python
疯狂打码的少年2 小时前
单向循环链表 + 尾指针:让插入删除更高效的秘密武器
数据结构·python·链表