如何在Python中使用ctypes卸载DLL?

26 投票
6 回答
30865 浏览
提问于 2025-04-11 20:45

我在Python中使用ctypes来加载一个DLL文件,这个方法很好用。

现在我们希望能够在程序运行时重新加载这个DLL。

看起来简单的方法是:
1. 卸载DLL
2. 重新加载DLL

不过,我不太确定正确的卸载DLL的方法是什么。

虽然有一个叫_ctypes.FreeLibrary的函数可以用,但它是私有的。

有没有其他方法可以卸载这个DLL呢?

6 个回答

5

2020年适用于Windows和Linux的最小可复现示例

类似讨论的概述

这里是一些类似讨论的概述(我从中整理了这个回答)。

最小可复现示例

这个示例适用于Windows和Linux,因此提供了两个编译脚本。测试环境如下:

  • Windows 8.1, Python 3.8.3 (anaconda), ctypes 1.1.0, mingw-w64 x86_64-8.1.0-posix-seh-rt_v6-rev0
  • Linux Fedora 32, Python 3.7.6 (anaconda), ctypes 1.1.0, g++ 10.2.1

cpp_code.cpp

extern "C" int my_fct(int n)
{
    int factor = 10;
    return factor * n;
}

compile-linux.sh

#!/bin/bash
g++ cpp_code.cpp -shared -o myso.so

compile-windows.cmd

set gpp="C:\Program Files\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64\bin\g++.exe"
%gpp% cpp_code.cpp -shared -o mydll.dll
PAUSE

Python代码

from sys import platform
import ctypes


if platform == "linux" or platform == "linux2":
    # https://stackoverflow.com/a/50986803/7128154
    # https://stackoverflow.com/a/52223168/7128154

    dlclose_func = ctypes.cdll.LoadLibrary('').dlclose
    dlclose_func.argtypes = [ctypes.c_void_p]

    fn_lib = './myso.so'
    ctypes_lib = ctypes.cdll.LoadLibrary(fn_lib)
    handle = ctypes_lib._handle

    valIn = 42
    valOut = ctypes_lib.my_fct(valIn)
    print(valIn, valOut)

    del ctypes_lib
    dlclose_func(handle)

elif platform == "win32": # Windows
    # https://stackoverflow.com/a/13129176/7128154
    # https://stackoverflow.com/questions/359498/how-can-i-unload-a-dll-using-ctypes-in-python

    lib = ctypes.WinDLL('./mydll.dll')
    libHandle = lib._handle

    # do stuff with lib in the usual way
    valIn = 42
    valOut = lib.my_fct(valIn)
    print(valIn, valOut)

    del lib
    ctypes.windll.kernel32.FreeLibrary(libHandle)

更通用的解决方案(面向对象的共享库及其依赖)

如果一个共享库有依赖,那么这个方法可能就不再有效了(但有时可以,这取决于依赖的情况^^)。我没有深入研究具体细节,但看起来机制是这样的:库和依赖都被加载。如果依赖没有被卸载,那么库就无法被卸载。

我发现,如果我在我的共享库中包含OpenCv(版本4.2),这会干扰卸载过程。以下示例只在Linux系统上测试过:

code.cpp

#include <opencv2/core/core.hpp>
#include <iostream> 


extern "C" int my_fct(int n)
{
    cv::Mat1b mat = cv::Mat1b(10,8,(unsigned char) 1 );  // change 1 to test unloading
    
    return mat(0,1) * n;
}

编译命令 g++ code.cpp -shared -fPIC -Wall -std=c++17 -I/usr/include/opencv4 -lopencv_core -o so_opencv.so

Python代码

from sys import platform
import ctypes


