Python ctypes LoadLibrary:它真的加载了一个新实例吗?如此不同的全局变量?

Python ctypes LoadLibrary: does it really load a new instance, e.g. so different global variables?

https://docs.python.org/3/library/ctypes.html开始大约cdll.LoadLibrary

This method always returns a new instance of the library.

这是否意味着如果库在内部使用一些全局变量,那么每次调用 cdll.LoadLibrary 时,它都会 return 一个使用一组新的独立全局变量的库?

(这可能说明了对此类共享库如何工作的一些误解...很高兴得到纠正)

它是 ctypes 包装器类型的新 Python 实例,但引用了相同的 DLL。这很容易测试:

test.c:

__declspec(dllexport)
int global = 7;

__declspec(dllexport)
int increment() {
    global += 1;
    return global;
}

test.py

from ctypes import *

dll = CDLL('./test')
print(dll)
print(c_int.in_dll(dll,'global'))
print(dll.increment())
print(c_int.in_dll(dll,'global'))

dll2 = CDLL('./test')
print(dll2)
print(c_int.in_dll(dll2,'global'))
print(dll2.increment())
print(c_int.in_dll(dll2,'global'))

输出如下。注意全局变量不断增加,实例地址不同但句柄(实际上是加载的DLL的虚拟基地址)是相同的。

<CDLL 'C:\test', handle 7ff81aad0000 at 0x2362f0dbee0>
c_long(7)
8
c_long(8)
<CDLL 'C:\test', handle 7ff81aad0000 at 0x2362f0dbeb0>
c_long(8)
9
c_long(9)

cdll.LoadLibrary 的行为不受 Python 控制,但它取决于 Python 是 运行 的 OS。 @MarkTolonen 的答案显示了 Windows 上的行为,这个答案集中在 Linux(和 MacOs)上。

cdll.LoadLibrary使用dlopen加载共享对象,因此dlopen的行为是我们需要分析的。

让我们看看下面的 C 代码 (foo.c):

#include <stdio.h>
static int init();

//global, initialized when so is loaded:
int my_global = init();

static int init(){
    printf("initializing address %p\n", (void*)&my_global);
    return 42;
    
}

extern "C" {
void set(int new_val){ my_global = new_val;}
int get() {return my_global;}
}

g++ --shared -fPIC foo.c -o foo.so 编译。我使用 C++ 而不是 C,所以每次初始化全局变量 my_global 时,它都会记录到标准输出(这不会是 that straight forward in C)。

经过一些准备:

import ctypes

def init_functions(dll):
    get = dll.get
    get.argtypes = []
    get.restype = ctypes.c_int
    set = dll.set
    set.argtypes = [ctypes.c_int]
    set.restype = None
    return get, set

我们观察到以下行为:

#first load:
get, set = init_functions(ctypes.CDLL("./foo.so"))
# initializing address 0x7f5ca4a8102c
print(get())   # 42
set(21)
print(get())   # 21

如预期:全局变量已初始化,我们可以read/write了。现在第二次加载:

get2, set2 = init_functions(ctypes.CDLL("./foo.so"))

ups,我们没有看到初始化的日志记录,这意味着...

print(get2())  # 21

全局变量没有重新初始化。这是 dlopen 的预期行为:加载共享对象后,它永远不会重新加载,而是会被重用。这就是为什么例如pyximport%%cython-魔法使用不同.

为了真正创建新版本,我们将共享对象 foo.so 复制到 foo.so.1,现在:

# load copied shared object:
get3, set3 = init_functions(ctypes.CDLL("./foo.so.1"))
# initializing address 0x7f5ca487102c
print(get3())    # 42

我们可以看到,全局变量被初始化了,但它是另一个地址,即不是旧变量,可以很容易地检查:

print(get())    # 21 - still the old value.

直到现在,Windows 和 Linux 上的行为大致相同,但是在 Linux 上我们可以使用符号插入来确保使用相同的全局变量.

普通CPython使用dlopenRTLD_LOCAL,即不使用符号插入,通过使用RTLD_GLOBAL,即

...
#first load:
get, set = init_functions(ctypes.CDLL("./foo.so", mode=ctypes.RTLD_GLOBAL))
# initializing address 0x7fd01efc102c
...

mode=ctypes.RTLD_GLOBAL 将在 Windows 上被忽略。

第一个区别可以看出

...
# load copied shared object:
get3, set3 = init_functions(ctypes.CDLL("./foo.so.1"))
# initializing address 0x7fd01efc102c
...

与“foo.so”相同的地址也用于“foo.so.1” - 由于 RTLD_GLOBAL 来自两个共享对象的符号 n 被插入.

现在:

print(get3())  # 42
print(get())   # 42

也就是说,旧的全局变量被重新初始化了。


虽然有趣,但我不建议依赖这些细节 - 它太聪明、太脆弱且不可移植:很容易引入内存泄漏或崩溃。

人们应该接受,一般来说,全局变量不能通过重新加载共享对象来重新初始化(以可移植的方式)。

通常,人们希望避免插入全局变量并确保它们在共享对象中具有内部链接(例如,通过将它们设为静态或在编译时使用 hidden-attribute),因此不会得到即使加载 RTLD_GLOBAL.

也会插入