LINUX.ORG.RU

FFI, сложные типы данных, как лучше?

 ,


1

3

Думаю по поводу того, как должен выглядеть идеальный FFI (foreign function interface) между языком со сборщиком мусора (например, лиспом) и Си.

Для простых типов всё хорошо. А вот что делать со строками, массивами и структурами?

Фактически варианта всего два: транслировать или оставлять указателем.

Вариант с трансляцией мне не нравится тем, что функция «изменить значение головы списка» превратится в «преобразовать весь список, изменить значение головного эдемента, преобразовать список обратно». Хотя видел решения с идеологией «транслировать всё». Например, cl-virgil.

Вариант «всё хранить указателями с тэгами» приводит к наличию в языке двух наборов типов. Строки языка и строки FFI, структуры языка и структуры FFI, массивы языка и массивы FFI. С разными функциями для работы с ними. Получаем протекающую абстракцию... вроде опять плохо.

Если кто-нибудь сталкивался с подобной проблемой, то по какому пути шли и почему?

★★★★★

Последнее исправление: monk (всего исправлений: 1)

Я брал virgil. Там внутри пипец, но всё же как временное решение мне подошло.

Вариант «всё хранить указателями с тэгами» приводит к наличию в языке двух наборов типов.

Ничего не поделать. Можно сделать лисп-типы и типы, связанные с машиной + функции по перегону туда-сюда. А посмотри, как в sbcl сделано. Например массивы можно передать в C через sap

knowledge_seeker
()
Ответ на: комментарий от tailgunner

Посмотри устройство. В virgil там просто копирование в/из foreign-value поэлементно, ЕМНИП

knowledge_seeker
()

Хотя видел решения с идеологией «транслировать всё». Например, cl-virgil.

Это же поделие lovesan. Почему бы его не спросить?

iVS ★★★★★
()
Ответ на: комментарий от iVS

Он сейчас, наверное, будет вещать, что-то про NIF, потому что у него эрланг сечас топ1-язык)

cdshines ★★★★★
()
Ответ на: комментарий от tailgunner

А ctypes из Python - это первый или второй?

>>> TenIntegers = c_int * 10
>>> ii = TenIntegers(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
>>> print ii
<c_long_Array_10 object at 0x...>

>>> i = c_int()
>>> f = c_float()
>>> s = create_string_buffer('\000' * 32)
...
>>> print i.value, f.value, repr(s.value)
1 3.1400001049 'Hello'

Второй. И всюду приходится явно ставить ".value".

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Я не вижу в ctypes «указателей с тегами», да и вообще эта дихотомия кажется несколько натянутой. ctypes по своей сути - это блоки памяти (т.е. данные, «полностью транслированные»), но одновременно это отдельные типы. Пожалуй, я сторонник промежуточного варианта.

tailgunner ★★★★★
()
Последнее исправление: tailgunner (всего исправлений: 1)
Ответ на: комментарий от tailgunner

ctypes по своей сути - это блоки памяти (т.е. данные, «полностью транслированные»),

Как я сказал, в virgil «сложные» данные копируются:

(defmethod write-value (value pointer (type array-type))
  (let* ((elt-type (at-elt-type type))
         (elt-size (compute-fixed-size elt-type)))
    (declare (type pointer pointer)
             (type array value)
             (type non-negative-fixnum elt-size))
    (dotimes (i (array-total-size value) pointer)
      (write-value (row-major-aref value i)
                   (inc-pointer pointer (* i elt-size))
                   elt-type)))

Я не вижу в ctypes «указателей с тегами»

В foreign-данных не может быть никаких тегов. Второй вариант их просто отрезает и дает C-pointer. (SAP в sbcl, вроде, то же самое)

дихотомия

Решил выпендриться?

lispfuerimmer
()
Ответ на: комментарий от lispfuerimmer

Ты смотри, забыл перелогиниться. Теперь все знают, что эти 2 дурака - один хрен.

lispfuerimmer
()
Ответ на: комментарий от tailgunner

ctypes по своей сути - это блоки памяти (т.е. данные, «полностью транслированные»), но одновременно это отдельные типы. Пожалуй, я сторонник промежуточного варианта.

А целые, флоаты - это что, тоже блоки памяти? Вот строки и массивы и вправду можно обозначать 1 типом - указателем. А для чисел нужны другие типы, не?

lispfuerimmer
()
Ответ на: комментарий от tailgunner

Я не вижу в ctypes «указателей с тегами»

Что возвращает c_int(), c_float(), create_string_buffer('\000' * 32)? Если скажешь, что c_int() — структура с целым числом, то расскажи как byref(i), где i = c_int() всегда возвращает один и тот же адрес памяти.

ctypes по своей сути - это блоки памяти (т.е. данные, «полностью транслированные»)

Полностью транслированные можно получить и v.value, где v — переменная с любым из типов ctypes. Да и результат create_string_buffer тоже строкой никак не назовёшь. Более того, .value для него может каждый раз новую строку создавать.

monk ★★★★★
() автор топика
Ответ на: комментарий от lispfuerimmer

Как я сказал, в virgil «сложные» данные копируются

Я уже написал, чем мне это очень не нравится. Получая из FFI структурку

struct node
{
   char* data;
   node *parent, *left, *right;
};

из функции node *get_tree_root();

внезапно вытягиваем всё(!) дерево. И так на каждый вызов любой функции работы с деревом. Или приходится делать на каждую функцию 2-3 типа с разными уровнями детализации (превращаю часть данных в void*).

monk ★★★★★
() автор топика
Ответ на: комментарий от lispfuerimmer

А целые, флоаты - это что, тоже блоки памяти?

Как ни странно — да. c_int() хранит в себе значение и указатель... или даже значение, скорее всего, каждый раз формируется из указателя, иначе было бы сложно сделать byref.

Минус этого подхода в том, что для create_string_buffer строка может быть и достаточно большой. А значит s.value достаточно долгой операцией (копирование строки в память Питона + возможно, трансляция Unicode).

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Что возвращает c_int(), c_float(), create_string_buffer('\000' * 32)

Объекты-обертки для буферов памяти.

Полностью транслированные можно получить и v.value, где v — переменная с любым из типов ctypes

Ты хочешь сказать, что ctypes относится к первому типу? :)

В общем, если говорить в твоих терминах, любой FFI - нечто среднее между двумя подходами; а протекающие абстракции - сама суть FFI.

tailgunner ★★★★★
()
Последнее исправление: tailgunner (всего исправлений: 1)
Ответ на: комментарий от anonymous

Почему нельзя тянуть лениво?

Потому что не Хаскелл...

Есть tree_node = get_tree_root_from_ffi();

С одной стороны хочется не тянуть всё дерево, а с другой, чтобы работало strlen(tree_node.left.right.data) и ffi_set_char(tree_node.left.right.data, 5, 'a').

Тянуть лениво = делать замыкания (ну или структуры) вместо полей. Но меняется семантика. Получается аналог ctypes. И слегка проблематично работать со строками, так как либо надо дописать все функции, умеющие работать со строками, чтобы они умели работать с FFI-указателем, либо каждый раз преобразовывать в родную строку, как в ctypes.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Похоже, что в целом наиболее разумен подход ctypes с хранением foreign данных внутри программы, а трансляцию выполнять максимально лениво. Недостаток только синтаксический, но скорость явно гораздо выше.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Но меняется семантика.

Нет, не меняется. Ты вообще и не заметишь, что там замыкания под капотом.

anonymous
()
Ответ на: комментарий от anonymous

Нет, не меняется. Ты вообще и не заметишь, что там замыкания под капотом.

Ну напиши ленивый вариант

struct node
{
   char* data;
   node *parent, *left, *right;
};

В смысле, как должна выглядеть структура, в которую будут записаны эти данные?

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

В том же питоне из за ленивости получаем

>>> s = c_char_p()
>>> s.value = "abc def ghi"
>>> s.value
'abc def ghi'
>>> s.value is s.value
False

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

В смысле, как должна выглядеть структура, в которую будут записаны эти данные?

Обычная ленивая структура. Упорно не могу понять в чем проблема с этим.

anonymous
()
Ответ на: комментарий от anonymous

Обычная ленивая структура. Упорно не могу понять в чем проблема с этим.

В том, что она не структура (в смысле defstruct). Также как ленивый вектор нельзя будет отправить в функцию, ожидающую вектор.

Или я неправильно представляю, почему и прошу пример. Или пример ленивого массива строк можешь привести.

monk ★★★★★
() автор топика
Ответ на: комментарий от anonymous

Надо же call by need делать

А как угадать этот by need?

s = c_char_p()
s.value = "abc def ghi"
a = s.value
a is s.value /// должно быть True
ffi_printf("%s", s)
a is s.value /// должно быть True
ffi_scanf("%s", s)
a is s.value /// должно быть False
monk ★★★★★
() автор топика
Ответ на: комментарий от monk
#lang racket

(struct yoba (x)
  #:transparent
  #:mutable)

(define (yoba-impersonate v) 
  (impersonate-struct v 
                      yoba-x 
                      (let ([first #t]
                            [res #f])
                        (λ (v x) 
                          (if first
                              (let ([x (x)])
                                (set! first #f)
                                (set! res x)
                                x)
                              res)))))

(define-syntax-rule (make-yoba x)
  (yoba-impersonate (yoba x)))

(define y (make-yoba (λ () (begin (displayln 'forced) 1))))
(yoba-x y)
(yoba-x y)

->
forced
1
1
anonymous
()
Ответ на: комментарий от anonymous
(define y (make-yoba (λ () (begin (displayln 'forced) 1))))

(define (expect-yoba s)
  (set-yoba-x! s 2)
  (+ (yoba-x s) 1))

(expect-yoba y) 

. . application: not a procedure;
 expected a procedure that can be applied to arguments
  given: 2
  arguments...: [none]

Не работает как обычная структура. И через контракт не пройдёт, как мне кажется.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

В Racket, конечно, можно сделать #lazy. И будет «Хаскелл».

Вот только для FFI это не поможет, так как кроме ленивого вычисления нужен ещё и сброс кэша при изменении на foreign стороне.

(define y (make-yoba (λ () (get-from-pointer))))
(yoba-x y) ; -> 1, из указателя
(yoba-x y) ; -> 1, из кэша

(ffi-change y)

(yoba-x y) ; -> 2, из указателя

Причём может быть и такое:

(define y (ffi-get-tree-root))

(tree-data y) ; -> 1, из указателя
(tree-data y) ; -> 1, из кэша

(define leaf (tree-left (tree-right y)))

(ffi-change-tree leaf)

(tree-data y) ; -> 2, из указателя

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Не работает как обычная структура.

Сеттер тоже можно переделать в имперсонаторе.

И через контракт не пройдёт, как мне кажется.

(yoba? y) -> #t

anonymous
()
Ответ на: комментарий от monk

Ты хочешь взаимоисключающих вещей. Вообще, чем тебя не устраивает вариант, в котором для указателей - есть специальный тип указателей? Тебе же все равно нужно различать ситуацию когда в структуре лежит объект и когда в структуре лежит указатель на объект.

anonymous
()
Ответ на: комментарий от anonymous

когда в структуре лежит объект и когда в структуре лежит указатель на объект.

Ну не взаимоисключающих. Я просто показал, почему в ctypes принципиально не кэшируют возвращаемое значение. И почему никаким ленивым вычислением обойтись нельзя.

Вообще, чем тебя не устраивает вариант, в котором для указателей - есть специальный тип указателей?

Что должна возвращать функция «char *get_title(Windget* widget)»? Строку, массив символов или указатель? Если строку, то становится невозможно портировать код

char *title = get_title(Windget* widget);
strcpy(title, "New title");

Если указатель, то пользователь библиотеки будет принуждён почти везде писать (cast (get-title widget) _string) вместо простого get-title. Код будет выглядеть неродным. Можно на каждый такой случай генерировать get-title (-> string) и get-title/ptr (-> _pointer), Но это хорошо, если строка одна, а если возвращается дерево строк? По-умолчанию лениво преобразовать и /ptr для особых случаев (если предполагается передавать обратно в foreign)?

Кстати, наверное это и будет наилучшим вариантом.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Что должна возвращать функция «char *get_title(Windget* widget)»?

Указатель на char.

Если указатель, то пользователь библиотеки будет принуждён почти везде писать

А по-другому нельзя. Если вместо указателя ты будешь получать хранящееся по нему значение, то как тогда передать сам указатель? Максимум можно сделать сахарок для того, чтобы автоматом разыменовывать указатель в конкретных случаях.

Преобразовывать char* в строку ты, конечно, никаокго права не имеешь и никакой ффи этого права делать не имеет - потому что никто тебе не гарантирует, что указатель на чар - это действительно строка. И даже если это строка - в рамках твоего языка это будет максимум массив чаров. Просто потому что строки, скорее всего, хранятся принципиально иначе. А для массива, опять же, придется пробежать по всей строке и посмотреть ее размер.

По-умолчанию лениво преобразовать и /ptr

Только наоборот - по умолчанию птр и разыменовывать в особых явно указанных случаях. При том последнее использовать очень оторожно.

anonymous
()
Ответ на: комментарий от anonymous

по умолчанию птр и разыменовывать в особых явно указанных случаях

Ну представь себе, что в racket/gui (send a-text-field get-value) будет возвращать не строку, а некий char-ptr, из которого потом можно получать строку.

Ведь в потрохах racket/gui имеет FFI к GTK (и вроде к WinAPI). Но нигде указатели вместо строк наружу не возвращает.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Ну представь себе, что в racket/gui (send a-text-field get-value) будет возвращать не строку, а некий char-ptr, из которого потом можно получать строку.

Ну специально для того, чтобы этого не было, в определении ffi-функции в racket можно сделать постпроцессинг результата

И без постпроцессинга тут ничего не сделаешь, т.к. строки на чарах и строки в racket имеют разное внутреннее представление. Можно довольно быстро сделать из char* байтстринг (сделать байтстринг, который указателем на область памяти собственно данных будет ссылаться туда, куда указывает char*) - но опять же, надо пробежать по всему char* до конца, чтобы узнать размер.

anonymous
()
Ответ на: комментарий от anonymous

в определении ffi-функции в racket можно сделать постпроцессинг результата

Любое обращение к чужим данным тогда можно считать постпроцессингом. Просто пишу (_fun _pointer -> _string) и получаю на выходе строку. А если напишу (_fun _pointer -> _pointer), то на выходе получу указатель (который могу потом преобразовать в строку).

Моя точка зрения: не надо пользователю библиотеки давать указатели без крайней нужды. Их очень легко неправильно использовать. И освобождать вручную неудобно.

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

то на выходе получу указатель (который могу потом преобразовать в строку).

Ну да.

Моя точка зрения: не надо пользователю библиотеки давать указатели без крайней нужды.

ну не надо - не используй. пиши (_fun _pointer -> _string), где не надо.

anonymous
()
Ответ на: комментарий от anonymous

пиши (_fun _pointer -> _string), где не надо.

То есть, ты считаешь, что (send a-text-field get-value) должен возвращать указатель? Ведь внутри это прямой вызов к gtk_entry_get_value

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Я же сказал - не хочешь указатель возвращать, делай враппер в виде (_fun _pointer -> _string), какие проблемы-то?

Но это с возвращаемыми значениями хорошо, а что если тебе потом надо это значение сунуть в другую foreign-функции? А что если в структуре лежит указатель и как отличить эту ситуация с ситуацией, когда в структуре лежит значение?

То есть если ты точно знаешь, что никаких проблем с этим не будет и не надо будет потом совать этот указатель в foreign обратно - ну ок тогда.

anonymous
()
Ответ на: комментарий от anonymous

а что если тебе потом надо это значение сунуть в другую foreign-функции? А что если в структуре лежит указатель и как отличить эту ситуация с ситуацией, когда в структуре лежит значение?

Вот эти вопросы и сподвигли меня на создание этой темы.

если тебе потом надо это значение сунуть в другую foreign-функции

Если пользователь знает, что значение надо будет передать в функцию, ожидающую указатель, а не строку (это достаточно редкий случай), он будет вызывать (ffi-func/ptr ...), а не (ffi-func ...).

если в структуре лежит указатель и как отличить эту ситуация с ситуацией, когда в структуре лежит значение

В структуре аналогично. Аксессоры можно делать обычные и /ptr.

Минус тот же, что в ctypes: (eq? (str-field (left struct)) (str-field (left struct))) = #f

monk ★★★★★
() автор топика
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от monk

Минус тот же, что в ctypes: (eq? (str-field (left struct)) (str-field (left struct))) = #f

А в чем здесь минус?

anonymous
()
Ответ на: комментарий от monk

Минус тот же, что в ctypes: (eq? (str-field (left struct)) (str-field (left struct))) = #f

И почему #f бтв? Если постпроцессинга нет, а есть только разыменовывание то все ок

anonymous
()
Ответ на: комментарий от anonymous

Если постпроцессинга нет, а есть только разыменовывание

str-field/ptr возвращает указатель (всегда один и тот же)

(str-field x) = (cast (str-field/ptr x) _pointer _string)

а значит при каждом вызове будет давать новую строку. Кэшировать результат нельзя, так как память по указателю может измениться между вызовами.

Теоретически можно сохранять соответствие (pointer . object) и обновлять содержимое строки, но тогда неясно какую длину строки резервировать...

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

а значит при каждом вызове будет давать новую строку.

Ну так тут постобработка у тебя, да. Чем плохо что результат не eq? Это вполне ожидаемое поведение. Сравнивай по equal тогда.

anonymous
()
Ответ на: комментарий от anonymous

Чем плохо что результат не eq?

Только затратами времени на создание/копирование строки. Впрочем, это меньшее зло по сравнению с вариантами «всегда указатель» (заставлять пользователя библиотеки делать вручную free — верный путь к утечкам памяти и сегфолтам) и «кэшировать данные» (можно получить рассогласование между сишной библиотекой и интерфейсом к ней).

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Только затратами времени на создание/копирование строки.

Ну так ты же сам захотел, чтобы создавалась/копировалась строка. Ты определись - либо ты этого хочешь, либо ты этого не хочешь.

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.