python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python调用DLL动态库

Python调用DLL动态库的入门指南

作者:yuanpan

本文介绍了在Windows环境下使用Python调用C/C++和C#动态链接库(DLL)的方法,主要包括如何使用ctypes库调用C/C++的DLL,如何使用pythonnet库调用C#的普通类库,以及如何将C#方法导出为原生函数以便ctypes调用,需要的朋友可以参考下

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

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

  1. Python 如何调用 C/C++ 编译出来的 DLL。
  2. Python 调用 C# 动态库时有哪些可行方案。
  3. DLL 里的常见数据类型如何对应到 Python 里的类型。

示例环境:

重要前提:Python、DLL、依赖库必须是同一位数。64 位 Python 调 64 位 DLL,32 位 Python 调 32 位 DLL。位数不一致会加载失败。

一、先理解 Python 调 DLL 的基本思路

Python 标准库里自带 ctypes,它可以加载 Windows 下的 .dll 文件,并调用 DLL 中导出的 C 风格函数。

核心步骤通常是:

from ctypes import CDLL

dll = CDLL(r".\native_demo.dll")
result = dll.AddInt(1, 2)
print(result)

但真实项目中不能只写这几行,因为 Python 不知道 DLL 函数的参数和返回值类型。如果不声明类型,遇到字符串、浮点数、结构体、指针时很容易出错。

更规范的写法是:

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

#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” 进入该文件所在目录,执行:

cl /LD /EHsc native_demo.cpp /Fe:native_demo.dll

编译成功后会得到:

native_demo.dll
native_demo.lib
native_demo.obj

Python 只需要加载 native_demo.dll

三、Python 调用 C++ DLL

新建 call_cpp_dll.py

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)

运行:

python call_cpp_dll.py

可能输出:

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,也就是把函数名改造成带参数信息的内部符号。

例如你写的是:

int AddInt(int a, int b);

编译后的导出名可能不是简单的 AddInt。Python 用 ctypes 找函数时会按导出名查找,如果导出名变了,就会找不到。

所以给 DLL 导出的函数建议写成:

extern "C" __declspec(dllexport)

它的含义是:

如果要在 32 位环境稳定导出函数名,建议再配合 .def 文件控制导出名。新手学习阶段优先使用 64 位 Python 和 64 位 DLL,会少很多麻烦。

五、DLL 类型与 Pythonctypes类型对应表

下面是 Windows DLL 开发中最常见的类型映射。

C/C++ 类型Windows 常见类型Python ctypes 类型说明
charCHARc_char单个字节字符
unsigned charBYTEc_ubyte无符号 8 位整数
shortSHORTc_short16 位整数
unsigned shortWORDc_ushort无符号 16 位整数
intINTc_int通常是 32 位整数
unsigned intUINTc_uint无符号 32 位整数
longLONGc_longWindows 下通常是 32 位
unsigned longDWORDc_ulongWindows 下常用 32 位无符号整数
long longLONGLONGc_longlong64 位整数
floatFLOATc_float单精度浮点数
doubleDOUBLEc_double双精度浮点数
boolBOOLc_boolc_intWin32 BOOL 一般用 c_int 更稳
char*LPSTRc_char_pANSI 字符串指针
wchar_t*LPWSTRc_wchar_pUnicode 宽字符字符串指针
const char*LPCSTRc_char_p输入用 ANSI 字符串
const wchar_t*LPCWSTRc_wchar_p输入用 Unicode 字符串
void*LPVOIDc_void_p通用指针
int*int*POINTER(c_int)指向整数的指针
structSTRUCTctypes.Structure字段顺序必须一致

1. 整数和浮点数

C++:

DLL_EXPORT int AddInt(int a, int b);
DLL_EXPORT double CircleArea(double radius);

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++ 示例里这样写:

DLL_EXPORT int IsEven(int value)
{
    return value % 2 == 0 ? 1 : 0;
}

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++:

DLL_EXPORT int BuildStudent(int id, const wchar_t* name, double score, Student* output);

Python:

dll.BuildStudent.argtypes = [C.c_int, C.c_wchar_p, C.c_double, C.POINTER(Student)]
dll.BuildStudent.restype = C.c_int

调用时直接传 Python 字符串:

dll.BuildStudent(1001, "Alice", 95.5, C.byref(student))

这里用的是 wchar_t*,对应 Python 的 c_wchar_p。在 Windows 上建议优先用宽字符,中文路径和中文内容会更省事。

4. 字符串输出

