我的 python 代码可以访问通过调用的 C 函数接收和返回的实际指针值ctypes
吗?
如果是,我该如何实现?
我想测试传递给共享库函数并从共享库函数返回的指针值,以使用 pytest 测试分配(这里,测试它strdup
没有返回相同的指针而是返回指向不同地址的新指针)。
我把要实现的函数之一(strdup
)包装在一个名为的文件中的一个新 C 函数中,wrapped_strdup.c
以显示指针值和内存区域的内容:
/*
** I'm compiling this into a .so the following way:
** - gcc -o wrapped_strdup.o -c wrapped_strdup.c
** - ar rc wrapped_strdup.a wrapped_strdup.o
** - ranlib wrapped_strdup.a
** - gcc -shared -o wrapped_strdup.so -Wl,--whole-archive wrapped_strdup.a -Wl,--no-whole-archive
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *wrapped_strdup(char *src){
char *dst;
printf("From C:\n");
printf("- src address: %X, src content: [%s].\n", src, src);
dst = strdup(src);
printf("- dst address: %X, dst content: [%s].\n", dst, dst);
return dst;
}
我还在同一目录中创建了一个名为的 pytest 测试文件test_strdup.py
:
#!/usr/bin/env python3
import ctypes
import pytest
# Setting wrapped_strdup:
lib_wrapped_strdup = ctypes.cdll.LoadLibrary("./wrapped_strdup.so")
wrapped_strdup = lib_wrapped_strdup.wrapped_strdup
wrapped_strdup.restype = ctypes.c_char_p
wrapped_strdup.argtypes = [ctypes.c_char_p]
@pytest.mark.parametrize("src", [b"", b"foo"])
def test_strdup(src: bytes):
print("")
dst = wrapped_strdup(src)
print("From Python:")
print(f"- src address: {hex(id(src))}, src content: [{src!r}].")
print(f"- dst address: {hex(id(dst))}, dst content: [{dst!r}].")
assert src == dst
assert hex(id(src)) != hex(id(dst))
然后,运行测试得到以下输出:
$ pytest test_strdup.py --maxfail=2 -v -s
=================================== test session starts ====================================
platform linux -- Python 3.12.5, pytest-8.3.2, pluggy-1.5.0 -- /usr/bin/python
cachedir: .pytest_cache
rootdir: /home/vmonteco/code/MREs/MRe_strdup_test_with_ctypes
plugins: anyio-4.4.0, cov-5.0.0, typeguard-4.3.0
collected 2 items
test_strdup.py::test_strdup[]
From C:
- src address: C19BDBE8, src content: [].
- dst address: 5977DFA0, dst content: [].
From Python:
- src address: 0x75bcc19bdbc8, src content: [b''].
- dst address: 0x75bcc19bdbc8, dst content: [b''].
FAILED
test_strdup.py::test_strdup[foo]
From C:
- src address: BF00A990, src content: [foo].
- dst address: 59791030, dst content: [foo].
From Python:
- src address: 0x75bcbf00a970, src content: [b'foo'].
- dst address: 0x75bcbefc18f0, dst content: [b'foo'].
PASSED
========================================= FAILURES =========================================
______________________________________ test_strdup[] _______________________________________
src = b''
@pytest.mark.parametrize("src", [b"", b"foo"])
def test_strdup(src: bytes):
print("")
dst = wrapped_strdup(src)
print("From Python:")
print(f"- src address: {hex(id(src))}, src content: [{src!r}].")
print(f"- dst address: {hex(id(dst))}, dst content: [{dst!r}].")
assert src == dst
> assert hex(id(src)) != hex(id(dst))
E AssertionError: assert '0x75bcc19bdbc8' != '0x75bcc19bdbc8'
E + where '0x75bcc19bdbc8' = hex(129453562518472)
E + where 129453562518472 = id(b'')
E + and '0x75bcc19bdbc8' = hex(129453562518472)
E + where 129453562518472 = id(b'')
test_strdup.py:22: AssertionError
================================= short test summary info ==================================
FAILED test_strdup.py::test_strdup[] - AssertionError: assert '0x75bcc19bdbc8' != '0x75bcc19bdbc8'
=============================== 1 failed, 1 passed in 0.04s ================================
此输出显示了两件事:
b''
尽管从底层角度看地址不同,但 Python 中引用的变量的地址无论如何都是相同的(即同一个对象)。这与一些纯 Python 测试一致,我猜这可能是某种优化功能。- C 和 Python 中的地址值
dst
和src
变量实际上似乎没有关联。
因此,上述尝试实际上不可靠,无法检查函数是否返回指向不同区域的指针。
我还可以尝试检索指针值本身,并通过更改属性进行第二次测试运行以专门检查此部分restype
:
#!/usr/bin/env python3
import ctypes
import pytest
# Setting wrapped_strdup:
lib_wrapped_strdup = ctypes.cdll.LoadLibrary("./wrapped_strdup.so")
wrapped_strdup = lib_wrapped_strdup.wrapped_strdup
wrapped_strdup.restype = ctypes.c_void_p # Note that it's not a c_char_p anymore.
wrapped_strdup.argtypes = [ctypes.c_char_p]
@pytest.mark.parametrize("src", [b"", b"foo"])
def test_strdup_for_pointers(src: bytes):
print("")
dst = wrapped_strdup(src)
print("From Python:")
print(f"- retrieved dst address: {hex(dst)}.")
以上代码给出以下输出:
$ pytest test_strdup_for_pointers.py --maxfail=2 -v -s
=================================== test session starts ====================================
platform linux -- Python 3.12.5, pytest-8.3.2, pluggy-1.5.0 -- /usr/bin/python
cachedir: .pytest_cache
rootdir: /home/vmonteco/code/MREs/MRe_strdup_test_with_ctypes
plugins: anyio-4.4.0, cov-5.0.0, typeguard-4.3.0
collected 2 items
test_strdup_for_pointers.py::test_strdup_for_pointers[]
From C:
- src address: E15BDBE8, src content: [].
- dst address: 84D4D820, dst content: [].
From Python:
- retrieved dst address: 0x608984d4d820.
PASSED
test_strdup_for_pointers.py::test_strdup_for_pointers[foo]
From C:
- src address: DEC7EA80, src content: [foo].
- dst address: 84EA7C40, dst content: [foo].
From Python:
- retrieved dst address: 0x608984ea7c40.
PASSED
==================================== 2 passed in 0.01s =====================================
这将提供实际的地址(或至少是看起来相关的信息)。
但如果不知道 C 函数接收的值,它就没有多大帮助。
附录:我从马克的回答中得出的结论(并且有效):
这是一个测试,它实现了所接受的答案中建议的解决方案:
#!/usr/bin/env python3
import ctypes
import pytest
# Setting libc:
libc = ctypes.cdll.LoadLibrary("libc.so.6")
strlen = libc.strlen
strlen.restype = ctypes.c_size_t
strlen.argtypes = (ctypes.c_char_p,)
# Setting wrapped_strdup:
lib_wrapped_strdup = ctypes.cdll.LoadLibrary("./wrapped_strdup.so")
wrapped_strdup = lib_wrapped_strdup.wrapped_strdup
# Restype will be set directly in the tests.
wrapped_strdup.argtypes = (ctypes.c_char_p,)
@pytest.mark.parametrize("src", [b"", b"foo"])
def test_strdup(src: bytes):
print("") # Just to make pytest output more readable.
# Set expected result type.
wrapped_strdup.restype = ctypes.POINTER(ctypes.c_char)
# Create the src buffer and retrieve its address.
src_buffer = ctypes.create_string_buffer(src)
src_addr = ctypes.addressof(src_buffer)
src_content = src_buffer[:strlen(src_buffer)]
# Run function to test.
dst = wrapped_strdup(src_buffer)
# Retrieve result address and content.
dst_addr = ctypes.addressof(dst.contents)
dst_content = dst[: strlen(dst)]
# Assertions.
assert src_content == dst_content
assert src_addr != dst_addr
# Output.
print("From Python:")
print(f"- Src content: {src_content!r}. Src address: {src_addr:X}.")
print(f"- Dst content: {dst_content!r}. Dst address: {dst_addr:X}.")
@pytest.mark.parametrize("src", [b"", b"foo"])
def test_strdup_alternative(src: bytes):
print("") # Just to make pytest output more readable.
# Set expected result type.
wrapped_strdup.restype = ctypes.c_void_p
# Create the src buffer and retrieve its address.
src_buffer = ctypes.create_string_buffer(src)
src_addr = ctypes.addressof(src_buffer)
src_content = src_buffer[:strlen(src_buffer)]
# Run function to test.
dst = wrapped_strdup(src_buffer)
# Retrieve result address and content.
dst_addr = dst
# cast dst:
dst_pointer = ctypes.cast(dst, ctypes.POINTER(ctypes.c_char))
dst_content = dst_pointer[:strlen(dst_pointer)]
# Assertions.
assert src_content == dst_content
assert src_addr != dst_addr
# Output.
print("From Python:")
print(f"- Src content: {src_content!r}. Src address: {src_addr:X}.")
print(f"- Dst content: {dst_content!r}. Dst address: {dst_addr:X}.")
输出 :
$ pytest test_strdup.py -v -s
=============================== test session starts ===============================
platform linux -- Python 3.10.14, pytest-8.3.2, pluggy-1.5.0 -- /home/vmonteco/.pyenv/versions/3.10.14/envs/strduo_test/bin/python3.10
cachedir: .pytest_cache
rootdir: /home/vmonteco/code/MREs/MRe_strdup_test_with_ctypes
plugins: anyio-4.4.0, stub-1.1.0
collected 4 items
test_strdup.py::test_strdup[]
From C:
- src address: 661BBE90, src content: [].
- dst address: F5D8A7A0, dst content: [].
From Python:
- Src content: b''. Src address: 7C39661BBE90.
- Dst content: b''. Dst address: 57B4F5D8A7A0.
PASSED
test_strdup.py::test_strdup[foo]
From C:
- src address: 661BBE90, src content: [foo].
- dst address: F5E03340, dst content: [foo].
From Python:
- Src content: b'foo'. Src address: 7C39661BBE90.
- Dst content: b'foo'. Dst address: 57B4F5E03340.
PASSED
test_strdup.py::test_strdup_alternative[]
From C:
- src address: 661BBE90, src content: [].
- dst address: F5B0AC50, dst content: [].
From Python:
- Src content: b''. Src address: 7C39661BBE90.
- Dst content: b''. Dst address: 57B4F5B0AC50.
PASSED
test_strdup.py::test_strdup_alternative[foo]
From C:
- src address: 661BBE90, src content: [foo].
- dst address: F5BF9C20, dst content: [foo].
From Python:
- Src content: b'foo'. Src address: 7C39661BBE90.
- Dst content: b'foo'. Dst address: 57B4F5BF9C20.
PASSED
================================ 4 passed in 0.01s ================================
返回类型为
ctypes.c_char_p
“有用”,并将返回值转换为 Python 字符串,丢失实际的 C 指针。用于ctypes.POINTER(ctypes.c_char)
保留指针。返回类型
ctypes.c_void_p
也很“有用”,它将返回的 C 地址转换为 Python 整数,但可以转换为更具体的指针类型来访问地址处的数据要找到它的地址,请使用指针的内容
ctypes.addressof
;否则您将获得指针存储的地址。我使用它
char* strcpy(char* dest, const char* src)
作为示例,因为返回的指针与dest
指针的地址相同,并且它显示 C 地址与 Python 中的地址相同,而不需要 C 辅助函数。在下面的代码中,可变字符串缓冲区
dest
具有与返回值相同的地址,并显示了几种检查返回值的 C 地址的方法:输出: