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

26

我正在使用ctypes在Python中加载DLL,这非常好。

现在我们想要能够在运行时重新加载该DLL。

直接的方法似乎是:

1. 卸载DLL

2. 加载DLL

不幸的是,我不确定卸载DLL的正确方法。

_ctypes.FreeLibrary可用,但是是私有的。

是否有其他方法可以卸载该DLL?


2
你有没有找到比我丑陋的方式更好的答案?如果没有,也许你应该在他们的邮件列表上询问,如果它不存在,就报告错误。'del' 应该调用函数来释放资源! - Piotr Lesnicki
6个回答

20

你应该能够通过处理对象来完成它

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

编辑:Hop的评论是正确的,这解除了名称的绑定,但垃圾回收并不会那么快,事实上我甚至怀疑它甚至是否释放了加载的库。

Ctypes似乎没有提供一种清理资源的简洁方法,它只提供了一个_handle字段来处理dlopen句柄...

所以我唯一看到的方式,是一个真正的,非常不干净的方式,即在系统依赖下关闭句柄,但这非常非常不干净,因为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)

我不确定,但我怀疑这不会卸载dll。按照语言参考,我猜它只是从当前命名空间中删除名称的绑定。 - user3850
1
当共享库无法进行引用计数递减时,POSIX的dlclose返回非零值,而Windows的FreeLibrary返回零。在这种情况下,_ctypes.dlclose_ctypes.FreeLibrary(注意下划线)会引发OSError异常。 - Eryk Sun
1
对于那些想在Windows上找到这样做的方法的人,可以参考https://dev59.com/uGIk5IYBdhLWcg3wI7EF - Gamrix

8

如果你正在使用iPython或类似的工作流程,能够卸载DLL是很有帮助的,这样你就可以在不必重新启动会话的情况下重建DLL。在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将其释放。我曾经认为先使用FreeLibary释放会产生问题,因此我保存了库句柄的副本,并按照示例中显示的顺序进行了释放。
我基于ctypes unload dll方法来实现,该方法在一开始就明确加载句柄。然而,加载约定并不像简单的"ctypes.cdll.LoadLibrary('test.dll')"那样干净,所以我选择了展示的方法。

4
这是错误的。一个 DLL/EXE 模块句柄是指向模块基地址的指针,通常在 64 位 Python 中是一个 64 位值。但是 ctypes 将整数传递为 32 位的 C int 值;这将截断 64 位指针的值。你需要将句柄包装成指针,即ctypes.c_void_p(mydll._handle),或者声明kernel32.FreeLibrary.argtypes = (ctypes.c_void_p,),或者调用 _ctypes.FreeLibrary(注意初始下划线;它是底层的 _ctypes 扩展模块)。 - Eryk Sun

5

2020年的Windows和Linux兼容的最小可复现示例

类似讨论的概述

以下是类似讨论的概述(我从中构建了这个答案)。

最小可复现示例

这适用于Windows和Linux,因此需要提供2个编译脚本。 测试环境如下:

  • Win 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://dev59.com/Oazka4cB1Zd3GeqP6lP1#50986803
    # 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://dev59.com/qGcs5IYBdhLWcg3wHwNH#13129176
    # https://dev59.com/ZXRC5IYBdhLWcg3wROpQ

    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

如果代码示例或目前的解释有任何问题,请告诉我。如果你知道更好的方法,那就更好了!如果我们能够一劳永逸地解决这个问题,那就太棒了。


3

为了实现完全的跨平台兼容性:我维护了一个包含各个平台上dlclose()等效函数以及它们所属库的列表。这是一个有些冗长的列表,但可以随意复制/粘贴。

import sys
import ctypes
import platform

OS = platform.system()

if OS == "Windows":  # pragma: Windows
    dll_close = ctypes.windll.kernel32.FreeLibrary

elif OS == "Darwin":
    try:
        try:
            # macOS 11 (Big Sur). Possibly also later macOS 10s.
            stdlib = ctypes.CDLL("libc.dylib")
        except OSError:
            stdlib = ctypes.CDLL("libSystem")
    except OSError:
        # Older macOSs. Not only is the name inconsistent but it's
        # not even in PATH.
        stdlib = ctypes.CDLL("/usr/lib/system/libsystem_c.dylib")
    dll_close = stdlib.dlclose

elif OS == "Linux":
    try:
        stdlib = ctypes.CDLL("")
    except OSError:
        # Alpine Linux.
        stdlib = ctypes.CDLL("libc.so")
    dll_close = stdlib.dlclose

elif sys.platform == "msys":
    # msys can also use `ctypes.CDLL("kernel32.dll").FreeLibrary()`. Not sure
    # if or what the difference is.
    stdlib = ctypes.CDLL("msys-2.0.dll")
    dll_close = stdlib.dlclose

elif sys.platform == "cygwin":
    stdlib = ctypes.CDLL("cygwin1.dll")
    dll_close = stdlib.dlclose

elif OS == "FreeBSD":
    # FreeBSD uses `/usr/lib/libc.so.7` where `7` is another version number.
    # It is not in PATH but using its name instead of its path is somehow the
    # only way to open it. The name must include the .so.7 suffix.
    stdlib = ctypes.CDLL("libc.so.7")
    dll_close = stdlib.close

else:
    raise NotImplementedError("Unknown platform.")

dll_close.argtypes = [ctypes.c_void_p]

你可以使用dll_close(dll._handle)来卸载库dll = ctypes.CDLL("your-library")

此列表摘自此文件。我将在每次遇到新平台时更新主干分支


2

Piotr的回答对我很有帮助,但在64位Windows上我遇到了一个问题:

Traceback (most recent call last):
...
ctypes.ArgumentError: argument 1: <class 'OverflowError'>: int too long to convert

调整FreeLibrary调用的参数类型,如此答案所建议的,解决了我的问题。
因此,我们得出以下完整的解决方案:
import ctypes, ctypes.windll

def free_library(handle):
    kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
    kernel32.FreeLibrary.argtypes = [ctypes.wintypes.HMODULE]
    kernel32.FreeLibrary(handle)

使用方法:

lib = ctypes.CDLL("foobar.dll")
free_library(lib._handle)

1
很好!我需要这个。 - Alen Wesker

1
如果您需要这个功能,可以编写两个dll文件,其中dll_A从dll_B加载/卸载库。使用dll_A作为Python接口加载器和dll_B中函数的传递。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接