← Архив: Common Lisp

CFFI: Представление сложных данных в лисповом виде

Author: · 03.12.2011 15:57
· original author: shamaz.mazum
Привет всем! Пишу обертку вокруг libFLAC для CL. Кому интересно, можете глянуть, хотя пока она может лишь немногое:
https://github.com/shamazmazum/cl-flac
Возник вопрос, как представлять сложные структуры в более-менее "лисповом" виде (например, в виде структур, определяемых через defstruct или в виде классов) и как представлять многомерные массивы данных (желательно, в виде векторов) так, чтобы конечный пользователь в идеале вообще не пользовался функциями CFFI для доступа к слотам этих структур/элементам массива.
Первый вопрос я с горем пополам решил, написав такой вот макрос:
http://lisper.ru/apps/format/199
Он создает CFFI структуру и соответствующий lisp класс + функцию для конвертации из этой структуры в класс. Вроде работает как надо, но выглядит монструозно. Можно ли решить проблему элегантней?
Со вторым вопросом пока затык.
Примеры сложных структур:
http://flac.sourceforge.net/api/structFLAC____StreamMetadata.html
Примеры многомерных массивов:
http://flac.sourceforge.net/api/group__flac__stream__decoder.html#ga13
см. buffer
Конечно, можно что-то выдумать, но лучше спросить совета.
· original author: shamaz.mazum
Упс, простите за форматирование, думал, что ссылки преобразуются в ссылки автоматически.
https://github.com/shamazmazum/cl-flac
http://flac.sourceforge.net/api/structFLAC____StreamMetadata.html
http://flac.sourceforge.net/api/group__flac__stream__decoder.html#ga13
Как-то так, вроде
· original author: Love5an
я для этого написал целое отдельное FFI  поверх CFFIhttps://github.com/Lovesan/Virgil
доков пока нет, но в целом все почти как в CFFI
вот, можно примеры посмотреть и т.п.
http://love5an.livejournal.com/tag/virgil
еще он в моих библиотеках используется, см:
https://github.com/Lovesan/doors
https://github.com/Lovesan/LDX
https://github.com/Lovesan/Reactivity/blob/master/src/threading-windows.lisp
· original author: shamaz.mazum
Ога, спасибо. Посмотрю, как там всё устроено, может даже перепишу для него.
· original author: shamaz.mazum
Зы, в массиве, ЕМНИП ~4096 элементов.
· original author: shamaz.mazum
Love5an, а как у тебя реализована поддержка массивов?
Ты их поэлементно транслируешь из foreign-типа в лисп?
И да, примерчик бы определения функции, возвращающей массив. Делаю что-то вроде:
(define-external-function
    ("return_array" return-array)
    ()
    ((& (array int) :out) rv rv))

(return-array)
Он пишет, что needs output to be supplied in read operations. Так как указать выход? При беглом взгляде на код пока не понял, как это сделать. Наугад пробовал в виде аргументов к return-array и тоже не прокатило.
Вчера сделал маленький примерчик, декодирующий flac в wav. flac -d работал на файле 7 секунд, мой же примерчик, где я тупейшим образом получал элементы массива при записи в файл и записывал побайтово, работал 23 секунды. Предварительно перегнав буфер в массив так (быдлокод-быдлокод!):
(setq write-buffer (make-array len :element-type '(signed-byte 32)))
......
(loop for i from 0 to (1- len) do (setf (elt write-buffer i) (mem-aref pointer :int32 i)))
(write-sequence write-buffer stream))
.....
тоже никакого выигрыша не получил. Убрав же полностью запись в файл получил исходные 7 секунд. Поэтому так важна быстрая работа с массивами.
· original author: shamaz.mazum
> Зы, в массиве, ЕМНИП ~4096 элементов.
В одном буфере, конечно. Буферов в файле дофига
· original author: Love5an
Он просит место, куда складывать данные из сишной памяти. Просит потому, потому что не знает как узнать, сколько элементов вытащить из Си. В Си с такими метаданными, как длина куска памяти, все довольно сложно - ну типа, их нет.
В данном случае, т.к. это просто возвращаемое значение, требуемое место ему поставить нельзя, так как неоткуда.
Но, само по себе возвращение указателя на непонятно-куда из Си - довольно странное явление, это, мягко говоря, очень плохой стиль.
В таком случае ничего бы не осталось, кроме как возвращать указатель и работать с ним где-то в другом месте.
В случае, когда длина примерно известна, есть следующие варианты.
Если сишная функция возвращает массив статического размера.
Типа так:
static int buffer[4]
    = {1, 2, 3, 4};
int* return_array_size_4()
{
    return buffer;
}

то, спецификация типа массива в Virgil тоже должна содержать этот размер:
(define-external-function
    ("return_array_size_4" (:snake-case))
    ()
    ((& (simple-array int (4)) :out))
)

Это раскрывается в определение лисповой функции с именем "return-array-size-4", у которой нет аргументов, и которая возвращает одномерный массив из 4 интов(точный лисповый тип массива будет (simple-array (signed-byte 32) (4))) каждый раз как вызывается.
Имя rv-переменной, кстати, и rv-форма - опциональные аргументы, их можно не писать, тогда возвращаться будет значение, возвращаемое сишной функцией.
И, кстати, я таки рекомендую использовать simple-array вместо просто array, т.к. они работают на порядок быстрее.
Если длина массива неизвестна, НО, мы знаем что он нуль-терминирован(как сишные строки, например. Но, Virgil умеет в таком стиле работать с данными разных размеров - хоть со структурами, например, главное чтобы размер внутреннего типа последовательности был фиксирован.),  то нужно использовать тип sequence, со значением длины в NIL, что будет указывать на нуль-терминированность.
Пример:
static char str[]
    = {72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33, 0};
char* return_array_null_terminated()
{
    return str;
}

в лиспе сигнатура такая:
(define-external-function
    ("return_array_null_terminated" (:snake-case))
    ()
    ((& (sequence char nil string) :out))
)

Четвертый параметр спецификации типа(лисповый type specifier) обозначает тип последовательности, в которую сишная последовательность будет отображаться. В данном случае - в лисповую строку. Функция каждый раз будет возвращать "Hello, world!"
Но, чем плохи эти варианты, так это тем, что каждый раз при возвращении значения, они будут создавать новые лисповые массивы соответствующего размера, а лишнее выделение памяти - худший враг производительности. Но, если сигнатуру сишной функции изменить, и перейти к более распространенной и более принятой форме, в которой в функцию передается указатель на выделенную заранее память, а функция ее заполняет:
void fill_array(int element, int* array, int length)
{
    while(length--)
        array[length] = element;
}

то в лиспе можно будет сделать так:
(define-external-function
    ("fill_array" (:snake-case))
    ()
    (void rv array)
  (element int)
  (array (& (simple-array int) :out))
  (length int :aux (length array))
)

Это создает лисповую функцию с двумя аргументами - элемент и массив. Длина вычисляется внутри функции - :aux параметры в лисповую сигнатуру не попадают.
Функция возвращает массив, который ей передали. Здесь спецификатор типа массива без указания размерностей разрешен, т.к. размерности вычисляются из лиспового объекта массива.
Чем, кроме прочего, хорош этот вариант, так это еще тем, что указатель на данные лиспового массива в большинстве современных реализаций будет браться прямо из самого массива(например через sb-sys:vector-sap). Если же это невозможно, то, если размер массива в спецификации типа фиксированный, на сишном стеке выделиться память, указатель на нее передастся в Си, и после возврата сишной функции данные перекачаются в лисповый массив. Если размер нефиксирован, память, на время вызова в Си,  выделиться динамическая.
Пример применения:
(let ((array (make-array 10 :element-type 'int)))
  (fill-array 123 array)
)

;; => #(123 123 123 123 123)
Если все-таки сигнатуру поменять нельзя, то для реюза одного лиспового массива можно сделать так:
(defun return-array (array)
  (declare (type (simple-array int (*)) array))
  (let ((pointer (external-function-call
                   "return_array"
                   (()
                    (pointer)
)
)
)
)

    (deref pointer '(simple-array int) 0 array)
    array
)
)

Надо бы доки к Virgil давно написать, но все руки не доходят...
Кстати, если работать с файлами, я бы рекомендовал mmap'ить их(ну или на винде, соответственно, CreateFileMapping и т.п.), создавать окно(ну т.е. MapViewOfFile например) размером кратным "allocation granularity", создавать лисповый массив такого же размера, перекачивать в него данные, как указано в последнем примере, и двигать окно по мере обработки.
· original author: shamaz.mazum
Отлично, спасибо за инфу. Как-нибудь на неделе попробую.
ЗЫ. У меня тут просто безумная идея написать аудио плеер для лиспа по типу mpd, но умеющий ape и CUE-sheet'ы) Даже есть прототипчик, проигрывающий несжатые WAV-файлы)
· original author: kalimehtar
Можешь посмотреть здесь:
http://common-lisp.net/viewvc/gtk-cffi/gtk-cffi/cffi/struct.lisp?view=markup

А вот здесь пример применения:
http://common-lisp.net/viewvc/gtk-cffi/gtk-cffi/gtk/text-tag.lisp?view=markup
Целью было построение простой системы типов CFFI, которая автоматически бы обеспечивала защиту от утечек памяти (на стороне Lisp любой указатель может быть потерян без утечки памяти). Соответственно, любая структура переданная в Lisp для долгосрочного хранения должна стать реальной структурой, но при этом оставить пользователю возможность работать через указатель (например, надо взять структуру, поменяить одно поле, и вернуть обратно)
· original author: shamaz.mazum
Вот только сегодня появилось время разобраться что к чему. Не особенно вчитывался в последний абзац, о чём сейчас жалею (так как потратил много времени на поиски истины)
Оказывается, virgil так быстро конвертирует массивы туда-сюда, потому что там используется какой-то такой макрос: with-pointer-to-vector-data. В документации по cffi такого нет, увы. Вот такой пример:
int* fill_array (int *array, int length)
{
    int val = 5;
    int i;
    for (i=0; i<length; i++) *(array+i)=val;
    return array;
}

(load "asdf/asdf.lisp")
(asdf:load-system :cffi)
(defpackage test
  (:use :cl :cffi)
)

(in-package :test)
(define-foreign-library test
  (T (:default "/home/vasily/test"))
)

(use-foreign-library test)
(defvar *array*
  (make-array 4096 :element-type '(signed-byte 32))
)

(defun read-value (array pointer len)
  (dotimes (i len array)
    (setf (elt array i)
          (mem-aref pointer :int i)
)
)
)

(defcfun ("fill_array" fill-array) :pointer
  (array :pointer)
  (length :int)
)

(time
 (loop for i from 1 to 29654 do
       (with-pointer-to-vector-data (ff *array*)
                                    (fill-array ff 4096)
)
)
)

(format t "~A~%" *array*)
работал всего 0.57 секунд! А до этого я смотрел в сторону методов read-value для массивов (они же читает поэлементно, как я понял) и не понимал, почему у тебя работает так быстро. Ещё хотел бы спросить про какие-то pinned-объекты. Как я понял это какие-то такие объекты, указатель на которые не съедет в течение некоторого времени, например в теле макроса sb-sys:with-pinned-objects. Но ты не пользуешься им, а, похоже, проверяешь pinned-vector-elt-type-p. Почему? Типа, некоторые типы гарантированно не переместятся, или как?
· original author: Love5an
with-pointer-to-vector-data в тех реализациях CL, в которых он есть, гарантирует pinning
· original author: Love5an
а я проверяю, соответственно, какие типы массивов могут использоваться в with-pointer-to-vector-data