Разница между массивом c_char внутри и снаружи структуры в python для библиотеки fortran - PullRequest
1 голос
/ 06 февраля 2020

Я связываю фортранскую библиотеку с python, используя c_types. Я инициализирую структуры в python, передаю их фортрану, который заполняет их, и читаю их обратно в python. Все отлично работает с массивом чисел, но теперь я застрял с сопряженными строковыми массивами.

Я пробовал подобный пример one , и это было нормально, но в этом случае массив c_char не в структуре. Поэтому я попытался изменить предыдущий пример, поместив массив c_char в структуру. Вот код, который я использовал, со структурой и без нее:

Python код:

    from ctypes import *
    lib = CDLL("./libf.so")

    if 1:
        print(">>> Without structure")
        func = getattr(lib, "fortran2py_")
        nstring = pointer(c_long(2))
        carr = (c_char * 255)()
        func.argtypes = [POINTER(c_long), POINTER(c_char)]

        print(type(carr))
        print('before:',carr)
        func(nstring, carr)
        str1, str2 = ''.join([v.decode("utf-8") for v in carr]).rstrip("\x00").split("\x00")
        print(str1, str2)


    class Struct0(Structure):
        _fields_ = [
            ("name", c_char * 255),
        ]

    if 1:    
        print(">>> With structure")
        func = getattr(lib, "fortran2pystr_")
        nstring = pointer(c_long(2))
        carr = Struct0()
        func.argtypes = [POINTER(c_long), POINTER(Struct0)]
        print(type(carr.name))
        print('before:',carr.name)
        func(nstring, byref(carr))
        print('after:',carr.name)

Код Фортрана:

    module c_interop

        use iso_c_binding
        implicit none
        integer, parameter :: STRLEN = 64

        type, bind(c) :: charStr
           character(c_char)  :: name(255)
        end type charStr

        contains

        subroutine fortran2py(nstring, cstring_p) bind(C, name="fortran2py_")
            integer(c_int), intent(in) :: nstring
            character(c_char), dimension(*), intent(inout) :: cstring_p
            integer :: i, j, ks, kf, n
            character(len=STRLEN) :: mystr(2)

            mystr(1) = "This is the first string."
            mystr(2) = "Wow. Fortran + Python + Strings = Pain !"
            ks = 1
            do i = 1, nstring
                n = len_trim(mystr(i))
                kf = ks + (n - 1)  
                cstring_p(ks:kf) = transfer(mystr(i)(1:n), cstring_p(ks:kf))
                cstring_p(kf + 1) = c_null_char
                ks = ks + n + 1
            enddo
        end subroutine fortran2py

        subroutine fortran2pystr(nstring, cstring_p) bind(C, name="fortran2pystr_")
            integer(c_int), intent(in) :: nstring
            type(charStr), intent(inout) :: cstring_p
            integer :: i, j, ks, kf, n
            character(len=STRLEN) :: mystr(2)

            mystr(1) = "This is the first string."
            mystr(2) = "Wow. Fortran + Python + Strings = Pain !"
            ks = 1
            do i = 1, nstring
                n = len_trim(mystr(i))
                kf = ks + (n - 1)  
                cstring_p%name(ks:kf) = transfer(mystr(i)(1:n), cstring_p%name(ks:kf))
                cstring_p%name(kf + 1) = c_null_char
                ks = ks + n + 1
            enddo
        end subroutine fortran2pystr

    end module c_interop

Я не получаю ошибка, за исключением того, что в модифицированной части Fortran должен заполнить массив циклов c_char carr.name для элементов mystr, но полученная строка содержит только первый элемент. Если carr не структура, а непосредственно массив c_char, python может читать все содержимое mystr.

Вывод:

>>> Without structure
<class '__main__.c_char_Array_255'>
before: <__main__.c_char_Array_255 object at 0x151b3b092bf8>
This is the first string. Wow. Fortran + Python + Strings = Pain !
>>> With structure
<class 'bytes'>
before: b''
after: b'This is the first string.'

