Как получить доступ к значениям указателей, передаваемым и возвращаемым функциями C из Python?

Может ли мой код 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 ================================
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
3
0
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Возвращаемый тип 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 возвращаемого значения:

import ctypes as ct

dll = ct.CDLL('msvcrt')

dll.strcpy.argtypes = ct.c_char_p, ct.c_char_p
dll.strcpy.restype = ct.POINTER(ct.c_char)  # NOT ct.c_char_p to keep pointer
dll.strlen.argtypes = ct.c_char_p,
dll.strlen.restype = ct.c_size_t

dest = ct.create_string_buffer(10)  # writable char buffer
print(f'{ct.addressof(dest) = :#x}')  # its C address
result = dll.strcpy(dest, b'abcdefg')

# Note that for strcpy, returned address is the same as dest address
print(f'{ct.addressof(dest) = :#x}')  # dest array's C address
print(f'{ct.addressof(result.contents) = :#x}')  # result pointer's C address (same as dest)
n = dll.strlen(result)
print(f'{result[:n] = }')  # must slice char* or only prints one character.
print(f'{dest.value = }')  # array has .value (nul-termination) or .raw

dll.strcpy.restype = ct.c_void_p  # alternative, get the pointer address as Python int
result = dll.strcpy(dest, b'abcdefg')
print(f'{result = :#x}')  # same C address as above
p = ct.cast(result, ct.POINTER(ct.c_char))  # Cast afterward
print(f'{ct.addressof(p.contents) = :#x}')  # same C address
n = dll.strlen(p)
print(f'{p[:n] = }')  # must slice char*

p = ct.cast(result, ct.POINTER(ct.c_char * n))  # alternate, pointer to sized array
print(f'{p.contents.value = }')  # don't have to slice, (char*)[n] has known size n

Выход:

ct.addressof(dest) = 0x1abe80ea398
ct.addressof(dest) = 0x1abe80ea398
ct.addressof(result.contents) = 0x1abe80ea398
result[:n] = b'abcdefg'
dest.value = b'abcdefg'
result = 0x1abe80ea398
ct.addressof(p.contents) = 0x1abe80ea398
p[:n] = b'abcdefg'
p.contents.value = b'abcdefg'

Спасибо. Это работает, и я реализовал оба этих решения во фрагменте, который добавил в свой пост.

vmonteco 31.08.2024 10:40

Другие вопросы по теме