Глава 23 Взаимодействие C с OCaml
В этой главе описывается, как определяемые пользователем примитивы, написанные на C, могут связываться с кодом OCaml и вызываться из функций OCaml, а также как эти функции C могут вызывать код OCaml.
1 Обзор и сведения о компиляции
1.1 Объявление примитивов
|
Пользовательские примитивы объявляются в файле реализации или в выражении модуля struct…end с помощью ключевого слова external:
external name : type = C-function-name
Это определяет имя значения name как функцию типа type, которая выполняется вызовом указанной функции C. Например, так в стандартном библиотечном модуле Stdlib объявлен примитив seek_in:
external seek_in : in_channel -> int -> unit = "caml_ml_seek_in"
Примитивы с несколькими аргументами всегда каррированы. Имя функции C не обязательно совпадает с именем функции ML.
Определенные таким образом внешние функции можно указывать в файлах интерфейсов или сигнатурах sig…end либо как обычные значения
val name : type
скрывая тем самым их реализацию в виде функций C, либо явно как «манифестные» внешние функции
external name : type = C-function-name
Последний способ немного эффективнее, поскольку позволяет клиентам модуля напрямую вызывать функцию C, а не обращаться к соответствующей функции OCaml. Однако его не следует использовать в библиотечных модулях с побочными эффектами на верхнем уровне, поскольку такой прямой вызов мешает алгоритму компоновщика удалять неиспользуемые модули из библиотек во время компоновки.
Арность (число аргументов) примитива автоматически определяется по его типу OCaml в объявлении external путем подсчета стрелок функции в типе. Например, у seek_in выше арность равна 2, и функция C caml_ml_seek_in вызывается с двумя аргументами. Аналогично,
external seek_in_pair: in_channel * int -> unit = "caml_ml_seek_in_pair"
имеет арность 1, и функция C caml_ml_seek_in_pair получает один аргумент (пару значений OCaml).
При определении арности примитива сокращения типов не раскрываются. Например,
type int_endo = int -> int
external f : int_endo -> int_endo = "f"
external g : (int -> int) -> (int -> int) = "f"
у f арность равна 1, а у g — 2. Это позволяет примитиву возвращать функциональное значение (как в примере с f выше): достаточно указать тип возвращаемой функции в сокращении типа.
Язык допускает внешние объявления с одной или двумя строками-флагами в дополнение к имени функции C. Эти флаги зарезервированы для реализации стандартной библиотеки.
1.2 Реализация примитивов
Пользовательские примитивы с арностью n ≤ 5 реализуются функциями C, принимающими n аргументов типа value и возвращающими результат типа value. Тип value используется для представления значений OCaml. Он кодирует объекты нескольких базовых типов (целые числа, числа с плавающей точкой, строки и т. д.), а также структуры данных OCaml. Тип value и связанные с ним функции и макросы преобразования подробно описаны ниже. Например, вот объявление функции C, реализующей примитив In_channel.input, который принимает 4 аргумента:
CAMLprim value input(value channel, value buffer, value offset, value length)
{
...
}
При применении функции-примитива в программе OCaml функция C вызывается со значениями выражений, к которым применяется примитив, в качестве аргументов. Значение, возвращаемое функцией, передается обратно в программу OCaml как результат применения функции.
Пользовательские примитивы с арностью больше 5 следует реализовывать двумя функциями C. Первая функция, используемая вместе с компилятором байт-кода ocamlc, получает два аргумента: указатель на массив значений OCaml (значения аргументов) и целое число, обозначающее количество переданных аргументов. Другая функция, используемая вместе с компилятором машинного кода ocamlopt, принимает аргументы непосредственно. Например, вот две функции C для примитива Nat.add_nat с 7 аргументами:
CAMLprim value add_nat_native(value nat1, value ofs1, value len1,
value nat2, value ofs2, value len2,
value carry_in)
{
...
}
CAMLprim value add_nat_bytecode(value * argv, int argn)
{
return add_nat_native(argv[0], argv[1], argv[2], argv[3],
argv[4], argv[5], argv[6]);
}
Имена двух функций C необходимо указать в объявлении примитива следующим образом:
external name : type =
bytecode-C-function-name native-code-C-function-name
Например, для add_nat объявление выглядит так:
external add_nat: nat -> int -> int -> nat -> int -> int -> int -> int
= "add_nat_bytecode" "add_nat_native"
Реализация пользовательского примитива фактически состоит из двух отдельных задач: с одной стороны, нужно декодировать аргументы, извлекая значения C из переданных значений OCaml, и закодировать возвращаемое значение как значение OCaml; с другой — вычислить результат по аргументам. За исключением совсем простых примитивов, часто удобнее реализовать эти задачи двумя отдельными функциями C. Первая функция непосредственно реализует примитив, принимая в качестве аргументов значения нативных типов C и возвращая значение нативного типа C. Вторая функция, часто называемая «кодом-заглушкой», представляет собой простую обертку над первой: она преобразует аргументы из значений OCaml в значения C, вызывает первую функцию и преобразует возвращенное значение C в значение OCaml. Например, вот код-заглушка для примитива Int64.float_of_bits:
CAMLprim value caml_int64_float_of_bits(value vi)
{
return caml_copy_double(caml_int64_float_of_bits_unboxed(Int64_val(vi)));
}
(Здесь caml_copy_double и Int64_val — функции преобразования и макросы для типа value, которые будут описаны далее. Макрос CAMLprim разворачивается в необходимые директивы компилятора, обеспечивающие экспорт функции и возможность ее вызова из OCaml.) Основная работа выполняется функцией caml_int64_float_of_bits_unboxed, объявленной так:
double caml_int64_float_of_bits_unboxed(int64_t i)
{
...
}
Для написания кода C, работающего со значениями OCaml, предусмотрены следующие заголовочные файлы:
| Заголовочный файл | Содержимое |
| <caml/mlvalues.h> | определение типа value и макросы преобразования |
| <caml/alloc.h> | функции выделения памяти (для создания структурированных объектов OCaml) |
| <caml/memory.h> | различные функции и макросы, связанные с памятью (для взаимодействия со сборщиком мусора, изменения структур на месте и т. д.) |
| <caml/fail.h> | функции для возбуждения исключений (см. раздел 23.4.5) |
| <caml/callback.h> | вызов OCaml из C (см. раздел 23.7). |
| <caml/custom.h> | операции с пользовательскими блоками (см. раздел 23.9). |
| <caml/intext.h> | операции для записи пользовательских функций сериализации и десериализации пользовательских блоков (см. раздел 23.9). |
| <caml/threads.h> | операции для взаимодействия в многопоточной среде (см. раздел 23.12). |
Эти файлы находятся в подкаталоге caml/ каталога стандартной библиотеки OCaml, путь к которому возвращает команда ocamlc -where (обычно это /usr/local/lib/ocaml или /usr/lib/ocaml).
1.3 Статическая компоновка кода C с кодом OCaml
Система времени выполнения OCaml состоит из трех основных частей: интерпретатора байт-кода, менеджера памяти и набора функций C, реализующих примитивные операции. Для вызова этих функций C предусмотрены специальные инструкции байт-кода; они задаются смещением в таблице функций (таблице примитивов).
В режиме по умолчанию компоновщик OCaml создает байт-код для стандартной системы времени выполнения со стандартным набором примитивов. Ссылки на примитивы, отсутствующие в этом стандартном наборе, приводят к ошибке «недоступный примитив C». (Если не поддерживается динамическая загрузка библиотек C — см. раздел 23.1.4 ниже.)
В режиме «пользовательской системы времени выполнения» компоновщик OCaml сканирует объектные файлы и определяет набор необходимых примитивов. Затем он создает подходящую систему времени выполнения, вызывая компоновщик машинного кода с:
- таблицей необходимых примитивов;
- библиотекой, предоставляющей интерпретатор байт-кода, менеджер памяти и стандартные примитивы;
- библиотеками и файлами объектного кода (файлами .o), указанными в командной строке компоновщика OCaml и содержащими реализации пользовательских примитивов.
В результате создается система времени выполнения с необходимыми примитивами. Компоновщик OCaml генерирует байт-код для этой пользовательской системы времени выполнения. Байт-код добавляется в конец пользовательской системы времени выполнения, чтобы он автоматически выполнялся при запуске выходного файла (пользовательская система времени выполнения + байт-код).
Для компоновки в режиме «пользовательской системы времени выполнения» выполните команду ocamlc, указав:
- опцию -custom;
- имена нужных объектных файлов OCaml (файлов .cmo и .cma);
- имена объектных файлов и библиотек C (файлов .o и .a), реализующих необходимые примитивы. В Unix и Windows библиотеку с именем libname.a (или, соответственно, .lib), расположенную в одном из стандартных каталогов библиотек, также можно указать как -cclib -lname.
При использовании компилятора машинного кода ocamlopt флаг -custom не нужен, поскольку заключительный этап компоновки ocamlopt всегда создает автономный исполняемый файл. Чтобы собрать исполняемый файл, объединяющий код OCaml и C, выполните команду ocamlopt, указав:
- имена нужных объектных файлов OCaml для машинного кода (файлов .cmx и .cmxa);
- имена объектных файлов и библиотек C (файлов .o, .a, .so или .dll), реализующих необходимые примитивы.
Начиная с Objective Caml 3.00, в библиотечном файле OCaml .cma или .cmxa можно сохранять опцию -custom и имена библиотек C. Например, рассмотрим библиотеку OCaml mylib.cma, собранную из объектных файлов OCaml a.cmo и b.cmo, которые ссылаются на код C в libmylib.a. Если собрать библиотеку следующим образом:
ocamlc -a -o mylib.cma -custom a.cmo b.cmo -cclib -lmylib
пользователям библиотеки достаточно выполнить компоновку с mylib.cma:
ocamlc -o myprog mylib.cma ...
и система автоматически добавит опции -custom и -cclib -lmylib, что даст тот же результат, что и
ocamlc -o myprog -custom a.cmo b.cmo ... -cclib -lmylib
Можно, конечно, собрать библиотеку и без дополнительных опций:
ocamlc -a -o mylib.cma a.cmo b.cmo
а затем попросить пользователей самостоятельно указать при компоновке опции -custom и -cclib -lmylib:
ocamlc -o myprog -custom mylib.cma ... -cclib -lmylib
Однако первый вариант удобнее для конечных пользователей библиотеки.
1.4 Динамическая компоновка кода C с кодом OCaml
Начиная с Objective Caml 3.03, вместо статической компоновки кода C с помощью режима -custom доступен альтернативный способ. В этом режиме компоновщик OCaml создает обычный исполняемый файл с байт-кодом (без встроенной пользовательской системы времени выполнения), в котором лишь записаны имена динамически загружаемых библиотек с кодом C. Затем стандартная система времени выполнения OCaml ocamlrun динамически загружает эти библиотеки, разрешает ссылки на необходимые примитивы и выполняет байт-код.
В настоящее время эта возможность доступна на всех платформах, поддерживаемых OCaml, за исключением 64-разрядной Cygwin.
Для динамической компоновки кода C с кодом OCaml сначала необходимо скомпилировать код C в общую библиотеку (в Unix) или DLL (в Windows). Для этого нужно: 1 — скомпилировать файлы C с подходящими флагами компилятора C для создания независимого от расположения кода (если этого требует операционная система); 2 — собрать общую библиотеку из полученных объектных файлов. Полученный файл общей библиотеки или DLL необходимо установить в место, где ocamlrun сможет найти его при запуске программы (см. раздел 15.3). Наконец (шаг 3), выполните команду ocamlc, указав
- имена нужных объектных файлов OCaml (файлов .cmo и .cma);
- имена общих библиотек C (файлов .so или .dll), реализующих необходимые примитивы. В Unix и Windows библиотеку с именем dllname.so (или, соответственно, .dll), расположенную в одном из стандартных каталогов библиотек, также можно указать как -dllib -lname.
Не устанавливайте флаг -custom, иначе вы вернетесь к статической компоновке, описанной в разделе 23.1.3. Инструмент ocamlmklib (см. раздел 23.14) автоматизирует шаги 2 и 3.
Как и при статической компоновке, имена библиотек C можно (и рекомендуется) сохранять в архиве библиотеки OCaml .cma. Снова рассмотрим библиотеку OCaml mylib.cma, собранную из объектных файлов OCaml a.cmo и b.cmo, которые ссылаются на код C в dllmylib.so. Если собрать библиотеку следующим образом:
ocamlc -a -o mylib.cma a.cmo b.cmo -dllib -lmylib
пользователям библиотеки достаточно выполнить компоновку с mylib.cma:
ocamlc -o myprog mylib.cma ...
и система автоматически добавит опцию -dllib -lmylib, что даст тот же результат, что и
ocamlc -o myprog a.cmo b.cmo ... -dllib -lmylib
Благодаря этому механизму пользователям библиотеки mylib.cma не нужно знать, что она ссылается на код C, а также разбираться, требуется ли для этого кода статическая компоновка (с помощью -custom) или динамическая.
1.5 Выбор между статической и динамической компоновкой
Описав два способа компоновки кода C с кодом OCaml, теперь рассмотрим их преимущества и недостатки, чтобы помочь разработчикам смешанных библиотек OCaml/C сделать выбор.
Главное преимущество динамической компоновки — сохранение платформенной независимости исполняемых файлов с байт-кодом. Иными словами, такой исполняемый файл не содержит машинного кода, поэтому его можно скомпилировать на платформе A и запустить на других платформах B, C, …, если на всех этих платформах доступны необходимые общие библиотеки. Напротив, исполняемые файлы, созданные с помощью ocamlc -custom, работают только на платформе, где они были собраны, поскольку содержат специально настроенную для этой платформы систему времени выполнения. Кроме того, при динамической компоновке исполняемые файлы получаются меньше.
Еще одно преимущество динамической компоновки — конечным пользователям библиотеки не нужно устанавливать на свои компьютеры компилятор C, компоновщик C и библиотеки времени выполнения C. В Unix и Cygwin это не представляет большой проблемы, однако многие пользователи Windows не хотят устанавливать Microsoft Visual C только ради возможности выполнить ocamlc -custom.
У динамической компоновки есть два недостатка. Первый: полученный исполняемый файл не является автономным — на компьютере, где выполняется код, должны быть установлены общие библиотеки и ocamlrun. Если требуется распространять автономный исполняемый файл, лучше выполнить статическую компоновку с помощью ocamlc -custom -ccopt -static или ocamlopt -ccopt -static. Динамическая компоновка также создает проблему «DLL-ада»: необходимо позаботиться о том, чтобы при запуске были найдены нужные версии общих библиотек.
Второй недостаток динамической компоновки — она усложняет создание библиотеки. Флаги компилятора C и компоновщика для создания независимого от расположения кода и общей библиотеки сильно различаются в разных системах Unix. Кроме того, динамическая компоновка поддерживается не во всех системах Unix, поэтому в Makefile библиотеки требуется предусмотреть резервный вариант со статической компоновкой. Команда ocamlmklib (см. раздел 23.14) призвана скрыть некоторые из этих системных различий.
Итак, в нативной версии для Windows настоятельно рекомендуется использовать динамическую компоновку: с ней нет проблем переносимости, и она гораздо удобнее для конечных пользователей. В Unix динамическую компоновку стоит рассмотреть для зрелых, часто используемых библиотек, поскольку она повышает платформенную независимость исполняемых файлов с байт-кодом. Для новых или редко используемых библиотек проще настроить переносимую статическую компоновку.
1.6 Создание автономных пользовательских систем времени выполнения
Иногда неудобно каждый раз создавать пользовательскую систему времени выполнения при компоновке кода OCaml с библиотеками C, как это делает ocamlc -custom. Во-первых, в некоторых системах создание системы времени выполнения занимает много времени (из-за медленных компоновщиков или медленных удаленных файловых систем); во-вторых, теряется платформенная независимость файлов байт-кода, и приходится выполнять компоновку ocamlc -custom отдельно для каждой интересующей платформы.
Альтернатива ocamlc -custom — отдельно собрать пользовательскую систему времени выполнения с нужными библиотеками C, а затем создать «чистые» исполняемые файлы с байт-кодом (без собственной системы времени выполнения), которые смогут работать в этой пользовательской системе. Это делается с помощью флагов -make-runtime и -use-runtime команды ocamlc. Например, чтобы собрать пользовательскую систему времени выполнения с компонентами C библиотек «Unix» и «Threads», выполните:
ocamlc -make-runtime -o /home/me/ocamlunixrun unix.cma threads.cma
Чтобы создать исполняемый файл с байт-кодом, который будет работать в этой системе времени выполнения, выполните:
ocamlc -use-runtime /home/me/ocamlunixrun -o myprog \
unix.cma threads.cma your .cmo and .cma files
Затем исполняемый файл с байт-кодом myprog можно запустить обычным способом: myprog args или /home/me/ocamlunixrun myprog args.
Обратите внимание, что библиотеки байт-кода unix.cma и threads.cma необходимо указать дважды: при сборке системы времени выполнения (чтобы ocamlc знал, какие примитивы C требуются) и при сборке исполняемого файла с байт-кодом (чтобы байт-код из unix.cma и threads.cma действительно был включен в него).
2 Тип value
Все объекты OCaml представлены типом C value, определенным в заголовочном файле <caml/mlvalues.h> вместе с макросами для работы со значениями этого типа. Объект типа value — это либо:
- неупакованное целое число;
- либо указатель на блок в куче, выделенный одной из функций
caml_alloc_*, описанных в разделе 23.4.4.
2.1 Целочисленные значения
Целочисленные значения кодируют знаковые 63-разрядные целые числа (31-разрядные в 32-разрядных архитектурах). Они не упаковываются (память для них не выделяется).
2.2 Блоки
Блоки в куче подвергаются сборке мусора, поэтому их структура должна строго соответствовать определенным ограничениям. Каждый блок содержит заголовок с размером блока (в словах) и тегом блока. Тег определяет структуру содержимого блока. Тег меньше No_scan_tag обозначает структурированный блок с корректно сформированными значениями, который сборщик мусора обходит рекурсивно. Тег, больший или равный No_scan_tag, обозначает необрабатываемый блок, содержимое которого сборщик мусора не сканирует. Для поддержки таких примитивов, как полиморфное равенство и структурированный ввод-вывод, структурированные и необрабатываемые блоки дополнительно классифицируются по тегам следующим образом:
| Тег | Содержимое блока |
| от 0 до No_scan_tag−1 | Структурированный блок (массив объектов OCaml). Каждое поле имеет тип value. |
| Closure_tag | Замыкание, представляющее функциональное значение. Первое слово — указатель на фрагмент кода, остальные слова — значения value, содержащие окружение. |
| String_tag | Строка символов или последовательность байтов. |
| Double_tag | Число с плавающей точкой двойной точности. |
| Double_array_tag | Массив или запись чисел с плавающей точкой двойной точности. |
| Abstract_tag | Блок, представляющий абстрактный тип данных. |
| Custom_tag | Блок, представляющий абстрактный тип данных, к которому присоединены пользовательские функции финализации, сравнения, хеширования, сериализации и десериализации. |
2.3 Указатели вне кучи
В более ранних версиях OCaml можно было использовать выровненные по слову указатели на адреса вне кучи в качестве значений OCaml, просто приводя указатель к типу value. Начиная с OCaml 5.0 такое использование больше не поддерживается.
Правильный способ работы с указателями на блоки вне кучи из OCaml — хранить эти указатели в блоках OCaml с тегом Abstract_tag или Custom_tag, а затем использовать эти блоки в качестве значений OCaml.
Вот пример инкапсуляции указателей на блоки вне кучи типа C ty * в блоках Abstract_tag. В разделе 23.6 приведён более полный пример с использованием блоков Custom_tag.
/* Create an OCaml value encapsulating the pointer p */
static value val_of_typtr(ty * p)
{
value v = caml_alloc(1, Abstract_tag);
*((ty **) Data_abstract_val(v)) = p;
return v;
}
/* Extract the pointer encapsulated in the given OCaml value */
static ty * typtr_of_val(value v)
{
return *((ty **) Data_abstract_val(v));
}
В качестве альтернативы указатели на блоки вне кучи можно рассматривать как «нативные» целые числа, то есть упакованные 32-битные целые числа на 32-битной платформе и упакованные 64-битные целые числа на 64-битной платформе.
/* Create an OCaml value encapsulating the pointer p */
static value val_of_typtr(ty * p)
{
return caml_copy_nativeint((intnat) p);
}
/* Extract the pointer encapsulated in the given OCaml value */
static ty * typtr_of_val(value v)
{
return (ty *) Nativeint_val(v);
}
Для указателей, выровненных как минимум по границе 2 байт (младший бит гарантированно равен нулю), существует ещё одно допустимое представление в виде целого числа OCaml с тегом.
/* Create an OCaml value encapsulating the pointer p */
static value val_of_typtr(ty * p)
{
assert (((uintptr_t) p & 1) == 0); /* check correct alignment */
return (value) p | 1;
}
/* Extract the pointer encapsulated in the given OCaml value */
static ty * typtr_of_val(value v)
{
return (ty *) (v & ~1);
}
3 Представление типов данных OCaml
В этом разделе описано, как типы данных OCaml кодируются типом value.
3.1 Атомарные типы
| Тип OCaml | Представление |
| int | Нераспакованные целочисленные значения. |
| char | Нераспакованные целочисленные значения (код ASCII). |
| float | Блоки с тегом Double_tag. |
| bytes | Блоки с тегом String_tag. |
| string | Блоки с тегом String_tag. |
| int32 | Блоки с тегом Custom_tag. |
| int64 | Блоки с тегом Custom_tag. |
| nativeint | Блоки с тегом Custom_tag. |
3.2 Кортежи и записи
Кортежи представлены указателями на блоки с тегом 0.
Записи также представлены блоками с нулевым тегом. Порядок меток в объявлении типа записи определяет расположение её полей: значение, связанное с первой объявленной меткой, хранится в поле 0 блока, значение, связанное со второй меткой, — в поле 1 и так далее.
В целях оптимизации записи, все поля которых имеют статический тип float, представляются в виде массивов чисел с плавающей точкой с тегом Double_array_tag. (См. раздел ниже о массивах.)
Ещё одна оптимизация: распаковываемые типы записей представляются особым образом; к распаковываемым относятся неизменяемые типы записей, имеющие только одно поле. Распаковываемый тип может представляться одним из двух способов: в упакованном или распакованном виде. Упакованные типы записей представляются описанным выше способом (блоком с тегом 0 или Double_array_tag). Распакованный тип записи представлен непосредственно значением своего поля (то есть отдельного блока для представления самой записи нет).
Способ представления выбирается по следующим правилам в порядке убывания приоритета:
- Атрибут ([@@boxed] или [@@unboxed]) в объявлении типа.
- Параметр компилятора (-unboxed-types или -no-unboxed-types).
- Представление по умолчанию. В текущей версии OCaml по умолчанию используется упакованное представление.
3.3 Массивы
Массивы целых чисел и указателей представляются так же, как кортежи и записи, то есть в виде указателей на блоки с тегом 0. Для чтения к ним обращаются с помощью макроса Field, а для записи — с помощью функции caml_modify.
Значения типа floatarray (обрабатываемые модулем Float.Array), а также записи, объявление которых содержит только поля типа float, используют эффективное распакованное представление: блоки с тегом Double_array_tag, содержимое которых состоит из необработанных значений double, сами по себе не являющихся допустимыми значениями OCaml. Для доступа к ним следует использовать макросы Double_flat_field и Store_double_flat_field.
Наконец, массивы типа float array могут использовать упакованное или распакованное представление в зависимости от конфигурации компилятора. Сейчас по умолчанию используется распакованное представление, но для переключения на упакованное представление скрипту «configure» можно передать флаг --disable-flat-float-array. Для доступа к ним следует использовать макросы Double_array_field и Store_double_array_field, которые корректно работают в обоих режимах.
3.4 Конкретные типы данных
Сконструированные значения представляются либо нераспакованными целыми числами (для конструкторов без аргументов), либо блоками, тег которых кодирует конструктор (для конструкторов с аргументами). Конструкторы без аргументов и конструкторы с аргументами для заданного конкретного типа нумеруются отдельно, начиная с 0, в порядке их появления в объявлении конкретного типа. Конструктор без аргументов представлен нераспакованным целым числом, равным номеру конструктора. Конструктор с n аргументами представлен блоком размера n с тегом, равным номеру конструктора; n полей содержат его аргументы. Пример:
| Сконструированное значение | Представление |
| () | Val_int(0) |
| false | Val_int(0) |
| true | Val_int(1) |
| [] | Val_int(0) |
| h::t | Блок размера 2 с тегом 0; первое поле содержит h, второе поле — t. |
Для удобства в <caml/mlvalues.h> определены макросы Val_unit, Val_false, Val_true и Val_emptylist для обозначения значений (), false, true и [].
В следующем примере показано, как целые числа и теги блоков назначаются конструкторам:
type t = | A (* First constant constructor -> integer "Val_int(0)" *) | B of string (* First non-constant constructor -> block with tag 0 *) | C (* Second constant constructor -> integer "Val_int(1)" *) | D of bool (* Second non-constant constructor -> block with tag 1 *) | E of t * t (* Third non-constant constructor -> block with tag 2 *)
В целях оптимизации распаковываемые конкретные типы данных представляются особым образом; конкретный тип данных является распаковываемым, если у него ровно один конструктор и этот конструктор имеет ровно один аргумент. Распаковываемые конкретные типы данных представляются так же, как распаковываемые типы записей: см. описание в разделе 23.3.2.
3.5 Объекты
Объекты представлены блоками с тегом Object_tag. Первое поле блока ссылается на класс объекта и связанный с ним набор методов в формате, который сложно использовать из C. Второе поле содержит уникальный идентификатор объекта, используемый для сравнений. Остальные поля объекта содержат значения его переменных экземпляра. Непосредственный доступ к переменным экземпляра небезопасен, поскольку система типов не гарантирует, какие именно переменные экземпляра содержит объект.
Извлечь публичный метод объекта можно с помощью функции C caml_get_public_method (объявленной в <caml/mlvalues.h>.) Поскольку теги публичных методов хешируются так же, как теги вариантов, а методы являются функциями, принимающими self в качестве первого аргумента, для вызова метода foo#bar со стороны C следует вызвать:
callback(caml_get_public_method(foo, caml_hash_variant("bar")), foo);
3.6 Полиморфные варианты
Как и сконструированные значения, значения полиморфных вариантов представляются либо целыми числами (для полиморфных вариантов без аргумента), либо блоками (для полиморфных вариантов с аргументом). В отличие от сконструированных значений, конструкторы вариантов нумеруются не начиная с 0, а идентифицируются хеш-значением (целым числом OCaml), вычисляемым функцией C caml_hash_variant (объявленной в <caml/mlvalues.h>): хеш-значение конструктора варианта с именем, например, VConstr — это caml_hash_variant("VConstr").
Значение варианта `VConstr представлено значением caml_hash_variant("VConstr"). Значение варианта `VConstr(v) представлено блоком размера 2 и с тегом 0: поле с номером 0 содержит caml_hash_variant("VConstr"), а поле с номером 1 — v.
В отличие от сконструированных значений, значения полиморфных вариантов с несколькими аргументами не уплощаются. То есть `VConstr(v, w) представлено блоком размера 2, поле с номером 1 которого содержит представление пары (v, w), а не блоком размера 3, содержащим v и w в полях 1 и 2.
4 Операции над значениями
4.1 Проверка вида значения
- Is_long(v) истинно, если значение v — непосредственное целое число, и ложно в противном случае
- Is_block(v) истинно, если значение v — указатель на блок, и ложно, если это непосредственное целое число.
- Is_none(v) истинно, если значение v — None.
- Is_some(v) истинно, если значение v (предполагается, что оно имеет тип option) соответствует конструктору Some.
4.2 Операции над целыми числами
Заголовочный файл <caml/config.h> определяет целочисленный тип C intnat для знаковых 64-битных целых чисел (32-битных целых чисел на 32-битных архитектурах). Этот тип используется для представления целого числа, закодированного значением value, соответствующим нераспакованному целому числу, без усечения.
- Val_long(n) возвращает значение, кодирующее intnat n.
- Long_val(v) возвращает значение intnat, закодированное в значении v.
- Val_int(i) возвращает значение, кодирующее int i.
- Int_val(v) возвращает значение int, закодированное в значении v. Учтите, что эта операция может привести к усечению значения, поскольку на 64-битных архитектурах размер int составляет 32 бита.
- Val_bool(x) возвращает логическое значение OCaml, соответствующее истинности целого числа C x.
- Bool_val(v) возвращает 0, если v — логическое значение OCaml false, и 1, если v — true.
- Val_true, Val_false представляют логические значения OCaml true и false.
- Val_emptylist представляет пустой список.
- Val_none представляет значение OCaml None.
4.3 Доступ к блокам
- Wosize_val(v) возвращает размер блока v в словах, не включая заголовок.
- Tag_val(v) возвращает тег блока v.
- Field(v, n) возвращает значение, содержащееся в поле номер nго структурированного блока v. Нумерация полей начинается с 0 и заканчивается на Wosize_val(v)−1.
- Store_field(b, n, v) записывает значение v в поле номер n значения b, которое должно быть структурированным блоком.
- Code_val(v) возвращает часть замыкания v, содержащую код.
- caml_string_length(v) возвращает длину (число байтов) строки или последовательности байтов v.
- Byte(v, n) возвращает байт номер nго строки или последовательности байтов v типа char. Нумерация байтов начинается с 0 и заканчивается на string_length(v)−1.
- Byte_u(v, n) возвращает байт номер nго строки или последовательности байтов v типа unsigned char. Нумерация байтов начинается с 0 и заканчивается на string_length(v)−1.
- String_val(v) возвращает указатель на первый байт строки v типа const char *. Этот указатель соответствует корректной строке C: после последнего байта строки находится нулевой байт. Однако строки OCaml могут содержать встроенные нулевые байты, из-за которых обычные функции C для работы со строками будут работать некорректно. Функция caml_string_is_c_safe(v) возвращает true, если строка OCaml v не содержит встроенных нулевых байтов.
- Bytes_val(v) возвращает указатель на первый байт последовательности байтов v типа unsigned char *.
- Double_val(v) возвращает число с плавающей точкой, содержащееся в значении v, типа double.
- Double_array_field(v, n) возвращает элемент номер nго массива float array v.
- Store_double_array_field(v, n, d) записывает число с плавающей точкой двойной точности d в элемент номер nго массива float array v.
- Double_flat_field(v, n) возвращает элемент номер nго значения floatarray или записи с числами с плавающей точкой v (неупакованного блока с тегом Double_array_tag).
- Store_double_flat_field(v, n, d) записывает число с плавающей точкой двойной точности d в элемент номер nго значения floatarray или записи с числами с плавающей точкой v.
- Data_custom_val(v) возвращает указатель на часть пользовательского блока v, содержащую данные. Этот указатель имеет тип void * и должен быть приведён к типу данных, содержащихся в пользовательском блоке.
- Int32_val(v) возвращает 32-битное целое число, содержащееся в значении int32 v.
- Int64_val(v) возвращает 64-битное целое число, содержащееся в значении int64 v.
- Nativeint_val(v) возвращает длинное целое число, содержащееся в значении nativeint v.
- caml_field_unboxed(v) возвращает значение поля значения v любого неупакованного типа (записи или конкретного типа данных).
- caml_field_boxed(v) возвращает значение поля значения v любого упакованного типа (записи или конкретного типа данных).
- caml_field_unboxable(v) вызывает либо caml_field_unboxed, либо caml_field_boxed в зависимости от представления по умолчанию для типов, допускающих упаковку и распаковку, в текущей версии OCaml.
- Some_val(v) возвращает аргумент x значения v вида Some(x).
Выражения Field(v, n), Byte(v, n) и Byte_u(v, n) являются допустимыми l-значениями. Поэтому им можно присваивать значения, изменяя значение v на месте. Прямое присваивание Field(v, n) следует выполнять осторожно, чтобы не нарушить работу сборщика мусора (см. ниже).
4.4 Выделение блоков
Простой интерфейс
- Atom(t) возвращает «атом» (блок нулевого размера) с тегом t. Блоки нулевого размера предварительно выделяются вне кучи. Выделять блок нулевого размера с помощью функций ниже нельзя. Например, Atom(0) представляет пустой массив.
- caml_alloc(n, t) возвращает новый блок размера n с тегом t. Если t меньше No_scan_tag, поля блока инициализируются допустимым значением для соблюдения ограничений сборщика мусора.
- caml_alloc_tuple(n) возвращает новый блок размером n слов с тегом 0.
- caml_alloc_string(n) возвращает значение последовательности байтов (или строки) длиной n байтов. Изначально последовательность содержит неинициализированные байты.
- caml_alloc_initialized_string(n, p) возвращает значение последовательности байтов (или строки) длиной n байтов. Значение инициализируется n байтами, начиная с адреса p.
- caml_copy_string(s) возвращает значение строки или последовательности байтов, содержащее копию строки C s с завершающим нулевым байтом (типа char *).
- caml_copy_double(d) возвращает значение с плавающей точкой, инициализированное значением double d.
- caml_copy_int32(i), caml_copy_int64(i) и caml_copy_nativeint(i) возвращают значения типов OCaml int32, int64 и nativeint соответственно, инициализированные целым числом i.
- caml_alloc_array(f, a) выделяет массив значений, вызывая функцию f для каждого элемента входного массива a, чтобы преобразовать его в значение. Массив a представляет собой массив указателей, завершающийся нулевым указателем. Функция f получает каждый указатель в качестве аргумента и возвращает значение. Блок с тегом 0, возвращаемый функцией alloc_array(f, a), заполняется значениями, возвращёнными последовательными вызовами f. (Эту функцию нельзя использовать для создания массива чисел с плавающей точкой.)
- caml_copy_string_array(p) выделяет массив строк или последовательностей байтов, скопированных из массива строк, на который указывает p (значение типа char **). Значение p должно завершаться NULL.
- caml_alloc_float_array(n) выделяет массив чисел с плавающей точкой размером n. Изначально массив содержит неинициализированные значения.
- caml_alloc_unboxed(v) возвращает значение (любого неупакованного типа), поле которого содержит значение v.
- caml_alloc_boxed(v) выделяет и возвращает значение (любого упакованного типа), поле которого содержит значение v.
- caml_alloc_unboxable(v) вызывает либо caml_alloc_unboxed, либо caml_alloc_boxed в зависимости от представления по умолчанию для типов, допускающих упаковку и распаковку, в текущей версии OCaml.
- caml_alloc_some(v) выделяет блок, представляющий Some(v).
Низкоуровневый интерфейс
Следующие функции немного эффективнее caml_alloc, но пользоваться ими значительно сложнее.
С точки зрения функций выделения памяти блоки делятся на блоки нулевого размера, маленькие блоки (размером не более Max_young_wosize) и большие блоки (размером больше Max_young_wosize). Константа Max_young_wosize объявлена в заголовочном файле <caml/mlvalues.h>. Гарантируется, что её значение не меньше 64 (слов), поэтому любой блок с постоянным размером не более 64 можно считать маленьким. Для блоков, размер которых вычисляется во время выполнения, нужно сравнить его с Max_young_wosize, чтобы определить правильную процедуру выделения памяти.
- caml_alloc_small(n, t) возвращает новый маленький блок размером n ≤ Max_young_wosize слов с тегом t. Если этот блок является структурированным (то есть если t < No_scan_tag), поля блока (первоначально содержащие произвольные данные) нужно инициализировать допустимыми значениями (присвоив их напрямую полям блока) до следующего выделения памяти.
-
caml_alloc_shr(n, t) возвращает новый блок размера n с тегом t. Размер блока может превышать
Max_young_wosize. (Он может быть и меньше, однако в этом случае вызов caml_alloc_small эффективнее вызова caml_alloc_shr.) Если этот блок является структурированным (то есть если t < No_scan_tag), поля блока (первоначально содержащие произвольные данные) нужно инициализировать допустимыми значениями (с помощью описанной ниже функции caml_initialize) до следующего выделения памяти.
4.5 Вызов исключений
Для вызова двух стандартных исключений предусмотрены две функции:
-
caml_failwith(s), где s — строка C с завершающим нулевым байтом (типа
char *), вызывает исключение Failure с аргументом s. -
caml_invalid_argument(s), где s — строка C с завершающим нулевым байтом (типа
char *), вызывает исключение Invalid_argument с аргументом s.
Вызов произвольных исключений из C требует большей осторожности: конструктор исключения выделяется программой OCaml во время выполнения, поэтому его необходимо передать функции C с помощью описанного ниже механизма регистрации (см. раздел 23.7.4). После получения конструктора исключения в C его можно вызвать с помощью следующих функций:
- caml_raise_constant(id) вызывает исключение id без аргументов;
- caml_raise_with_arg(id, v) вызывает исключение id с аргументом — значением OCaml v;
- caml_raise_with_args(id, n, v) вызывает исключение id со значениями OCaml v[0], …, v[n-1] в качестве аргументов;
- caml_raise_with_string(id, s), где s — строка C с завершающим нулевым байтом, вызывает исключение id, передавая в качестве аргумента копию строки C s.
Иногда перед передачей исключения обратно в OCaml необходимо очистить состояние и освободить ресурсы. Для этого предусмотрены альтернативные функции, которые возвращают исключение вместо его непосредственного вызова (caml_exception_failure, caml_exception_invalid_argument и т. д.). Тип caml_result представляет собой либо значение OCaml, либо исключение; см. раздел 23.7.2.
5 Согласованная работа со сборщиком мусора
Неиспользуемые блоки в куче автоматически освобождаются сборщиком мусора. Для этого требуется определённое взаимодействие с кодом C, который работает с выделенными в куче блоками.
5.1 Простой интерфейс
Как правило, любая переменная в коде C, ссылающаяся на значение OCaml, выделенное в куче, должна быть известна среде выполнения во время работы сборщика мусора, поскольку ссылки на объекты, перемещённые сборщиком, необходимо обновлять. Для корректной работы сборщика мусора также нужно отслеживать изменения изменяемых полей. Ниже перечислены простые правила, позволяющие этого добиться. В некоторых случаях правила требуют регистрировать переменные, которые технически в этом не нуждаются (например, если ссылка указывает на непосредственное значение или если гарантируется, что сборщик мусора не будет вызван в течение срока жизни переменной). Однако попытки оптимизировать регистрацию корней часто приводят к ошибкам безопасности, поэтому в большинстве случаев настоятельно рекомендуется следовать приведённым ниже правилам. Механизм регистрации корней реализован эффективно и обычно не снижает производительность.
Все макросы, описанные в этом разделе, объявлены в заголовочном файле <caml/memory.h>.
Существует шесть макросов CAMLparam: CAMLparam0 — CAMLparam5, принимающих соответственно от нуля до пяти аргументов. Если у вашей функции не более пяти параметров типа value, используйте соответствующий макрос, передав эти параметры в качестве аргументов. Если у функции больше пяти параметров типа value, используйте CAMLparam5 с пятью из этих параметров и один или несколько вызовов макросов CAMLxparam для оставшихся параметров (CAMLxparam1 — CAMLxparam5). Если функция принимает аргумент x — массив из value длины n, объявите его с помощью CAMLparamN (x, n) (или CAMLxparamN (x, n), если для других аргументов уже используется вызов CAMLparam).
Макросы CAMLreturn, CAMLreturn0 и CAMLreturnT заменяют ключевое слово C return; любая функция, при входе в которую используется макрос CAMLparam, должна применять CAMLreturn во всех точках выхода. Функции C, экспортируемые как внешние функции OCaml, должны возвращать значение типа value и использовать CAMLreturn (x) вместо return x. Некоторые вспомогательные функции могут работать со значениями OCaml, но возвращать void или данные другого типа. Процедуры, возвращающие void, должны явно использовать CAMLreturn0 и не должны иметь неявного оператора возврата. Вспомогательные функции, возвращающие данные C некоторого типа t, должны использовать CAMLreturnT (t, x) вместо return x.
Примечание:
Некоторые компиляторы C выдают ложные предупреждения о неиспользуемых переменных caml__dummy_xxx при каждом использовании CAMLparam и CAMLlocal. Их можно игнорировать.
Примеры:
CAMLprim value my_external (value v1, value v2, value v3)
{
CAMLparam3 (v1, v2, v3);
...
CAMLreturn (Val_unit);
}
static void helper_procedure (value v1, value v2)
{
CAMLparam2 (v1, v2);
...
CAMLreturn0;
}
static int helper_function (value v1, value v2)
{
CAMLparam2 (v1, v2);
...
CAMLreturnT (int, 0);
}
Примечание:
Если ваша функция является примитивом с более чем 5 аргументами для использования со средой выполнения байт-кода, её аргументы не являются value и не должны объявляться (они имеют типы value * и int).
Предупреждение:
CAMLreturn0 следует использовать только для внутренних процедур, возвращающих void. Для функций, возвращающих значение типа unit в OCaml, следует использовать CAMLreturn(Val_unit). Примитивы (функции C, которые можно вызывать из OCaml) никогда не должны возвращать void.
Макросы от CAMLlocal1 до CAMLlocal5 объявляют и инициализируют от одной до пяти локальных переменных типа value. Имена переменных передаются макросам в качестве аргументов. CAMLlocalN(x, n) объявляет и инициализирует локальную переменную типа value [n]. Если у вас больше 5 локальных переменных, можно несколько раз вызвать эти макросы.
Пример:
CAMLprim value bar (value v1, value v2, value v3)
{
CAMLparam3 (v1, v2, v3);
CAMLlocal1 (result);
result = caml_alloc (3, 0);
...
CAMLreturn (result);
}
Предупреждение:
CAMLlocal (и CAMLxparam) можно вызывать только после CAMLparam. Если функция объявляет локальные значения, но не принимает аргументы типа value, она должна начинаться с CAMLparam0 ().
static value foo (int n)
{
CAMLparam0 ();;
CAMLlocal (result);
...
CAMLreturn (result);
}
Store_field (b, n, v) сохраняет значение v в поле с номером n значения b, которое должно быть блоком (то есть Is_block(b) должно иметь значение true).
Пример:
CAMLprim value bar (value v1, value v2, value v3)
{
CAMLparam3 (v1, v2, v3);
CAMLlocal1 (result);
result = caml_alloc (3, 0);
Store_field (result, 0, v1);
Store_field (result, 1, v2);
Store_field (result, 2, v3);
CAMLreturn (result);
}
Предупреждение:
Первый аргумент Store_field и Store_double_field должен быть переменной, объявленной с помощью CAMLparam*, либо параметром, объявленным с помощью CAMLlocal*. Это необходимо, чтобы сборка мусора, вызванная вычислением других аргументов, не сделала первый аргумент недействительным после его вычисления.
Использование с CAMLlocalN:
В массивы значений, объявленные с помощью CAMLlocalN, нельзя записывать с помощью Store_field. Вместо этого используйте обычный синтаксис массивов C.
То же относится к любой области памяти вне кучи OCaml, которая содержит значение и не гарантированно остаётся доступной — пока содержит это значение — ни из другой зарегистрированной глобальной переменной или области памяти, ни из локальной переменной, объявленной с помощью CAMLlocal, ни из параметра функции, объявленного с помощью CAMLparam.
Глобальную переменную v регистрируют вызовом caml_register_global_root(&v) непосредственно до или после первого сохранения в v допустимого значения; аналогично, произвольную область памяти p регистрируют вызовом caml_register_global_root(p).
Между регистрацией и сохранением значения нельзя вызывать функции или макросы среды выполнения OCaml. Также нельзя сохранять в переменную v (и, соответственно, в область памяти p) что-либо, не являющееся допустимым значением.
При регистрации сборщик мусора обновляет содержимое переменной или области памяти всякий раз, когда значение в ней перемещается внутри кучи OCaml. При использовании потоков необходимо обеспечить надлежащую синхронизацию со средой выполнения OCaml, чтобы избежать гонки с выполнением сборщика мусора при чтении или записи значения. (См. раздел 23.12.2.)
Зарегистрированную глобальную переменную v можно отменить вызовом caml_remove_global_root(&v).
Если содержимое глобальной переменной v после регистрации меняется редко, производительность можно повысить, зарегистрировав v вызовом caml_register_generational_global_root(&v) (после её инициализации допустимым значением value, но до любого выделения памяти или вызова функций сборщика мусора), а затем отменив регистрацию вызовом caml_remove_generational_global_root(&v). В этом случае нельзя напрямую изменять значение v: для присваивания ему значения x необходимо использовать caml_modify_generational_global_root(&v,x). Сборщик мусора использует гарантию того, что значение v не изменяется между вызовами caml_modify_generational_global_root, чтобы сканировать его реже. Это повышает производительность, если значение v изменяется реже, чем выполняются сборки мусора младшего поколения.
Примечание:
Макросы CAML используют идентификаторы (локальные переменные, идентификаторы типов, теги структур), начинающиеся с caml__. Не используйте в своих программах идентификаторы, начинающиеся с caml__.
5.2 Низкоуровневый интерфейс
Теперь приведём правила работы сборщика мусора, соответствующие низкоуровневым функциям выделения памяти caml_alloc_small и caml_alloc_shr. Эти правила можно не учитывать, если вы используете упрощённую функцию выделения памяти caml_alloc.
Field(v, n) = vn;
Если блок выделен с помощью caml_alloc_shr, заполнение выполняется с помощью функции caml_initialize:
caml_initialize(&Field(v, n), vn);
Следующая операция выделения памяти может вызвать сборку мусора. Сборщик мусора предполагает, что все структурированные блоки содержат корректные значения. Созданные блоки содержат случайные данные, которые обычно не являются корректными значениями.
Если вам действительно необходимо выделить память до того, как поля получат окончательные значения, сначала инициализируйте их постоянным значением (например, Val_unit), затем выделите память, а после этого измените поля, присвоив им правильные значения (см. правило 6).
Field(v, n) = w;
безопасно только в том случае, если v — блок, только что выделенный с помощью caml_alloc_small, то есть если между выделением v и присваиванием полю не происходило выделения памяти. Во всех остальных случаях прямые присваивания запрещены. Если блок только что выделен с помощью caml_alloc_shr, для первого присваивания значения полю используйте caml_initialize:
caml_initialize(&Field(v, n), w);
В противном случае вы обновляете поле, в котором ранее находилось корректное значение; тогда вызовите функцию caml_modify:
caml_modify(&Field(v, n), w);
Чтобы проиллюстрировать приведённые выше правила, рассмотрим функцию C, которая строит и возвращает список, содержащий два целых числа, заданных в качестве параметров. Сначала напишем её с использованием упрощённых функций выделения памяти:
value alloc_list_int(int i1, int i2)
{
CAMLparam0 ();
CAMLlocal2 (result, r);
r = caml_alloc(2, 0); /* Allocate a cons cell */
Store_field(r, 0, Val_int(i2)); /* car = the integer i2 */
Store_field(r, 1, Val_emptylist); /* cdr = the empty list [] */
result = caml_alloc(2, 0); /* Allocate the other cons cell */
Store_field(result, 0, Val_int(i1)); /* car = the integer i1 */
Store_field(result, 1, r); /* cdr = the first cons cell */
CAMLreturn (result);
}
В данном случае регистрация result не является строго необходимой, поскольку после присваивания ему значения выделение памяти не происходит. Однако проще и безопаснее регистрировать все локальные переменные типа value.
Вот та же функция, написанная с использованием низкоуровневых функций выделения памяти. Мы видим, что ячейки списка являются небольшими блоками, которые можно выделить с помощью caml_alloc_small и заполнить прямыми присваиваниями их полям.
value alloc_list_int(int i1, int i2)
{
CAMLparam0 ();
CAMLlocal2 (result, r);
r = caml_alloc_small(2, 0); /* Allocate a cons cell */
Field(r, 0) = Val_int(i2); /* car = the integer i2 */
Field(r, 1) = Val_emptylist; /* cdr = the empty list [] */
result = caml_alloc_small(2, 0); /* Allocate the other cons cell */
Field(result, 0) = Val_int(i1); /* car = the integer i1 */
Field(result, 1) = r; /* cdr = the first cons cell */
CAMLreturn (result);
}
В двух приведённых выше примерах список строится снизу вверх. Вот альтернативный способ, при котором построение идёт сверху вниз. Он менее эффективен, но иллюстрирует использование caml_modify.
value alloc_list_int(int i1, int i2)
{
CAMLparam0 ();
CAMLlocal2 (tail, r);
r = caml_alloc_small(2, 0); /* Allocate a cons cell */
Field(r, 0) = Val_int(i1); /* car = the integer i1 */
Field(r, 1) = Val_int(0); /* A dummy value
tail = caml_alloc_small(2, 0); /* Allocate the other cons cell */
Field(tail, 0) = Val_int(i2); /* car = the integer i2 */
Field(tail, 1) = Val_emptylist; /* cdr = the empty list [] */
caml_modify(&Field(r, 1), tail); /* cdr of the result = tail */
CAMLreturn (r);
}
Выполнять прямое присваивание Field(r, 1) = tail было бы неправильно, поскольку выделение памяти для tail произошло после выделения памяти для r.
5.3 Отложенные действия и асинхронные исключения
Начиная с версии 4.10 гарантируется, что функции выделения памяти не запускают код OCaml, включая финализаторы, обработчики сигналов и другие потоки, работающие в том же домене. Вместо этого их выполнение откладывается до следующей безопасной точки.
Функция caml_process_pending_actions из <caml/signals.h> выполняет все ожидающие обработчики сигналов и финализаторы, обратные вызовы Memprof, вытесняющее переключение системных потоков, а также запрошенные сборки мусора младшего и старшего поколений. В частности, она может вызвать асинхронные исключения и привести к изменениям в куче OCaml из того же домена. Рекомендуется регулярно вызывать её в безопасных точках внутри длительно работающего неблокирующего кода C.
Функция caml_process_pending_actions_res возвращает исключение, а не вызывает его непосредственно в коде OCaml. Для этого используется другой тип C — caml_result вместо value. Результат можно проверить с помощью caml_result_is_exception, затем выполнить необходимую очистку и, наконец, вызвать caml_get_value_or_raise, которая возвращает значение (в данном случае просто значение unit, которое игнорируется) или вызывает исключение.
CAMLlocalresult(res);
...
res = caml_process_pending_actions_res();
if(caml_result_is_exception(res)) {
...cleanup...
(void)caml_get_value_or_raise(res);
}
Дополнительные сведения о caml_result см. в разделе 23.7.2.
6 Полный пример
В этом разделе описано, как сделать функции библиотеки Unix curses доступными программам OCaml. Для начала приведём интерфейс curses.ml, в котором объявлены примитивы и типы данных curses:
(* File curses.ml -- declaration of primitives and data types *)
type window (* The type "window" remains abstract *)
external initscr: unit -> window = "caml_curses_initscr"
external endwin: unit -> unit = "caml_curses_endwin"
external refresh: unit -> unit = "caml_curses_refresh"
external wrefresh : window -> unit = "caml_curses_wrefresh"
external newwin: int -> int -> int -> int -> window = "caml_curses_newwin"
external addch: char -> unit = "caml_curses_addch"
external mvwaddch: window -> int -> int -> char -> unit = "caml_curses_mvwaddch"
external addstr: string -> unit = "caml_curses_addstr"
external mvwaddstr: window -> int -> int -> string -> unit
= "caml_curses_mvwaddstr"
(* lots more omitted *)
Чтобы скомпилировать этот интерфейс:
ocamlc -c curses.ml
Чтобы реализовать эти функции, нужно лишь предоставить код-заглушку; основные функции уже реализованы в библиотеке curses. Файл с кодом-заглушкой curses_stubs.c выглядит так:
/* File curses_stubs.c -- stub code for curses */
#include <curses.h>
#include <caml/mlvalues.h>
#include <caml/memory.h>
#include <caml/alloc.h>
#include <caml/custom.h>
/* Encapsulation of opaque window handles (of type WINDOW *)
as OCaml custom blocks. */
static struct custom_operations curses_window_ops = {
"fr.inria.caml.curses_windows",
custom_finalize_default,
custom_compare_default,
custom_hash_default,
custom_serialize_default,
custom_deserialize_default,
custom_compare_ext_default,
custom_fixed_length_default
};
/* Accessing the WINDOW * part of an OCaml custom block */
#define Window_val(v) (*((WINDOW **) Data_custom_val(v)))
/* Allocating an OCaml custom block to hold the given WINDOW * */
static value alloc_window(WINDOW * w)
{
value v = caml_alloc_custom(&curses_window_ops, sizeof(WINDOW *), 0, 1);
Window_val(v) = w;
return v;
}
CAMLprim value caml_curses_initscr(value unit)
{
CAMLparam1 (unit);
CAMLreturn (alloc_window(initscr()));
}
CAMLprim value caml_curses_endwin(value unit)
{
CAMLparam1 (unit);
endwin();
CAMLreturn (Val_unit);
}
CAMLprim value caml_curses_refresh(value unit)
{
CAMLparam1 (unit);
refresh();
CAMLreturn (Val_unit);
}
CAMLprim value caml_curses_wrefresh(value win)
{
CAMLparam1 (win);
wrefresh(Window_val(win));
CAMLreturn (Val_unit);
}
CAMLprim value caml_curses_newwin(value nlines, value ncols, value x0, value y0)
{
CAMLparam4 (nlines, ncols, x0, y0);
CAMLreturn (alloc_window(newwin(Int_val(nlines), Int_val(ncols),
Int_val(x0), Int_val(y0))));
}
CAMLprim value caml_curses_addch(value c)
{
CAMLparam1 (c);
addch(Int_val(c)); /* Characters are encoded like integers */
CAMLreturn (Val_unit);
}
CAMLprim value caml_curses_mvwaddch(value win, value x, value y, value c)
{
CAMLparam4 (win, x, y, c);
mvwaddch(Window_val(win), Int_val(x), Int_val(y), Int_val(c));
CAMLreturn (Val_unit);
}
CAMLprim value caml_curses_addstr(value s)
{
CAMLparam1 (s);
addstr(String_val(s));
CAMLreturn (Val_unit);
}
CAMLprim value caml_curses_mvwaddstr(value win, value x, value y, value s)
{
CAMLparam4 (win, x, y, s);
mvwaddstr(Window_val(win), Int_val(x), Int_val(y), String_val(s));
CAMLreturn (Val_unit);
}
/* This goes on for pages. */
Файл curses_stubs.c можно скомпилировать командой:
cc -c -I`ocamlc -where` curses_stubs.c
или, что ещё проще,
ocamlc -c curses_stubs.c
(Если передать команде ocamlc файл .c, она просто вызовет для него компилятор C с нужным параметром -I.)
А теперь пример программы OCaml prog.ml, использующей модуль curses:
(* File prog.ml -- main program using curses *) open Curses;; let main_window = initscr () in let small_window = newwin 10 5 20 10 in mvwaddstr main_window 10 2 "Hello"; mvwaddstr small_window 4 3 "world"; refresh(); Unix.sleep 5; endwin()
Чтобы скомпилировать и скомпоновать эту программу, выполните команду:
ocamlc -custom -o prog unix.cma curses.cmo prog.ml curses_stubs.o -cclib -lcurses
(На некоторых машинах вместо -cclib -lcurses может понадобиться указать -cclib -lcurses -cclib -ltermcap или -cclib -ltermcap.)
7 Дополнительная тема: обратные вызовы из C в OCaml
До сих пор мы описывали вызов функций C из OCaml. В этом разделе покажем, как функции C могут вызывать функции OCaml — как в виде обратных вызовов (OCaml вызывает C, а та вызывает OCaml), так и в программах, главный код которых написан на C.
7.1 Применение замыканий OCaml из C
Функции C могут применять функциональные значения OCaml (замыкания) к значениям OCaml. Для этого предусмотрены следующие функции:
- caml_callback(f, a) применяет функциональное значение f к значению a и возвращает значение, возвращённое f.
- caml_callback2(f, a, b) применяет функциональное значение f (предполагается, что это каррированная функция OCaml с двумя аргументами) к a и b.
- caml_callback3(f, a, b, c) применяет функциональное значение f (каррированную функцию OCaml с тремя аргументами) к a, b и c.
- caml_callbackN(f, n, args) применяет функциональное значение f к n аргументам, содержащимся в массиве значений C args.
Если функция f не возвращает значение, а вызывает исключение, выходящее за пределы области вызова, это исключение передаётся ближайшему внешнему коду OCaml, минуя код C. То есть если функция OCaml f вызывает функцию C g, которая вызывает функцию OCaml h, вызывающую необработанное исключение, выполнение g прерывается, а исключение передаётся обратно в f.
7.2 caml_result: очистка ресурсов при исключениях OCaml
Если функция OCaml, вызванная с помощью caml_callback (или caml_process_pending_actions, как упоминалось в разделе 23.5.3), вызывает исключение, вызывающая функция C немедленно прерывается, чтобы управление вернулось ближайшему обработчику исключений OCaml. Такое поведение часто нежелательно, если вызывающей функции C необходимо очистить ресурсы для безопасного завершения работы.
FFI OCaml предоставляет альтернативный API для вызова кода OCaml из C, который возвращает значение типа caml_result вместо value. Значение C типа caml_result содержит либо корректно возвращённое функцией OCaml значение OCaml, либо исключение OCaml, вызванное этой функцией. Вызывающий код C может выполнить необходимую очистку, прежде чем повторно вызвать исключение (если оно есть) или продолжить вычисления.
Значениями caml_result можно управлять с помощью следующих функций и макросов:
- value caml_get_value_or_raise(caml_result res) (из <caml/fail.h>) возвращает значение, содержащееся в res, или повторно вызывает содержащееся в нём исключение. В частности, (void)caml_get_value_or_raise(res) можно использовать, чтобы игнорировать результат OCaml типа unit, но при этом передать исключение вызывающему коду.
- Result_value(value v) (из <caml/mlvalues.h>) — результат, представляющий возврат значения v.
- Result_exception(value exn) (из <caml/mlvalues.h>) — результат, представляющий вызов исключения exn.
- int caml_result_is_exception(caml_result res) (из <caml/mlvalues.h>) возвращает true, если res представляет исключение.
- Макрос CAMLlocalresult(foo) (из <caml/memory.h>) — аналог CAMLlocal1 для типа caml_result: он объявляет локальную переменную типа caml_result, содержимое которой отслеживается сборщиком мусора OCaml. Как и CAMLlocal1, его можно использовать только между макросом CAMLparam и макросами CAMLreturn или CAMLreturnT. Аналогов CAMLlocal2, CAMLlocal3 и т. д. нет, но этот макрос можно использовать несколько раз.
Для удобства Result_unit определён как Result_value(Val_unit).
По соглашению, имена функций C, возвращающих результат, оканчиваются на _res, например caml_callback2_res и caml_process_pending_actions_res.
Примеры:
#include <caml/mlvalues.h> // for the caml_result type, caml_result_is_exception
#include <caml/memory.h> // CAMLlocalresult
#include <caml/callback.h> // caml_callback_res
#include <caml/fail.h> // caml_get_value_or_raise
#include <caml/alloc.h> // caml_alloc_2
/* This function calls an OCaml callback,
and returns the value or reraises the exception. */
value logging_callback(value f, value arg) {
CAMLparam2(f, arg);
CAMLlocalresult(res);
printf("Start callback.\n");
res = caml_callback_res(f, arg);
printf("End callback.\n");
CAMLreturn (caml_get_value_or_raise(res));
}
/* This function assumes that the callback returns
a 'unit' value and ignores it -- or reraises
its exception. It must be called by C, as it
does not return a value, but the caller cannot
handle exceptions as they are raised directly. */
void logging_unit_callback(value f, value arg) {
CAMLparam2(f, arg);
CAMLlocalresult(res);
printf("Start unit callback.\n");
res = caml_callback_res(f, arg);
printf("End callback.\n");
(void)caml_get_value_or_raise(res);
CAMLreturn0;
}
/* This function calls a callback on two arguments in turn,
and returns the pair of values;
it does not reraise exceptions, but returns a caml_result instead, letting
its own C caller perform its own cleanup. */
caml_result two_callbacks_res(value f, value arg1, value arg2) {
CAMLparam3(f, arg1, arg2);
CAMLlocalresult(res);
CAMLlocal3(v1, v2, pair);
res = caml_callback_res(f, arg1);
// early exit on exception
if (caml_result_is_exception(res))
CAMLreturnT(caml_result, res);
v1 = caml_get_value_or_raise(res);
res = caml_callback_res(f, arg2);
if (caml_result_is_exception(res))
CAMLreturnT(caml_result, res);
v2 = caml_get_value_or_raise(res);
// build the pair of values, and
// wrap it in a caml_result
pair = caml_alloc_2(0, v1, v2);
res = Result_value(pair);
CAMLreturnT(caml_result, res);
}
Совместимость:
Тип caml_result доступен начиная с OCaml 5.3. В более старых версиях OCaml используется небезопасная концепция encoded exceptions (суффикс _exn, операции Is_exception_result и Extract_exception). Они имеют тип value, но не являются допустимыми значениями OCaml и могут привести к сбою сборщика мусора, если вызывающий код немедленно их не извлечёт. Мы настоятельно рекомендуем вместо этого использовать caml_result, чтобы чётко различать допустимые значения и представленные в виде значений исключения с помощью разных типов C. Однако старый подход по-прежнему доступен для обеспечения обратной совместимости.
7.3 Получение или регистрация замыканий OCaml для использования в функциях C
Есть два способа получить функциональные значения OCaml (замыкания) для передачи функциям callback, описанным выше. Один из способов — передать функцию OCaml в качестве аргумента примитивной функции. Например, если код OCaml содержит объявление
external apply : ('a -> 'b) -> 'a -> 'b = "caml_apply"
соответствующую заглушку C можно написать так:
CAMLprim value caml_apply(value vf, value vx)
{
CAMLparam2(vf, vx);
CAMLlocal1(vy);
vy = caml_callback(vf, vx);
CAMLreturn(vy);
}
Другой вариант — использовать механизм регистрации, предоставляемый OCaml. Он позволяет коду OCaml регистрировать функции OCaml под глобальным именем, а коду C — получать соответствующее замыкание по этому глобальному имени.
В OCaml регистрация выполняется вычислением выражения Callback.register n v. Здесь n — глобальное имя (произвольная строка), а v — значение OCaml. Например:
let f x = print_string "f is applied to "; print_int x; print_newline()
let _ = Callback.register "test function" f
В C указатель на значение, зарегистрированное под именем n, получают вызовом caml_named_value(n). Затем для получения самого значения OCaml необходимо разыменовать возвращённый указатель. Если под именем n не зарегистрировано ни одного значения, возвращается нулевой указатель. Например, ниже приведена функция-обёртка C, вызывающая описанную выше функцию OCaml f:
void call_caml_f(int arg)
{
caml_callback(*caml_named_value("test function"), Val_int(arg));
}
Указатель, возвращённый функцией caml_named_value, является постоянным, поэтому его можно безопасно сохранить в переменной C, чтобы избежать повторного поиска по имени. Значение, на которое он указывает, нельзя изменить из C. Однако оно может измениться во время сборки мусора, поэтому его необходимо заново получать непосредственно в месте использования. Ниже приведён более эффективный вариант описанной выше функции call_caml_f, который вызывает caml_named_value только один раз:
void call_caml_f(int arg)
{
static const value * closure_f = NULL;
if (closure_f == NULL) {
/* First time around, look up by name */
closure_f = caml_named_value("test function");
}
caml_callback(*closure_f, Val_int(arg));
}
7.4 Регистрация исключений OCaml для использования в функциях C
Описанный выше механизм регистрации можно также использовать для передачи конструкторов исключений из OCaml в C. Код OCaml регистрирует конструктор исключения, вычисляя выражение Callback.register_exception n exn, где n — произвольное имя, а exn — значение исключения с регистрируемым конструктором. Например:
exception Error of string
let _ = Callback.register_exception "test exception" (Error "any string")
Затем код C может получить конструктор исключения с помощью caml_named_value и передать его в качестве первого аргумента функциям raise_constant, raise_with_arg и raise_with_string (описанным в разделе 23.4.5), чтобы вызвать исключение. Например, ниже приведена функция C, вызывающая исключение Error с заданным аргументом:
void raise_error(char * msg)
{
caml_raise_with_string(*caml_named_value("test exception"), msg);
}
7.5 Главная программа на C
При обычном выполнении смешанная программа OCaml/C начинает работу с выполнения кода инициализации OCaml, который затем может вызывать функции C. В этом случае главной программой является код OCaml. В некоторых приложениях желательно, чтобы роль главной программы выполнял код C, вызывая функции OCaml при необходимости. Этого можно добиться следующим образом:
- Часть программы на C должна предоставлять функцию main, которая заменит стандартную функцию main, предоставляемую средой выполнения OCaml. Выполнение начнётся в определённой пользователем функции main, как и в обычной программе на C.
- В какой-то момент код на C должен вызвать caml_main(argv), чтобы инициализировать код OCaml. Аргумент argv — это массив строк C (тип char **), завершающийся указателем NULL; он представляет аргументы командной строки, передаваемые вторым аргументом функции main. Массив OCaml Sys.argv будет инициализирован из этого параметра. Компилятор байт-кода также использует argv[0] и argv[1], чтобы найти файл с байт-кодом.
- Вызов caml_main инициализирует среду выполнения OCaml, загружает байт-код (в случае компилятора байт-кода) и выполняет код инициализации программы на OCaml. Обычно этот код инициализации регистрирует функции обратного вызова с помощью Callback.register. После завершения кода инициализации OCaml управление возвращается к коду на C, вызвавшему caml_main.
- Затем код на C может вызывать функции OCaml с помощью механизма обратных вызовов (см. раздел 23.7.1).
7.6 Встраивание кода OCaml в код C
Компилятор байт-кода в режиме пользовательской среды выполнения (ocamlc -custom) обычно добавляет байт-код в исполняемый файл, содержащий пользовательскую среду выполнения. Это имеет два следствия. Во-первых, окончательное связывание должен выполнять ocamlc. Во-вторых, библиотека среды выполнения OCaml должна иметь возможность получить имя исполняемого файла из аргументов командной строки. При использовании caml_main(argv), как описано в разделе 23.7.5, это означает, что argv[0] или argv[1] должно содержать имя исполняемого файла.
В качестве альтернативы байт-код можно встроить в код C. Для этого предназначены параметры -output-obj и -output-complete-obj компилятора ocamlc. Они заставляют компилятор ocamlc выводить объектный файл C (файл .o, в Windows — .obj), содержащий байт-код для части программы на OCaml, а также функцию caml_startup. Объектный файл C, создаваемый командой ocamlc -output-complete-obj, также содержит среду выполнения и библиотеки автоматического связывания. Объектный файл C, созданный командами ocamlc -output-obj или ocamlc -output-complete-obj, можно связать с кодом C с помощью стандартного компилятора C или поместить в библиотеку C.
Чтобы инициализировать среду выполнения OCaml и выполнить код инициализации OCaml, из основной программы на C необходимо вызвать функцию caml_startup. Как и caml_main, она принимает один параметр argv, содержащий параметры командной строки. В отличие от caml_main, этот параметр argv используется только для инициализации Sys.argv, но не для поиска имени исполняемого файла.
Если исключение выходит за пределы инициализатора модуля верхнего уровня, функция caml_startup вызывает обработчик неперехваченных исключений (или запускает отладчик, если программа работает под ocamldebug). Такие исключения можно перехватывать в коде C, используя вместо этого функцию caml_startup_exn и проверяя результат с помощью Is_exception_result (а при необходимости — Extract_exception).
Параметры -output-obj и -output-complete-obj также можно использовать для получения исходного файла C. Что ещё интереснее, эти параметры позволяют непосредственно создать разделяемую библиотеку (файл .so, в Windows — .dll), содержащую код OCaml, среду выполнения OCaml и любой другой статический код C, переданный компилятору ocamlc (файлы .o, .a или, соответственно, .obj, .lib). Такое использование параметров -output-obj и -output-complete-obj очень похоже на обычный этап связывания, но вместо основной программы, автоматически запускающей код OCaml, создаётся разделяемая библиотека, которая может запускать код OCaml по запросу. Три возможных режима работы параметров -output-obj и -output-complete-obj (создание исходного кода C .c, объектного файла C .o или разделяемой библиотеки .so) выбираются в зависимости от расширения результирующего файла (указанного с помощью -o).
Компилятор машинного кода ocamlopt также поддерживает параметры -output-obj и -output-complete-obj. Они заставляют его выводить объектный файл C или разделяемую библиотеку, содержащие машинный код всех модулей OCaml из командной строки, а также код запуска OCaml. Инициализация выполняется вызовом caml_startup (или caml_startup_exn), как и в случае компилятора байт-кода. Файл, созданный командой ocamlopt -output-complete-obj, также содержит среду выполнения и библиотеки автоматического связывания.
На заключительном этапе связывания, помимо объектного файла, созданного с помощью -output-obj, необходимо предоставить библиотеку среды выполнения OCaml (libcamlrun.a для байт-кода, libasmrun.a для машинного кода), а также все библиотеки C, необходимые используемым библиотекам OCaml. Например, предположим, что часть вашей программы на OCaml использует библиотеку Unix. Для ocamlc следует выполнить:
ocamlc -output-obj -o camlcode.o unix.cma other .cmo and .cma files
cc -o myprog C objects and libraries \
camlcode.o -L‘ocamlc -where‘ -lunix -lcamlrun
Для ocamlopt следует выполнить:
ocamlopt -output-obj -o camlcode.o unix.cmxa other .cmx and .cmxa files
cc -o myprog C objects and libraries \
camlcode.o -L‘ocamlc -where‘ -lunix -lasmrun
На заключительном этапе связывания, помимо объектного файла, созданного с помощью -output-complete-obj, потребуется предоставить только библиотеки C, необходимые среде выполнения OCaml.
Например, предположим, что часть вашей программы на OCaml использует библиотеку Unix. Для ocamlc следует выполнить:
ocamlc -output-complete-obj -o camlcode.o unix.cma other .cmo and .cma files
cc -o myprog C objects and libraries \
camlcode.o C libraries required by the runtime, eg -lm -ldl -lcurses -lpthread
Для ocamlopt следует выполнить:
ocamlopt -output-complete-obj -o camlcode.o unix.cmxa other .cmx and .cmxa files
cc -o myprog C objects and libraries \
camlcode.o C libraries required by the runtime, eg -lm -ldl
Предупреждение:
В некоторых вариантах сборки на заключительном этапе связывания объектного файла, созданного с помощью параметров -output-obj и -output-complete-obj, с остальной частью программы требуются специальные параметры. Эти параметры указаны в файле конфигурации Makefile.config, создаваемом при компиляции OCaml, в переменной OC_LDFLAGS.
- Windows с компилятором MSVC: объектный файл, созданный OCaml, скомпилирован с флагом /MD, поэтому все остальные связываемые с ним объектные файлы также должны быть скомпилированы с флагом /MD.
- Другие системы: в зависимости от ОС и компилятора C может потребоваться добавить один или оба параметра: -lm и -ldl.
Трассировки стека.
Если байт-код OCaml, созданный командой ocamlc -g, встроен в программу на C, отладочная информация не включается, поэтому при неперехваченных исключениях невозможно вывести трассировки стека. Для машинного кода, созданного командой ocamlopt -g и встроенного в программу на C, это не так: информация для трассировки стека доступна, но механизм трассировки необходимо включить программно. Это можно сделать со стороны OCaml, вызвав Printexc.record_backtrace true при инициализации одного из модулей OCaml. Это также можно сделать со стороны C, вызвав caml_record_backtraces(1); в связующем коде OCaml-C. (caml_record_backtraces объявлена в <caml/backtrace.h>)
Выгрузка среды выполнения.
Если разделяемая библиотека, созданная с помощью -output-obj, многократно загружается и выгружается одним процессом, необходимо явно выгружать среду выполнения OCaml, чтобы избежать утечек различных системных ресурсов.
Начиная с версии 4.05, для корректного завершения работы среды выполнения можно использовать функцию caml_shutdown. Она выполняет следующие действия:
- Запускает функции, зарегистрированные с помощью Stdlib.at_exit.
- Запускает финализацию выделенных пользовательских блоков (см. раздел 23.9). Например, Stdlib.in_channel и Stdlib.out_channel представлены пользовательскими блоками, содержащими файловые дескрипторы, которые необходимо освободить.
- Выгружает зависимые разделяемые библиотеки, загруженные средой выполнения, включая плагины dynlink.
- Освобождает блоки памяти, выделенные средой выполнения с помощью malloc. В примитивах C для управления статическими (то есть неподвижными) блоками памяти в куче рекомендуется использовать функции caml_stat_* из <caml/memory.h>, поскольку все блоки, выделенные этими функциями, автоматически освобождаются функцией caml_shutdown. Для совместимости с устаревшими заглушками C, в которых caml_stat_* использовались неправильно, это поведение включается только в том случае, если среда выполнения запущена специальной функцией caml_startup_pooled.
Поскольку у разделяемой библиотеки одновременно может быть несколько клиентов, для удобства допускается многократный вызов caml_startup (и caml_startup_pooled) при условии, что каждому такому вызову соответствует вызов caml_shutdown (с соблюдением вложенности). Среда выполнения будет выгружена, когда не останется незавершённых вызовов caml_startup.
После выгрузки среды выполнения её нельзя запустить снова без повторной загрузки разделяемой библиотеки и повторной инициализации её статических данных. Поэтому на данный момент эта возможность полезна только для создания перезагружаемых разделяемых библиотек.
Обработка сигналов Unix.
В зависимости от целевой платформы и операционной системы среда выполнения машинного кода при вызове caml_startup может устанавливать обработчики для одного или нескольких сигналов из списка SIGSEGV, SIGTRAP и SIGFPE, а при вызове caml_shutdown сбрасывать эти сигналы к поведению по умолчанию. Основная программа на C не должна пытаться обрабатывать эти сигналы самостоятельно.
8 Расширенный пример с обратными вызовами
В этом разделе показаны средства обратного вызова, описанные в разделе 23.7. Мы упакуем несколько функций OCaml таким образом, чтобы их можно было связывать с кодом C и вызывать из C, как обычные функции C. Функции OCaml определены в следующем исходном файле OCaml mod.ml:
(* File mod.ml -- some "useful" OCaml functions *) let rec fib n = if n < 2 then n else fib(n-1) + fib(n-2) let format_result n = Printf.sprintf "Result is: %d\n" n (* Export those two functions to C *) let _ = Callback.register "fib" fib let _ = Callback.register "format_result" format_result
Ниже приведён код заглушек C для вызова этих функций из C:
/* File modwrap.c -- wrappers around the OCaml functions */
#include <stdio.h>
#include <string.h>
#include <caml/mlvalues.h>
#include <caml/callback.h>
int fib(int n)
{
static const value * fib_closure = NULL;
if (fib_closure == NULL) fib_closure = caml_named_value("fib");
return Int_val(caml_callback(*fib_closure, Val_int(n)));
}
char * format_result(int n)
{
static const value * format_result_closure = NULL;
if (format_result_closure == NULL)
format_result_closure = caml_named_value("format_result");
return strdup(String_val(caml_callback(*format_result_closure, Val_int(n))));
/* We copy the C string returned by String_val to the C heap
so that it remains valid after garbage collection. */
}
Теперь скомпилируем код OCaml в объектный файл C и поместим его в библиотеку C вместе с кодом заглушек из modwrap.c и средой выполнения OCaml:
ocamlc -custom -output-obj -o modcaml.o mod.ml
ocamlc -c modwrap.c
cp `ocamlc -where`/libcamlrun.a mod.a && chmod +w mod.a
ar r mod.a modcaml.o modwrap.o
(Вместо ocamlc -custom -output-obj можно также использовать ocamlopt -output-obj. В этом случае замените libcamlrun.a (библиотеку среды выполнения байт-кода) на libasmrun.a (библиотеку среды выполнения машинного кода).)
Теперь мы можем использовать функции fib и format_result в любой программе на C так же, как обычные функции C. Не забудьте предварительно один раз вызвать caml_startup (или caml_startup_exn).
/* File main.c -- a sample client for the OCaml functions */
#include <stdio.h>
#include <caml/callback.h>
extern int fib(int n);
extern char * format_result(int n);
int main(int argc, char ** argv)
{
int result;
/* Initialize OCaml code */
caml_startup(argv);
/* Do some computation */
result = fib(10);
printf("fib(10) = %s\n", format_result(result));
return 0;
}
Чтобы собрать всю программу, вызовите компилятор C следующим образом:
cc -o prog -I `ocamlc -where` main.c mod.a -lcurses
(На некоторых машинах вместо -lcurses может потребоваться указать -ltermcap или -lcurses -ltermcap.)
9 Дополнительная тема: пользовательские блоки
Блоки с тегом Custom_tag начинаются с указателя на структуру C типа struct custom_operations, которая задаёт предоставленные пользователем функции финализации, сравнения, хеширования, сериализации и десериализации этого блока.
После этого указателя на пользовательские операции могут располагаться произвольные данные длиной в ноль, одно или несколько машинных слов; их семантика определяется пользователем (например, это может быть просто указатель на структуру C или сама структура C). Используйте Data_custom_val для значения блока, чтобы получить указатель на начало этих произвольных данных.
9.1 Структура struct custom_operations
Структура struct custom_operations определена в <caml/custom.h> и содержит следующие поля:
-
char *identifier
Строка символов с нулевым завершающим символом, используемая в качестве идентификатора для операций сериализации и десериализации. -
void (*finalize)(value v)
Поле finalize содержит указатель на функцию C, которая вызывается, когда блок становится недостижимым и подлежит освобождению. Блок передаётся функции в качестве первого аргумента. В поле finalize также можно указать custom_finalize_default, чтобы обозначить, что для блока не задана функция финализации. Обратите внимание на приведённое ниже предупреждение: поведение таких финализаторов пользовательских блоков сильно ограничено. Для более мощной и гибкой финализации используйте Gc.finalise (см. Gc). -
int (*compare)(value v1, value v2)
Поле compare содержит указатель на функцию C, вызываемую при сравнении двух пользовательских блоков с помощью универсальных операторов сравнения OCaml (=, <>, <=, >=, <, > и compare). Функция C должна возвращать 0, если данные в двух блоках структурно равны, отрицательное целое число, если данные первого блока меньше данных второго, и положительное целое число, если данные первого блока больше данных второго.Для поля compare можно задать значение custom_compare_default; эта функция сравнения по умолчанию просто возбуждает исключение Failure.
-
int (*compare_ext)(value v1, value v2)
(Начиная с версии 3.12.1) Поле compare_ext содержит указатель на функцию C, вызываемую при сравнении пользовательского блока и неупакованного целого числа с помощью универсальных операторов сравнения OCaml (=, <>, <=, >=, <, > и compare). Как и в случае поля compare, функция C должна возвращать 0, если два аргумента структурно равны, отрицательное целое число, если первый аргумент меньше второго, и положительное целое число, если первый аргумент больше второго.Для поля compare_ext можно задать значение custom_compare_ext_default; эта функция сравнения по умолчанию просто возбуждает исключение Failure.
-
intnat (*hash)(value v)
Поле hash содержит указатель на функцию C, вызываемую при применении универсальной операции хеширования OCaml (см. модуль Hashtbl) к пользовательскому блоку. Функция C может возвращать произвольное целое число, представляющее хеш-значение данных в указанном пользовательском блоке. Хеш-значение должно быть совместимо с функцией compare: данные, структурно равные друг другу (то есть два пользовательских блока, для которых compare возвращает 0), должны иметь одинаковое хеш-значение.Для поля hash можно задать значение custom_hash_default; в этом случае пользовательский блок игнорируется при вычислении хеша.
-
void (*serialize)(value v, uintnat * bsize_32, uintnat * bsize_64)
Поле serialize содержит указатель на функцию C, вызываемую при необходимости сериализовать пользовательский блок (маршализовать его) с помощью функций OCaml output_value или Marshal.to_.... Для пользовательского блока эти функции сначала записывают в выходной поток идентификатор блока (указанный в поле identifier), а затем вызывают предоставленную пользователем функцию serialize. Эта функция отвечает за запись данных пользовательского блока с помощью функций serialize_..., определённых в <caml/intext.h> и перечисленных в разделе 23.9.4. Затем предоставленная пользователем функция serialize должна сохранить в параметрах bsize_32 и bsize_64 размеры в байтах части данных пользовательского блока соответственно для 32-разрядной и 64-разрядной архитектур.Для поля serialize можно задать значение custom_serialize_default; в этом случае при попытке сериализовать пользовательский блок возбуждается исключение Failure.
-
uintnat (*deserialize)(void * dst)
Поле deserialize содержит указатель на функцию C, вызываемую при необходимости десериализовать пользовательский блок с идентификатором identifier (выполнить обратную маршализацию) с помощью функций OCaml input_value или Marshal.from_.... Эта функция, предоставленная пользователем, отвечает за чтение данных, записанных операцией serialize, с помощью функций deserialize_..., определённых в <caml/intext.h> и перечисленных в разделе 23.9.4. Затем она должна восстановить часть данных пользовательского блока и записать её по указателю, переданному в аргументе dst. Наконец, она возвращает размер в байтах части данных пользовательского блока. Этот размер должен совпадать с результатом bsize_32 операции serialize для 32-разрядной архитектуры или с bsize_64 для 64-разрядной архитектуры.Для поля deserialize можно задать значение custom_deserialize_default, чтобы указать, что десериализация не поддерживается. В этом случае не регистрируйте struct custom_operations в десериализаторе с помощью register_custom_operations (см. ниже).
-
const struct custom_fixed_length* fixed_length
(Начиная с версии 4.08.0) Обычно в сериализованном выводе резервируется место для записи полей bsize_32 и bsize_64, возвращаемых функцией serialize. Однако для очень коротких пользовательских блоков это место может превышать размер самих данных! Для экономии места, если функция serialize всегда возвращает одинаковые значения bsize_32 и bsize_64, их можно указать в структуре fixed_length; тогда место в сериализованном выводе для них не потребуется.
Внимание: функции finalize, compare, hash, serialize и deserialize, связанные с дескрипторами пользовательских блоков, могут лишь ограниченно взаимодействовать со средой выполнения OCaml. Внутри этих функций не вызывайте функции выделения памяти OCaml, не выполняйте обратные вызовы в код OCaml и не допускайте их вызова (например, не вызывайте caml_release_runtime_system()). Не используйте CAMLparam для регистрации параметров этих функций, CAMLlocal для регистрации их локальных переменных и CAMLreturn для возврата результата. Не возбуждайте исключения. Для сообщения об ошибке при десериализации используйте caml_deserialize_error; функции serialize и deserialize могут свободно использовать функции из раздела 23.9.4. Для финализаторов пользовательских блоков действуют особенно строгие ограничения. Помимо перечисленных выше ограничений, финализаторы не должны обращаться к куче OCaml, изменять её или изменять любую память, достижимую из корней сборщика мусора OCaml. Однако функции finalize могут свободно использовать caml_remove_global_root и caml_remove_generational_global_root. Короче говоря, если финализатор пользовательского блока что-либо делает со значением value, которое он получает, кроме вызова для него Data_custom_val, это, скорее всего, небезопасно. В сомнительных случаях выбирайте более осторожный подход.
9.2 Выделение пользовательских блоков
Пользовательские блоки необходимо выделять с помощью caml_alloc_custom или caml_alloc_custom_mem:
возвращает новый пользовательский блок, содержащий size байт пользовательских данных, а связанные с ним операции задаются параметром ops (указателем на struct custom_operations, обычно статически выделенным как глобальная переменная C).
Два параметра used и max служат для управления скоростью сборки мусора, когда финализируемый объект содержит указатели на ресурсы вне кучи. В общем случае инкрементальный основной сборщик мусора OCaml регулирует свою скорость в зависимости от темпа выделения памяти программой. Чем быстрее программа выделяет память, тем интенсивнее работает сборщик мусора, чтобы быстро освободить недостижимые блоки и не допустить накопления большого количества «плавающего мусора» (неиспользуемых объектов, которые сборщик мусора ещё не собрал).
Обычно темп выделения памяти измеряется подсчётом размера выделенных блоков в куче. Однако финализируемые объекты часто содержат указатели на блоки памяти вне кучи и другие ресурсы (например, дескрипторы файлов, растровые изображения X Windows и т. д.). Для таких блоков их размер в куче не является надёжной мерой объёма ресурсов, выделенных программой.
Аргументы used и max дают сборщику мусора представление о том, сколько ресурсов вне кучи использует выделяемый финализируемый блок: в качестве параметра used указывается объём ресурсов, выделенных этому объекту, а в качестве параметра max — максимальный объём, который допустимо оставить в виде плавающего мусора. Единицы измерения произвольны: сборщик мусора учитывает только отношение used / max.
Например, если вы выделяете финализируемый блок, содержащий растровое изображение X Windows размером w на h пикселей, и хотите, чтобы в виде неосвобождённых растровых изображений оставалось не более 1 мегапикселя, укажите used = w * h и max = 1000000.
Эффект параметров used и max можно также описать через полные циклы сборки мусора. Если вы выделяете много пользовательских блоков с отношением used / max = 1 / N, сборщик мусора будет выполнять один полный цикл (просматривать каждый объект в куче и вызывать функции финализации для недостижимых объектов) каждые N выделений. Например, если used = 1 и max = 1000, сборщик мусора будет выполнять полный цикл как минимум каждые 1000 выделений пользовательских блоков.
Если ваши финализируемые блоки не содержат указателей на ресурсы вне кучи или если предыдущее объяснение оказалось непонятным, просто задайте used = 0 и max = 1. Но если позднее выяснится, что функции финализации вызываются «недостаточно часто», попробуйте увеличить отношение used / max.
Используйте эту функцию, если пользовательский блок содержит только память вне кучи (память, выделенную с помощью malloc или caml_stat_alloc) и не содержит других ресурсов. Значение used должно соответствовать количеству байт памяти вне кучи, хранящихся в пользовательском блоке. Эта функция работает как caml_alloc_custom, но параметр max задаётся пользователем (с помощью параметров custom_major_ratio, custom_minor_ratio и custom_minor_max_size) и пропорционален размеру кучи. Доступна начиная с OCaml 4.08.0.
9.3 Доступ к пользовательским блокам
К данным пользовательского блока v можно получить доступ через указатель Data_custom_val(v). Этот указатель имеет тип void * и должен быть приведён к фактическому типу данных, хранящихся в пользовательском блоке.
Содержимое пользовательских блоков не сканируется сборщиком мусора и поэтому не должно содержать указателей внутрь кучи OCaml. Иными словами, никогда не храните значение OCaml типа value в пользовательском блоке и не используйте Field, Store_field или caml_modify для доступа к данным пользовательского блока. При этом в пользовательском блоке можно хранить любые структуры данных C (не содержащие указателей в кучу).
9.4 Создание пользовательских функций сериализации и десериализации
Для переносимой записи и чтения содержимого пользовательских блоков предусмотрены следующие функции, объявленные в <caml/intext.h>. Эти функции выполняют преобразование порядка байтов, например, когда данные записываются на машине с порядком байтов от младшего к старшему, а читаются на машине с порядком от старшего к младшему.
| Функция | Действие |
| caml_serialize_int_1 | Записывает целое число размером 1 байт |
| caml_serialize_int_2 | Записывает целое число размером 2 байта |
| caml_serialize_int_4 | Записывает целое число размером 4 байта |
| caml_serialize_int_8 | Записывает целое число размером 8 байт |
| caml_serialize_float_4 | Записывает число с плавающей точкой размером 4 байта |
| caml_serialize_float_8 | Записывает число с плавающей точкой размером 8 байт |
| caml_serialize_block_1 | Записывает массив значений размером 1 байт |
| caml_serialize_block_2 | Записывает массив значений размером 2 байта |
| caml_serialize_block_4 | Записывает массив значений размером 4 байта |
| caml_serialize_block_8 | Записывает массив значений размером 8 байт |
| caml_deserialize_uint_1 | Читает беззнаковое целое число размером 1 байт |
| caml_deserialize_sint_1 | Читает знаковое целое число размером 1 байт |
| caml_deserialize_uint_2 | Читает беззнаковое целое число размером 2 байта |
| caml_deserialize_sint_2 | Читает знаковое целое число размером 2 байта |
| caml_deserialize_uint_4 | Читает беззнаковое целое число размером 4 байта |
| caml_deserialize_sint_4 | Читает знаковое целое число размером 4 байта |
| caml_deserialize_uint_8 | Читает беззнаковое целое число размером 8 байт |
| caml_deserialize_sint_8 | Читает знаковое целое число размером 8 байт |
| caml_deserialize_float_4 | Читает число с плавающей точкой размером 4 байта |
| caml_deserialize_float_8 | Читает число с плавающей точкой размером 8 байт |
| caml_deserialize_block_1 | Читает массив значений размером 1 байт |
| caml_deserialize_block_2 | Читает массив значений размером 2 байта |
| caml_deserialize_block_4 | Читает массив значений размером 4 байта |
| caml_deserialize_block_8 | Читает массив значений размером 8 байт |
| caml_deserialize_error | Сообщает об ошибке при десериализации; input_value или Marshal.from_... возбуждает исключение Failure после очистки своих внутренних структур данных |
Функции сериализации привязаны к пользовательским блокам, к которым они применяются. Очевидно, что функции десериализации нельзя привязать таким образом, поскольку в начале десериализации пользовательский блок ещё не существует! Поэтому содержащие функции десериализации структуры struct custom_operations необходимо заранее зарегистрировать в десериализаторе с помощью функции register_custom_operations, объявленной в <caml/custom.h>. Десериализация выполняется чтением идентификатора из входного потока, выделением пользовательского блока размера, указанного во входном потоке, поиском среди зарегистрированных блоков struct custom_operation блока с таким же идентификатором и вызовом его функции deserialize для заполнения области данных пользовательского блока.
9.5 Выбор идентификаторов
Идентификаторы в struct custom_operations необходимо выбирать внимательно, поскольку они должны однозначно идентифицировать структуру данных для операций сериализации и десериализации. В частности, можно включить в идентификатор номер версии; тогда формат данных можно будет изменить позднее, сохранив возможность предоставлять обратно совместимые функции десериализации.
Идентификаторы, начинающиеся с _ (символа подчёркивания), зарезервированы для системы времени выполнения OCaml; не используйте их для своих пользовательских данных. Чтобы свести к минимуму риск совпадения идентификаторов, рекомендуется использовать URL (http://mymachine.mydomain.com/mylibrary/version-number) или имя пакета в стиле Java (com.mydomain.mymachine.mylibrary.version-number).
9.6 Финализируемые блоки
Пользовательские блоки обобщают финализируемые блоки, существовавшие в OCaml до версии 3.00. Для обратной совместимости формат пользовательских блоков совместим с форматом финализируемых блоков, а функция caml_alloc_final по-прежнему доступна для выделения пользовательского блока с заданной функцией финализации, но стандартными функциями сравнения, хеширования и сериализации. (В частности, функция финализации не должна обращаться к среде выполнения OCaml.)
caml_alloc_final(n, f, used, max) возвращает новый пользовательский блок размером n+1 слов с функцией финализации f. Первое слово зарезервировано для хранения пользовательских операций; остальные n слов доступны для ваших данных. Параметры used и max используются для управления скоростью сборки мусора, как описано для caml_alloc_custom.
10 Дополнительная тема: Bigarray и интерфейс OCaml–C
В этом разделе объясняется, как заглушечный код C, связывающий код C или Fortran с кодом OCaml, может использовать Bigarray.
10.1 Подключаемый файл
В файл заглушек C необходимо включить файл <caml/bigarray.h>. В нём объявлены описанные ниже функции, константы и макросы.
10.2 Доступ к Bigarray OCaml из C или Fortran
Если v — значение OCaml типа value, представляющее Bigarray, выражение Caml_ba_data_val(v) возвращает указатель на область данных массива. Этот указатель имеет тип void * и может быть приведён к соответствующему типу C для массива (например, double [], char [][10] и т. д.).
Из C можно получить различные характеристики Bigarray OCaml следующим образом:
| Выражение C | Возвращаемое значение |
| Caml_ba_array_val(v)->num_dims | число измерений |
| Caml_ba_array_val(v)->dim[i] | i-е измерение |
| Caml_ba_array_val(v)->flags & CAML_BA_KIND_MASK | тип элементов массива |
Тип элементов массива — одна из следующих констант:
| Константа | Тип элемента |
| CAML_BA_FLOAT16 | 16-разрядные числа с плавающей точкой половинной точности |
| CAML_BA_FLOAT32 | 32-разрядные числа с плавающей точкой одинарной точности |
| CAML_BA_FLOAT64 | 64-разрядные числа с плавающей точкой двойной точности |
| CAML_BA_SINT8 | 8-разрядные знаковые целые числа |
| CAML_BA_UINT8 | 8-разрядные беззнаковые целые числа |
| CAML_BA_SINT16 | 16-разрядные знаковые целые числа |
| CAML_BA_UINT16 | 16-разрядные беззнаковые целые числа |
| CAML_BA_INT32 | 32-разрядные знаковые целые числа |
| CAML_BA_INT64 | 64-разрядные знаковые целые числа |
| CAML_BA_CAML_INT | 31- или 63-разрядные знаковые целые числа |
| CAML_BA_NATIVE_INT | 32- или 64-разрядные целые числа (нативного для платформы размера) |
| CAML_BA_COMPLEX32 | 32-разрядные комплексные числа с плавающей точкой одинарной точности |
| CAML_BA_COMPLEX64 | 64-разрядные комплексные числа с плавающей точкой двойной точности |
| CAML_BA_CHAR | 8-разрядные символы |
Предупреждение:
Caml_ba_array_val(v) необходимо всегда разыменовывать немедленно и нигде не сохранять, в том числе в локальных переменных. Это выражение возвращает производный указатель: он не является допустимым значением OCaml, а указывает на область памяти, управляемую сборщиком мусора. Поэтому его нельзя сохранять в области памяти, которая может оставаться доступной во время сборки мусора.
В следующем примере показана передача двумерного Bigarray функции C и функции Fortran.
extern void my_c_function(double * data, int dimx, int dimy);
extern void my_fortran_function_(double * data, int * dimx, int * dimy);
CAMLprim value caml_stub(value bigarray)
{
int dimx = Caml_ba_array_val(bigarray)->dim[0];
int dimy = Caml_ba_array_val(bigarray)->dim[1];
/* C passes scalar parameters by value */
my_c_function(Caml_ba_data_val(bigarray), dimx, dimy);
/* Fortran passes all parameters by reference */
my_fortran_function_(Caml_ba_data_val(bigarray), &dimx, &dimy);
return Val_unit;
}
10.3 Обёртывание массива C или Fortran в Bigarray OCaml
Указатель p на уже выделенный массив C или Fortran можно обернуть в Bigarray и вернуть в OCaml с помощью функций caml_ba_alloc или caml_ba_alloc_dims.
-
caml_ba_alloc(kind | layout, numdims, p, dims)
Возвращает Bigarray OCaml, обёртывающий данные, на которые указывает p. Параметр kind задаёт тип элементов массива (одна из приведённых выше констант типа CAML_BA_). Параметр layout равен CAML_BA_C_LAYOUT для массива с раскладкой C и CAML_BA_FORTRAN_LAYOUT для массива с раскладкой Fortran. Параметр numdims задаёт число измерений массива. dims — массив длинных целых чисел размером numdims, задающий размер массива по каждому измерению.
-
caml_ba_alloc_dims(kind | layout, numdims, p, (long) dim1, (long) dim2, …, (long) dimnumdims)
То же, что и caml_ba_alloc, но размеры массива по каждому измерению передаются как дополнительные аргументы вызова функции, а не в виде массива.
В следующем примере показано, как сделать статически выделенные массивы C и Fortran доступными в OCaml.
extern long my_c_array[100][200];
extern float my_fortran_array_[300][400];
CAMLprim value caml_get_c_array(value unit)
{
long dims[2];
dims[0] = 100; dims[1] = 200;
return caml_ba_alloc(CAML_BA_NATIVE_INT | CAML_BA_C_LAYOUT,
2, my_c_array, dims);
}
CAMLprim value caml_get_fortran_array(value unit)
{
return caml_ba_alloc_dims(CAML_BA_FLOAT32 | CAML_BA_FORTRAN_LAYOUT,
2, my_fortran_array_, 300L, 400L);
}
11 Дополнительная тема: более дешёвый вызов C
В этом разделе описано, как снизить стоимость вызова функций C.
Примечание: Это относится только к нативному компилятору. Поэтому при использовании любого из этих методов необходимо предоставить альтернативную заглушку для байт-кодового компилятора, игнорирующую все специальные аннотации.
11.1 Передача неупакованных значений
Ранее мы говорили, что все объекты OCaml представлены типом C value и что для извлечения данных из значений типа value необходимо использовать такие макросы, как Int_val. Однако можно поручить нативному компилятору OCaml выполнить это за нас и передавать аргументы функции C в неупакованном виде. Аналогично можно указать OCaml ожидать неупакованный результат и упаковать его за нас.
Это полезно, поскольку, поручив упаковку «ocamlopt», можно часто добиться её полного устранения.
Рассмотрим, например, следующий пример:
external foo : float -> float -> float = "foo"
let f a b =
let len = Array.length a in
assert (Array.length b = len);
let res = Array.make len 0. in
for i = 0 to len - 1 do
res.(i) <- foo a.(i) b.(i)
done
В OCaml массивы чисел с плавающей точкой хранятся в неупакованном виде, однако функция C foo ожидает аргументы в виде упакованных чисел с плавающей точкой и возвращает упакованное число с плавающей точкой. Поэтому компилятору OCaml приходится упаковывать a.(i) и b.(i), а результат foo — распаковывать. Это приводит к выделению 3 * len временных значений с плавающей точкой.
Если теперь аннотировать аргументы и результат атрибутом [@unboxed], нативный компилятор сможет избежать всех этих выделений:
external foo : (float [@unboxed]) -> (float [@unboxed]) -> (float [@unboxed]) = "foo_byte" "foo"
В этом случае функции C должны выглядеть так:
CAMLprim double foo(double a, double b)
{
...
}
CAMLprim value foo_byte(value a, value b)
{
return caml_copy_double(foo(Double_val(a), Double_val(b)))
}
Для удобства, если все аргументы и результат аннотированы атрибутом [@unboxed], атрибут можно указать только один раз в объявлении. Поэтому можно записать и так:
external foo : float -> float -> float = "foo_byte" "foo" [@@unboxed]
В следующей таблице перечислены типы OCaml, которые можно распаковать, и соответствующие им типы C:
| Тип OCaml | Тип C |
| float | double |
| int32 | int32_t |
| int64 | int64_t |
| nativeint | intnat |
Аналогично можно передавать между OCaml и C целые числа OCaml без тегов. Для этого аргументы и/или результат аннотируют атрибутом [@untagged]:
external f : string -> (int [@untagged]) = "f_byte" "f"
Соответствующий тип C должен быть intnat.
Примечание: Не используйте тип C int в качестве соответствия для (int [@untagged]). Часто их размеры различаются.
Атрибутом [@untagged] можно аннотировать любой непосредственный тип, то есть тип, представленный как int. К ним относятся bool, char и любой вариантный тип, содержащий только конструкторы-константы. Примечание: сюда не входит Unix.file_descr, который не на всех платформах представлен целым числом.
11.2 Прямой вызов C
Чтобы сборщик мусора мог запускаться во время выполнения функции C, нативный компилятор OCaml генерирует вспомогательный код вокруг вызовов C. Технически каждый вызов C оборачивается функцией caml_c_call, входящей в среду выполнения OCaml.
Для небольших часто вызываемых функций это дополнительное косвенное обращение может значительно снизить производительность. Однако оно не требуется, если известно, что функция C не выделяет память, не вызывает исключений и не освобождает блокировку домена (см. раздел 23.12.2). Сообщить об этом нативному компилятору OCaml можно, аннотировав внешнее объявление атрибутом [@@noalloc]:
external bar : int -> int -> int = "foo" [@@noalloc]
В этом случае вызов bar из OCaml обходится так же дёшево, как вызов любой другой функции OCaml, если не считать того, что компилятор OCaml не может встроить функции C...
11.3 Пример: вызов функций библиотеки C без дополнительного косвенного обращения
С помощью этих атрибутов можно вызывать функции библиотеки C без дополнительного косвенного обращения. Например, многие математические функции стандартной библиотеки OCaml определены таким образом:
external sqrt : float -> float = "caml_sqrt_float" "sqrt" [@@unboxed] [@@noalloc] (** Square root. *) external exp : float -> float = "caml_exp_float" "exp" [@@unboxed] [@@noalloc] (** Exponential. *) external log : float -> float = "caml_log_float" "log" [@@unboxed] [@@noalloc] (** Natural logarithm. *)
12 Дополнительная тема: многопоточность
Использование нескольких потоков (параллелизма с общей памятью) в приложении, сочетающем OCaml и C, требует особых мер предосторожности, описанных в этом разделе.
12.1 Регистрация потоков, созданных из C
Обратные вызовы из C в OCaml возможны только в том случае, если вызывающий поток известен системе времени выполнения OCaml. Потоки, созданные из OCaml (с помощью функции Thread.create из библиотеки системных потоков), автоматически становятся известны системе времени выполнения. Если приложение создаёт дополнительные потоки из C и хочет вызывать из этих потоков код OCaml, оно должно сначала зарегистрировать их в системе времени выполнения. Следующие функции объявлены в подключаемом файле <caml/threads.h>.
- caml_c_thread_register() регистрирует вызывающий поток в системе времени выполнения OCaml. Возвращает 1 в случае успеха и 0 в случае ошибки. Повторная регистрация уже зарегистрированного потока ничего не делает и возвращает 0.
- caml_c_thread_register_in_domain(uintnat dom_unique_id) регистрирует вызывающий поток в системе времени выполнения OCaml в домене с идентификатором dom_unique_id, полученным с помощью Domain.self и Domain.get_id. В момент вызова этой функции домен должен быть запущен. Повторная регистрация уже зарегистрированного потока ничего не делает и возвращает 0, даже если ранее он был зарегистрирован в другом домене. Регистрация потока в одном домене, его отмена и последующая регистрация в другом домене не поддерживаются.
- caml_c_thread_unregister() необходимо вызвать до завершения потока, чтобы отменить его регистрацию в системе времени выполнения OCaml. Возвращает 1 в случае успеха и 0 в случае ошибки. Если вызывающий поток ранее не был зарегистрирован, функция ничего не делает и возвращает 0.
12.2 Параллельное выполнение длительного кода C с systhreads
Домены — единица параллелизма в программах OCaml. При использовании библиотеки systhreads к одному домену могут быть присоединены несколько потоков. Однако в каждый момент времени в домене не более одного из этих потоков может выполнять код OCaml или код C, использующий систему времени выполнения OCaml. Технически это обеспечивается «блокировкой домена», которую любой поток должен удерживать при выполнении такого кода в домене.
Когда OCaml вызывает код C, реализующий примитив, блокировка домена удерживается, поэтому код C получает полный доступ к средствам системы времени выполнения. Однако никакой другой поток в том же домене не может одновременно с кодом примитива выполнять код OCaml. Поведение при использовании нескольких доменов см. также в главе 9.6.
Если примитив C выполняется долго или выполняет потенциально блокирующие операции ввода-вывода, он может явно освободить блокировку домена, чтобы другие потоки OCaml в том же домене могли работать параллельно с его операциями. Перед возвратом в OCaml код C должен снова захватить блокировку домена. Для этого используются следующие функции, объявленные в подключаемом файле <caml/threads.h>.
- caml_release_runtime_system() Вызывающий поток освобождает блокировку домена и другие ресурсы OCaml, позволяя другим потокам выполнять код OCaml параллельно с выполнением вызывающего потока.
- caml_acquire_runtime_system() Вызывающий поток снова захватывает блокировку домена и другие ресурсы OCaml. Вызов может блокироваться, пока ни один другой поток в том же домене не использует систему времени выполнения OCaml.
Перед освобождением и после захвата блокировки эти функции проверяют наличие ожидающих сигналов, вызывая асинхронные обратные вызовы (раздел 23.5.3). Поэтому они могут выполнять произвольный код OCaml, в том числе вызывать асинхронное исключение.
После вызова caml_release_runtime_system() и до вызова caml_acquire_runtime_system() код C не должен обращаться к данным OCaml, вызывать какие-либо функции системы времени выполнения или вызывать код OCaml. Следовательно, аргументы, переданные из OCaml примитиву C, необходимо скопировать в структуры данных C до вызова caml_release_runtime_system(), а результаты, которые нужно вернуть в OCaml, следует преобразовать в значения OCaml после возврата из caml_acquire_runtime_system().
Пример: следующий примитив C вызывает rmdir для удаления каталога. Функция rmdir может блокироваться некоторое время, поэтому мы освобождаем систему времени выполнения OCaml на время её выполнения.
#include <caml/memory.h>
#include <caml/threads.h>
#include <caml/unixsupport.h>
CAMLprim value stub_rmdir(value vpath)
{
CAMLparam1(vpath);
/* Raise Unix.Unix_error(Unix.ENOENT, ...) if the OCaml string
contains embedded null bytes */
caml_unix_check_path(vpath, "rmdir");
/* Copy the string argument to a C string, allocated outside the
OCaml heap. */
char_os *path = caml_stat_strdup_to_os(String_val(vpath));
/* Release the OCaml run-time system */
caml_release_runtime_system();
/* Delete the directory */
#ifdef _WIN32
int ret = _wrmdir(path);
#else
int ret = rmdir(path);
#endif
/* Free the copy of the string, which we might as well do before
acquiring the runtime system to benefit from parallelism. */
caml_stat_free(path);
/* Re-acquire the OCaml run-time system */
caml_acquire_runtime_system();
/* Raise Unix.Unix_error(...) if rmdir failed */
if (ret == -1) caml_uerror("rmdir", vpath);
/* Return to OCaml */
CAMLreturn(Val_unit);
}
Макрос Caml_state вычисляется в переменную состояния домена и в режиме отладки проверяет, удерживается ли блокировка домена. В обычном режиме такая проверка также выполняется в ключевых точках входа в API C; поэтому вызов некоторых функций и макросов среды выполнения без надлежащего владения блокировкой домена может привести к фатальной ошибке: no domain lock held. Вариант Caml_state_opt не выполняет проверку и возвращает NULL, если блокировка домена не удерживается. Это позволяет в различных целях определить, удерживает ли в данный момент поток, принадлежащий домену, блокировку этого домена.
Обратные вызовы из C в OCaml должны выполняться при удержании блокировки системы времени выполнения OCaml. Это естественным образом выполняется, если обратный вызов делает примитив C, который не освобождал систему времени выполнения. Если примитив C ранее освободил систему времени выполнения или обратный вызов выполняется из другого кода C, вызванного не из OCaml (например, из цикла обработки событий в приложении с графическим интерфейсом), перед обратным вызовом необходимо захватить систему времени выполнения, а после него — освободить:
caml_acquire_runtime_system(); /* Resolve OCaml function vfun to be invoked */ /* Build OCaml argument varg to the callback */ vres = callback(vfun, varg); /* Copy relevant parts of result vres to C data structures */ caml_release_runtime_system();
Примечание: описанные выше функции acquire и release появились в OCaml 3.12. В более старом коде используются следующие исторические имена, объявленные в <caml/signals.h>:
- caml_enter_blocking_section — псевдоним для caml_release_runtime_system
- caml_leave_blocking_section — псевдоним для caml_acquire_runtime_system
Интуитивно «блокирующая секция» — это фрагмент кода C, который не использует систему времени выполнения OCaml, обычно блокирующая операция ввода-вывода.
13 Дополнительная тема: взаимодействие с API Unicode в Windows
В этом разделе приведены общие рекомендации по написанию заглушек C, использующих API Unicode в Windows.
Систему OCaml в Windows при сборке можно настроить в одном из двух режимов:
- устаревший режим: предполагается, что все имена путей, переменные среды, аргументы командной строки и т. д. на стороне OCaml закодированы с использованием текущей 8-битной кодовой страницы системы.
- режим Unicode: предполагается, что все имена путей, переменные среды, аргументы командной строки и т. д. на стороне OCaml закодированы в UTF-8.
Далее строка называется имеющей кодировку OCaml, если в режиме Unicode она закодирована в UTF-8, в устаревшем режиме — в текущей кодовой странице, а в Unix может быть произвольной строкой. Строка называется имеющей кодировку платформы, если в Windows она закодирована в UTF-16, а в Unix может быть произвольной строкой.
С точки зрения автора заглушек C, при взаимодействии с API Unicode в Windows возникают две основные сложности:
- API Windows использует кодировку UTF-16 для поддержки Unicode. Система времени выполнения выполняет необходимые преобразования, поэтому программисту на OCaml нужно работать только с кодировкой OCaml. Заглушки C, вызывающие API Unicode в Windows, должны использовать специальные функции системы времени выполнения, чтобы выполнять необходимые преобразования совместимым образом.
- При написании заглушек, которые нужно компилировать как в Windows, так и в Unix, их необходимо реализовать так, чтобы в Windows выполнялись нужные преобразования, но чтобы они также работали в Unix, где обычно для поддержки Unicode ничего особенного делать не требуется.
В Windows собственным символьным типом C является WCHAR шириной в два байта, а в Unix — char шириной в один байт. В <caml/misc.h> определён тип char_os, обозначающий конкретный символьный тип C на каждой платформе. Строки в кодировке платформы имеют тип char_os *.
Для написания совместимых заглушек C доступны следующие функции. Чтобы использовать их, необходимо подключить оба файла: <caml/misc.h> и <caml/osdeps.h>.
-
char_os* caml_stat_strdup_to_os(const char *) копирует аргумент, преобразуя его из кодировки OCaml в кодировку платформы. Обычно эту функцию используют для преобразования типа char *, лежащего в основе строки OCaml, перед её передачей в API операционной системы, принимающий аргумент Unicode. В Unix она эквивалентна caml_stat_strdup.
Примечание: Для максимальной обратной совместимости в режиме Unicode, если аргумент не является допустимой строкой UTF-8, эта функция будет считать, что он закодирован в текущей кодовой странице.
- char* caml_stat_strdup_of_os(const char_os *) копирует аргумент, преобразуя его из кодировки платформы в кодировку OCaml. Эта функция обратна caml_stat_strdup_to_os. Обычно её используют для преобразования строки, полученной от операционной системы, перед передачей в код OCaml. В Unix она эквивалентна caml_stat_strdup.
- value caml_copy_string_of_os(char_os *) выделяет строку OCaml, содержимое которой совпадает с аргументом после преобразования в кодировку OCaml. По сути эта функция эквивалентна вызову caml_stat_strdup_of_os, за которым следует caml_copy_string, но она позволяет избежать выделения промежуточной строки, возвращаемой caml_stat_strdup_of_os. В Unix она эквивалентна caml_copy_string.
Примечание: Строки, возвращаемые функциями caml_stat_strdup_to_os и caml_stat_strdup_of_os, выделяются с помощью caml_stat_alloc, поэтому после использования их необходимо освободить с помощью caml_stat_free.
Пример
Предположим, что мы хотим связать функцию getenv так, чтобы она работала и в Unix, и в Windows. В Unix эта функция имеет прототип:
char *getenv(const char *);
Вариант для Unicode в Windows имеет следующий прототип:
WCHAR *_wgetenv(const WCHAR *);
С точки зрения типа char_os, обе функции принимают аргумент типа char_os * и возвращают результат того же типа. Сначала выберем подходящую реализацию связываемой функции:
#ifdef _WIN32 #define getenv_os _wgetenv #else #define getenv_os getenv #endif
Остальная часть связывания одинакова для обеих платформ:
#include <caml/mlvalues.h>
#include <caml/misc.h>
#include <caml/alloc.h>
#include <caml/fail.h>
#include <caml/osdeps.h>
#include <stdlib.h>
CAMLprim value stub_getenv(value var_name)
{
CAMLparam1(var_name);
CAMLlocal1(var_value);
char_os *var_name_os, *var_value_os;
var_name_os = caml_stat_strdup_to_os(String_val(var_name));
var_value_os = getenv_os(var_name_os);
caml_stat_free(var_name_os);
if (var_value_os == NULL)
caml_raise_not_found();
var_value = caml_copy_string_of_os(var_value_os);
CAMLreturn(var_value);
}
14 Сборка смешанных библиотек C/OCaml: ocamlmklib
Команда ocamlmklib упрощает создание библиотек, содержащих код OCaml и код C и пригодных как для статической, так и для динамической компоновки. Эта команда доступна в Windows начиная с Objective Caml 3.11, а в других операционных системах — начиная с Objective Caml 3.03.
Команда ocamlmklib принимает аргументы трёх видов:
- Исходные файлы и объектные файлы OCaml (.cmo, .cmx, .ml), составляющие часть библиотеки на OCaml;
- Объектные файлы C (.o, .a или .obj, .lib), составляющие часть библиотеки на C;
- Вспомогательные библиотеки для части на C (-llib).
Команда создаёт следующие файлы:
- Библиотеку байт-кода OCaml .cma, включающую переданные в аргументах файлы OCaml .cmo и .ml и автоматически ссылающуюся на библиотеку C, созданную из объектных файлов C.
- Библиотеку машинного кода OCaml .cmxa, включающую переданные в аргументах файлы OCaml .cmx и .ml и автоматически ссылающуюся на библиотеку C, созданную из объектных файлов C.
- Если целевая платформа поддерживает динамическую компоновку, общую библиотеку .so (или .dll), созданную из переданных в аргументах объектных файлов C и автоматически ссылающуюся на вспомогательные библиотеки.
- Статическую библиотеку C .a (или .lib), созданную из объектных файлов C.
Кроме того, распознаются следующие параметры:
- -cclib, -ccopt, -I, -linkall
- Эти параметры передаются без изменений команде ocamlc или ocamlopt. См. документацию к этим командам.
- -rpath, -R, -Wl,-rpath, -Wl,-R
- Эти параметры передаются без изменений компилятору C. См. документацию к компилятору C.
- -custom
- Принудительно создавать только библиотеку со статической компоновкой, даже если поддерживается динамическая компоновка.
- -failsafe
- Если при создании общей библиотеки возникнет проблема (например, некоторые вспомогательные библиотеки недоступны в виде общих библиотек), перейти к созданию библиотеки со статической компоновкой.
- -Ldir
- Добавить dir в путь поиска вспомогательных библиотек (-llib).
- -ocamlc cmd
- Использовать cmd вместо ocamlc для вызова компилятора байт-кода.
- -ocamlopt cmd
- Использовать cmd вместо ocamlopt для вызова компилятора машинного кода.
- -o output
- Задать имя создаваемой библиотеки OCaml. ocamlmklib создаст output.cma и/или output.cmxa. Если параметр не указан, по умолчанию используется имя a.
- -oc outputc
- Задать имя создаваемой библиотеки C. ocamlmklib создаст liboutputc.so (если поддерживаются общие библиотеки) и liboutputc.a. Если параметр не указан, используется имя выходного файла, заданное с помощью -o.
Пример
Рассмотрим интерфейс OCaml к стандартной библиотеке C libz для чтения и записи сжатых файлов. Предположим, что эта библиотека находится в /usr/local/zlib. Интерфейс состоит из части на OCaml zip.cmo/zip.cmx и части на C zipstubs.o, содержащей код заглушек для точек входа libz. Следующая команда создаёт библиотеки OCaml zip.cma и zip.cmxa, а также соответствующие библиотеки C dllzip.so и libzip.a:
ocamlmklib -o zip zip.cmo zip.cmx zipstubs.o -lz -L/usr/local/zlib
Если общие библиотеки поддерживаются, выполняются следующие команды:
ocamlc -a -o zip.cma zip.cmo -dllib -lzip \
-cclib -lzip -cclib -lz -ccopt -L/usr/local/zlib
ocamlopt -a -o zip.cmxa zip.cmx -cclib -lzip \
-cclib -lzip -cclib -lz -ccopt -L/usr/local/zlib
gcc -shared -o dllzip.so zipstubs.o -lz -L/usr/local/zlib
ar rc libzip.a zipstubs.o
Примечание: пример предназначен для системы Unix. В других системах точные строки команд могут отличаться.
Если общие библиотеки не поддерживаются, вместо этого выполняются следующие команды:
ocamlc -a -custom -o zip.cma zip.cmo -cclib -lzip \
-cclib -lz -ccopt -L/usr/local/zlib
ocamlopt -a -o zip.cmxa zip.cmx -lzip \
-cclib -lz -ccopt -L/usr/local/zlib
ar rc libzip.a zipstubs.o
Вместо одновременной сборки библиотеки байт-кода, библиотеки машинного кода и библиотек C команду ocamlmklib можно вызвать три раза, чтобы собрать их по отдельности. Таким образом,
ocamlmklib -o zip zip.cmo -lz -L/usr/local/zlib
собирает библиотеку байт-кода zip.cma, а
ocamlmklib -o zip zip.cmx -lz -L/usr/local/zlib
собирает библиотеку машинного кода zip.cmxa, а
ocamlmklib -o zip zipstubs.o -lz -L/usr/local/zlib
собирает библиотеки C dllzip.so и libzip.a. Обратите внимание, что вспомогательные библиотеки (-lz) и соответствующие параметры (-L/usr/local/zlib) необходимо указывать во всех трёх вызовах ocamlmklib, поскольку они нужны в разное время в зависимости от поддержки общих библиотек.
15 Предупреждение: внутренний API среды выполнения
Не все заголовочные файлы из каталога caml/ были описаны в предыдущих разделах. Все неупомянутые заголовочные файлы относятся к внутреннему API среды выполнения, стабильность которого не гарантируется. Если вам действительно необходим доступ к этому внутреннему API среды выполнения, в этом разделе приведены рекомендации, которые помогут написать код, не ломающийся с каждым новым выпуском OCaml.
Примечание
Программистам, которые используют внутренний API для реалистичного и полезного, по их мнению, сценария, рекомендуется создать запрос на улучшение в системе отслеживания ошибок.
15.1 Внутренние переменные и CAML_INTERNALS
Начиная с OCaml 4.04, получить доступ ко всем частям внутреннего API среды выполнения можно, определив макрос CAML_INTERNALS до подключения заголовочных файлов caml. Если этот макрос не определён, некоторые части внутреннего API среды выполнения скрыты.
Если вы используете внутренние переменные C, не переопределяйте их вручную. Подключайте соответствующие заголовочные файлы, чтобы импортировать эти переменные. Представление этих переменных уже изменилось один раз в OCaml 4.10 и продолжает развиваться. Если ваш код зависит от таких внутренних и нестабильных свойств, рано или поздно он перестанет работать.
Например, вместо того чтобы переопределять caml_young_limit:
extern int caml_young_limit;
что перестаёт работать в OCaml ≥ 4.10, следует подключить заголовочный файл minor_gc:
#include <caml/minor_gc.h>
15.2 Макросы версии OCaml
Наконец, если подключения нужных заголовочных файлов недостаточно или необходимо поддерживать версии OCaml старше 4.04, заголовочный файл <caml/version.h> поможет определить собственный уровень совместимости. В этом файле представлены несколько макросов, задающих текущую версию OCaml. В частности, макрос OCAML_VERSION описывает текущую версию в формате MmmPP. Например, если для версий старше 4.10.0 требуется особая обработка, можно написать:
#include <caml/version.h> #if OCAML_VERSION >= 41000 ... #else ... #endif
© 1995-2026 INRIA.
https://ocaml.org/manual/5.5/intfc.html