class CtypesLib:

    def __init__(self, fp_lib, dependencies=[]):
        self._dependencies = [CtypesLib(fp_dep) for fp_dep in dependencies]

        if platform == "linux" or platform == "linux2":  # Linux
            self._dlclose_func = ctypes.cdll.LoadLibrary('').dlclose
            self._dlclose_func.argtypes = [ctypes.c_void_p]
            self._ctypes_lib = ctypes.cdll.LoadLibrary(fp_lib)
        elif platform == "win32":  # Windows
            self._ctypes_lib = ctypes.WinDLL(fp_lib)

        self._handle = self._ctypes_lib._handle

    def __getattr__(self, attr):
        return self._ctypes_lib.__getattr__(attr)

    def __del__(self):
        for dep in self._dependencies:
            del dep

        del self._ctypes_lib

        if platform == "linux" or platform == "linux2":  # Linux
            self._dlclose_func(self._handle)
        elif platform == "win32":  # Windows
            ctypes.windll.kernel32.FreeLibrary(self._handle)


fp_lib = './so_opencv.so'

ctypes_lib = CtypesLib(fp_lib, ['/usr/lib64/libopencv_core.so'])

valIn = 1
ctypes_lib.my_fct.argtypes = [ctypes.c_int]
ctypes_lib.my_fct.restype = ctypes.c_int
valOut = ctypes_lib.my_fct(valIn)
print(valIn, valOut)

del ctypes_lib

如果代码示例或解释中有任何问题,请告诉我。如果你知道更好的方法也请分享!如果我们能彻底解决这个问题,那就太好了。

8

能够卸载DLL(动态链接库)是很有帮助的,这样你就可以在不重启会话的情况下重建DLL,特别是在使用iPython或类似的工作流程时。在Windows系统上,我只尝试过与Windows DLL相关的方法。

REBUILD = True
if REBUILD:
  from subprocess import call
  call('g++ -c -DBUILDING_EXAMPLE_DLL test.cpp')
  call('g++ -shared -o test.dll test.o -Wl,--out-implib,test.a')

import ctypes
import numpy

# Simplest way to load the DLL
mydll = ctypes.cdll.LoadLibrary('test.dll')

# Call a function in the DLL
print mydll.test(10)

# Unload the DLL so that it can be rebuilt
libHandle = mydll._handle
del mydll
ctypes.windll.kernel32.FreeLibrary(libHandle)

我对内部原理了解不多,所以不太确定这样做是否干净。我认为删除mydll会释放Python的资源,而FreeLibrary这个调用则是告诉Windows释放它。我原本以为先用FreeLibrary释放会出现问题,所以我保存了一份库的句柄,并按照示例中的顺序释放它。

我这个方法是基于ctypes卸载dll,这个方法是先明确加载句柄的。不过,这种加载方式没有简单的“ctypes.cdll.LoadLibrary('test.dll')”那么干净,所以我选择了示例中展示的方法。

20

你应该可以通过处理这个对象来做到这一点。

mydll = ctypes.CDLL('...')
del mydll
mydll = ctypes.CDLL('...')

编辑:Hop的评论是对的,这样可以解除名称的绑定,但垃圾回收并不会那么快发生,实际上我甚至怀疑它是否会释放加载的库。

Ctypes似乎没有提供一个干净的方法来释放资源,它只提供了一个_handle字段来处理dlopen的句柄……

所以我看到的唯一方法,真的,真的非常不干净的方法,就是依赖系统来dlclose这个句柄,但这非常非常不干净,因为ctypes内部还保留着对这个句柄的引用。所以卸载的过程大致是这样的:

mydll = ctypes.CDLL('./mylib.so')
handle = mydll._handle
del mydll
while isLoaded('./mylib.so'):
    dlclose(handle)

这方法太不干净了,我只检查了它是否有效,使用的是:

def isLoaded(lib):
   libp = os.path.abspath(lib)
   ret = os.system("lsof -p %d | grep %s > /dev/null" % (os.getpid(), libp))
   return (ret == 0)

def dlclose(handle)
   libdl = ctypes.CDLL("libdl.so")
   libdl.dlclose(handle)

撰写回答