DLL 如果要返回字符串,不建议直接返回内部临时指针。更稳的方式是:Python 创建缓冲区,把缓冲区指针传给 DLL,DLL 往缓冲区里写。

C++:

DLL_EXPORT void WriteMessage(wchar_t* buffer, int bufferLength);

Python:

buffer = C.create_unicode_buffer(128)
dll.WriteMessage(buffer, len(buffer))
print(buffer.value)

这种方式的优点是内存由 Python 分配和释放,不容易出现“谁申请、谁释放”的问题。

5. 结构体

C++:

struct Point
{
    int x;
    int y;
};

Python 必须按相同字段顺序定义:

class Point(C.Structure):
    _fields_ = [
        ("x", C.c_int),
        ("y", C.c_int),
    ]

传给 DLL 时:

point = Point(3, 5)
dll.MovePoint(C.byref(point), 10, -2)

C.byref(point) 表示把结构体地址传过去,C++ 侧收到的是 Point*

6. 数组

C/C++ 里的数组可以用 ctypes 的数组类型表示。

IntArray10 = C.c_int * 10
arr = IntArray10(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

如果 DLL 函数参数是:

void SumArray(const int* values, int count);

Python 可以声明为:

dll.SumArray.argtypes = [C.POINTER(C.c_int), C.c_int]

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

六、调用约定:CDLL和WinDLL怎么选

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

Python 里对应:

C.CDLL("xxx.dll")     # cdecl
C.WinDLL("xxx.dll")  # stdcall

如果 C++ 函数没有特别声明,MSVC 默认通常是 cdecl,所以使用 CDLL

如果 DLL 函数写成:

extern "C" __declspec(dllexport) int __stdcall AddInt(int a, int b);

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# 类库:

dotnet new classlib -n CsLibraryDemo

修改 CsLibraryDemo/Class1.cs

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; }
}

编译:

dotnet build -c Release

安装 Python 包:

pip install pythonnet

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 侧表现
intint
longint
floatfloat
doublefloat
boolbool
stringstr
class.NET 对象
List<T>.NET 集合对象,可遍历
byte[].NET 字节数组,必要时转 bytes

九、方案二:把 C# 方法导出成原生函数给ctypes调

如果你希望 Python 像调用 C++ DLL 一样调用 C#,可以使用 .NET NativeAOT,把 C# 方法导出为原生函数。

这种方式适合:

创建项目:

dotnet new classlib -n CsNativeExportDemo

修改 CsNativeExportDemo.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <PublishAot>true</PublishAot>
    <NativeLib>Shared</NativeLib>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>
</Project>

修改 Class1.cs

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;
    }
}

发布:

dotnet publish -c Release -r win-x64

发布目录中会生成原生 DLL。Python 可以像调用 C++ DLL 一样调用它:

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 位不一致

报错类似:

OSError: [WinError 193] %1 不是有效的 Win32 应用程序

通常是 Python 和 DLL 位数不一致。

检查 Python 位数:

import platform
print(platform.architecture())

2. DLL 依赖项找不到

报错类似:

OSError: [WinError 126] 找不到指定的模块

不一定是目标 DLL 不存在,也可能是目标 DLL 依赖的其他 DLL 找不到。

解决思路:

示例:

import os

os.add_dll_directory(r"C:\path\to\dll_folder")

3. 没写argtypes和restype

ctypes 默认返回 int。如果 DLL 实际返回 double、指针、结构体,结果就会错。

建议每个函数都显式声明:

dll.SomeFunction.argtypes = [...]
dll.SomeFunction.restype = ...

4. 字符串编码混乱

Windows 下建议优先使用 Unicode 宽字符接口:

如果使用 char*,就需要明确编码,比如 UTF-8 或 GBK。

5. 内存释放责任不清楚

跨语言调用时一定要明确:内存是谁申请的,就尽量由谁释放。

新手推荐:

十一、推荐的 DLL 接口设计习惯

为了让 Python 调用更稳定,建议 DLL 对外接口尽量保持 C 风格:

extern "C" __declspec(dllexport) int FunctionName(...);

接口设计上尽量:

十二、完整文件清单

学习 C++ DLL 调用时,建议把下面文件放在同一目录:

demo/
  native_demo.cpp
  native_demo.dll
  call_cpp_dll.py

学习 C# 普通类库调用时:

demo/
  CsLibraryDemo/
  call_csharp_by_pythonnet.py

学习 C# NativeAOT 导出时:

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

以上就是Python调用DLL动态库的入门指南的详细内容,更多关于Python调用DLL动态库的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文