Как вы можете видеть тип carr и carr .name тоже не одно и то же. Ты хоть представляешь, что не так с моим измененным кодом? Спасибо!

1 Ответ

1 голос
/ 10 февраля 2020

Listing [Python 3.Docs]: ctypes - библиотека сторонних функций для Python.

Причина в том, что CTypes неуловимо ведет себя. c_char (а также c_wchar ) массивы автоматически преобразуются в байт (или str ), когда они присутствуют в виде полей в структуре. Это делается через c_char_p (или c_wchar_p ), которые NUL заканчиваются , что означает, что "массив" будет усеченный , если встретится символ NUL ( 0x00 ), что является именно вашим случаем. Вы можете проверить это, посмотрев на тип поля.
Не знаю, почему это так (возможно, для облегчения использования), но бывают случаи, когда это приносит больше вреда, чем пользы. Он может быть воспроизведен только с кодом Python.

code00.py

#!/usr/bin/env python

import sys
import ctypes as ct


ARR_DIM = 10
CharArr = ct.c_char * ARR_DIM


class CharArrStruct(ct.Structure):
    _fields_ = [
        ("data", CharArr),
    ]


def print_array(arr,  text, size=ARR_DIM):
    print(text)
    for i in range(size):
        print("{0:3d}".format(i), end=" - ")
        try:
            print(arr[i])
        except IndexError:
            print("IndexError!!!")
            break
    print()


def main(*argv):
    arr = CharArr()
    sarr = CharArrStruct()
    print("Array (plain) type: {0:}".format(type(arr)))
    print("Array (in structure) type: {0:}".format(type(sarr.data)))

    string_separator = b"\x00"
    print("\nString separator: {0:}".format(string_separator))
    text = string_separator.join((b"abcd", b"efgh"))
    arr[0:len(text)] = text
    sarr.data = text

    print_array(arr, "Plain array:")
    print_array(sarr.data, "Structure with array:")
    print("Strings (in structure): {0:}".format(sarr.data.split(string_separator)))

    string_separator = b"\xFF"
    print("\nString separator: {0:}".format(string_separator))
    sarr.data = string_separator.join((b"abcd", b"efgh"))

    print_array(sarr.data, "Structure with array:")
    print("Strings (in structure): {0:}".format(sarr.data.split(string_separator)))


if __name__ == "__main__":
    print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    main(*sys.argv[1:])
    print("\nDone.")

Выход :

e:\Work\Dev\StackOverflow\q060093054>"e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" code00.py
Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 64bit on win32

Array (plain) type: <class '__main__.c_char_Array_10'>
Array (in structure) type: <class 'bytes'>

String separator: b'\x00'
Plain array:
  0 - b'a'
  1 - b'b'
  2 - b'c'
  3 - b'd'
  4 - b'\x00'
  5 - b'e'
  6 - b'f'
  7 - b'g'
  8 - b'h'
  9 - b'\x00'

Structure with array:
  0 - 97
  1 - 98
  2 - 99
  3 - 100
  4 - IndexError!!!

Strings (in structure): [b'abcd']

String separator: b'\xff'
Structure with array:
  0 - 97
  1 - 98
  2 - 99
  3 - 100
  4 - 255
  5 - 101
  6 - 102
  7 - 103
  8 - 104
  9 - IndexError!!!

Strings (in structure): [b'abcd', b'efgh']

Done.

Примечания :

  • Как видно, data тип поля был изменен
  • Самое простое решение, которое пришло мне в голову, это заменить разделитель строк с NUL на char , который, как вы уверены, не появится ни в одной из ваших строк . Я выбрал 0xFF ( 255 ). Я думаю, что это было бы возможно и со структурами, содержащими ctypes.POINTER(ctypes.c_char), но это было бы немного сложнее (также я не проверял это)
  • My Fortran знание очень близко к 0 , но что-то выглядит неправильно с fortran2pystr . Я не знаю, как структурированы типы Fortran , но передает массив char , завернутый в указатель struct (действительно, они имеют одинаковый адрес) из Python и обрабатывать его как обычный массив char кажется неправильным. Изменение struct потенциально может стать причиной катастрофы
...