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使用dlopen
和RTLD_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
.
也会插入
从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使用dlopen
和RTLD_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
.