Spec-Zone.ru › OCaml
☰Инструменты OCaml
  • Партиционная компиляция (ocamlc)
  • Система интерпретатора или REPL (ocaml)
  • Система выполнения (ocamlrun)
  • Компиляция кода для нативной архитектуры (ocamlopt)
  • Генераторы лексических и синтаксических анализаторов (ocamllex, ocamlyacc)
  • Генератор зависимостей (ocamldep)
  • Генератор документации (ocamldoc)
  • Отладчик (ocamldebug)
  • Профилирование (ocamlprof)
  • Интерфейс C с OCaml
  • Оптимизация с помощью Flambda
  • Мультирование с afl-fuzz
  • Отслеживание выполнения с помощью событий runtime
  • Трансформация программы «Tail Modulo Constructor»
  • Обнаружение гонок данных во время выполнения с помощью ThreadSanitizer

Глава 22 Интерфейс C с OCaml

В этой главе описано, как пользовательские примитивы, написанные на C, могут быть связаны с кодом OCaml и вызываться из функций OCaml, а также как эти функции C могут вызывать код OCaml.

1 Обзор и информация о компиляции

1.1 Объявление примитивов

определение ::= ...
∣ external имя-значения : тип-выражения = объявление-внешнего
объявление-внешнего ::= строковая-литераль [ строковая-литераль [ строковая-литераль ] ]

Пользовательские примитивы объявляются в файле реализации или модуле выражения struct…end с помощью ключевого слова external:

        external name : type = C-function-name

Это определяет имя значения name как функцию с типом type, которая выполняется путем вызова заданной функции C. Например, вот как объявлен примитив seek_in в стандартном модуле библиотеки Stdlib:

        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 для примитива с 7 аргументами Nat.add_nat:

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 функции для возбуждения исключений (см. раздел ‍22.4.5)
caml/callback.h обратный вызов из C в OCaml (см. раздел ‍22.7).
caml/custom.h операции с пользовательскими блоками (см. раздел ‍22.9).
caml/intext.h операции для написания пользовательских функций сериализации и десериализации для пользовательских блоков (см. раздел ‍22.9).
caml/threads.h операции для взаимодействия в многопоточной среде (см. раздел ‍22.12).

Эти файлы находятся в подкаталоге caml/ каталога стандартной библиотеки OCaml, который возвращается командой ocamlc -where (обычно /usr/local/lib/ocaml или /usr/lib/ocaml).

1.3 Статическая компоновка кода C с кодом OCaml

Система выполнения OCaml состоит из трех основных частей: интерпретатора байткода, менеджера памяти и набора функций C, реализующих базовые операции. Некоторые инструкции байткода предназначены для вызова этих функций C, определяемых по их смещению в таблице функций (таблица примитивов).

В режиме по умолчанию компоновщик OCaml генерирует байткод для стандартной системы выполнения с стандартным набором примитивов. Ссылки на примитивы, которые не входят в этот стандартный набор, приводят к ошибке «недоступный C-примитив». (Если не поддерживается динамическая загрузка C-библиотек — см. раздел ‍22.1.4 ниже.)

В режиме «пользовательской системы выполнения» компоновщик OCaml сканирует файлы объектов и определяет набор необходимых примитивов. Затем он создает подходящую систему выполнения, вызвав компоновщик кода нативного уровня с:

  • таблицей необходимых примитивов;
  • библиотекой, предоставляющей интерпретатор байткода, менеджер памяти и стандартные примитивы;
  • библиотеками и файлами объектного кода (.o файлы), указанными в командной строке для компоновщика OCaml, которые предоставляют реализации примитивов пользователя.

Это создает систему выполнения с необходимыми примитивами. Компоновщик OCaml генерирует байткод для этой пользовательской системы выполнения. Байткод добавляется в конец пользовательской системы выполнения, так что он будет автоматически выполняться при запуске выходного файла (пользовательская система выполнения + байткод).

Для компоновки в режиме «пользовательской системы выполнения» выполните команду ocamlc с:

  • параметром -custom;
  • именами необходимых файлов OCaml-объектов (.cmo и .cma файлы);
  • именами файлов C-объектов и библиотек (.o и .a файлы), которые реализуют необходимые примитивы. В Unix и Windows библиотека с именем libимя.a (соответственно .lib) в одном из стандартных каталогов библиотек также может быть указана как -cclib -lимя.

Если вы используете компилятор нативного кода ocamlopt, флаг -custom не нужен, так как заключительная фаза компоновки ocamlopt всегда создает автономный исполняемый файл. Чтобы создать смешанный OCaml/C исполняемый файл, выполните команду ocamlopt с:

  • именами необходимых OCaml файлов нативного объекта (.cmx и .cmxa файлы);
  • именами файлов C-объектов и библиотек (.o, .a, .so или .dll файлы), которые реализуют необходимые примитивы.

Начиная с Objective Caml 3.00, можно записать параметр -custom, а также имена C-библиотек в файл OCaml-библиотеки .cma или .cmxa. Например, рассмотрим 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, кроме Cygwin 64 бит.

Для динамической компоновки кода C с кодом OCaml код C сначала необходимо скомпилировать в динамическую библиотеку (в Unix) или DLL (в Windows). Это включает 1 — компиляцию файлов C с соответствующими флагами компилятора C для создания кода независимого от позиции (когда это требуется операционной системой) и 2 — создание динамической библиотеки из полученных файлов объектного кода. Полученный файл динамической библиотеки или DLL должен быть установлен в месте, где ocamlrun сможет его найти во время запуска программы (см. раздел ‍15.3). Наконец (шаг 3), выполните команду ocamlc с:

  • именами необходимых файлов OCaml-объектов (.cmo и .cma файлы);
  • именами C-динамических библиотек (.so или .dll файлы), которые реализуют необходимые примитивы. В Unix и Windows библиотека с именем dllимя.so (соответственно, .dll) в одном из стандартных каталогов библиотек также может быть указана как -dllib -lимя.

Не устанавливайте флаг -custom, в противном случае вы вернетесь к статической компоновке, как описано в разделе ‍22.1.3. Инструмент ocamlmklib (см. раздел ‍22.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, или о том, должен ли этот код 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 (см. раздел ‍22.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_* функций, описанных в разделе ‍22.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. Раздел ‍22.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, содержимое которых состоит из необработанных значений с плавающей точкой, которые сами по себе не являются допустимыми значениями OCaml. К ним следует обращаться с помощью макросов Double_flat_field и Store_double_flat_field.

Наконец, массивы типа float array могут использовать либо упакованное, либо неупакованное представление в зависимости от конфигурации компилятора. В настоящее время они по умолчанию используют неупакованное представление, но могут быть переведены в упакованное представление, передав флаг --disable-flat-float-array в скрипт «configure». К ним следует обращаться с помощью макросов 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 *)

В целях оптимизации, распаковываемые конкретные типы данных представляются специальным образом; конкретный тип данных допускает распаковку, если он имеет ровно один конструктор, а этот конструктор имеет ровно один аргумент. Распаковываемые конкретные типы данных представляются так же, как и распаковываемые типы записей: см. описание в разделе ‍22.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, hash_variant("bar")), foo);

3.6 Полиморфные варианты

Как и конструируемые термины, значения полиморфных вариантов представляются либо как целые числа (для полиморфных вариантов без аргумента), либо как блоки (для полиморфных вариантов с аргументом). В отличие от конструируемых терминов, конструкторы вариантов не нумеруются, начиная с 0, а идентифицируются по хеш-значению (целому числу OCaml), вычисленному функцией C hash_variant (объявленной в <caml/mlvalues.h>): хеш-значение для конструктора варианта, например, VConstr, равно hash_variant("VConstr").

Значение варианта `VConstr представлено значением hash_variant("VConstr"). Значение варианта `VConstr(v) представлено блоком размера 2 и тегом 0, с полем номер 0, содержащим 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 (предполагается, что оно имеет тип опции) соответствует конструктору Some.

4.2 Операции с целыми числами

  • Val_long(l) возвращает кодированное значение целочисленной переменной long int l.
  • Long_val(v) возвращает значение long int, закодированное в значении v.
  • Val_int(i) возвращает значение, кодирующее целое число int i.
  • Int_val(v) возвращает целое число int, закодированное в значении v.
  • 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_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 над строками.
  • 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) возвращает аргумент \var{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 получает каждый указатель в качестве аргумента и возвращает значение. Блок с тегом ноль, возвращаемый alloc_array(f, a), заполняется значениями, возвращаемыми последовательными вызовами f. (Эта функция не должна использоваться для построения массива чисел с плавающей запятой.)
  • caml_copy_string_array(p) выделяет массив строк или последовательностей байтов, скопированных из указателя на массив строк p (тип char **). p должен быть завершен нулём.
  • 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 объявлена в файле заголовков 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 с помощью средства регистрации, описанного ниже в разделе ‍22.7.3. После того, как идентификатор исключения будет получен в 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 в качестве аргумента.

5 Совместимость с сборщиком мусора

Неиспользуемые блоки в куче автоматически освобождаются сборщиком мусора. Для этого требуется некоторое взаимодействие с кодом C, который манипулирует блоками, выделенными в куче.

5.1 Простой интерфейс

Все макросы, описанные в этом разделе, объявлены в заголовочном файле memory.h.

Правило ‍1 Функция, имеющая параметры или локальные переменные типа value, должна начинаться с вызова одного из макросов CAMLparam и завершаться CAMLreturn, CAMLreturn0 или CAMLreturnT.

Существует шесть макросов CAMLparam: от CAMLparam0 до CAMLparam5, которые принимают от нуля до пяти аргументов соответственно. Если ваша функция имеет не более 5 параметров типа value, используйте соответствующие макросы с этими параметрами в качестве аргументов. Если ваша функция имеет более 5 параметров типа value, используйте CAMLparam5 для пяти из них и используйте один или несколько вызовов макросов CAMLxparam для оставшихся параметров (CAMLxparam1 до CAMLxparam5).

Макросы 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 выдают ложные предупреждения об unused переменных 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. CAMLreturn(Val_unit) следует использовать для функций, возвращающих OCaml-значение unit. Примитивы (функции C, которые могут вызываться из OCaml) никогда не должны возвращать void.

Правило ‍2 Локальные переменные типа value должны объявляться с использованием макросов CAMLlocal. Массивы value объявляются с помощью CAMLlocalN. Эти макросы должны использоваться в начале функции, а не в вложенном блоке.

Макросы 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);
}
Правило ‍3 Присваивания полям структурированных блоков должны выполняться с помощью макроса Store_field (для обычных блоков), макроса Store_double_array_field (для значений float array) или Store_double_flat_field (для значений floatarray и записей чисел с плавающей точкой). Другие присваивания не должны использовать Store_field, Store_double_array_field и Store_double_flat_field.

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.

Правило 4 Глобальные переменные, содержащие значения, должны быть зарегистрированы в сборщике мусора с помощью функции caml_register_global_root, за исключением случаев, когда глобальные переменные и места, которые будут содержать только целые числа OCaml (и никогда не указатели), не требуют регистрации.

То же самое относится к любой области памяти вне кучи OCaml, которая содержит значение и не гарантируется, что она будет достижима — пока она содержит такое значение — ни из другой зарегистрированной глобальной переменной или области, локальной переменной, объявленной с помощью CAMLlocal, или параметра функции, объявленного с помощью CAMLparam.

Регистрация глобальной переменной v достигается путем вызова caml_register_global_root(&v) непосредственно перед или непосредственно после того, как в v впервые будет сохранено допустимое значение; аналогично, регистрация произвольной области p достигается путем вызова caml_register_global_root(p).

Вы не должны вызывать ни одну из функций или макросов среды выполнения OCaml между регистрацией и сохранением значения. Также вы не должны сохранять в переменной v (аналогично, в области p) ничего, что не является допустимым значением.

Регистрация заставляет содержимое переменной или области памяти обновляться сборщиком мусора всякий раз, когда значение в такой переменной или области перемещается внутри кучи OCaml. При наличии потоков необходимо позаботиться о соответствующей синхронизации со средой выполнения OCaml, чтобы избежать гонки с сборщиком мусора при чтении или записи значения. (См. раздел 22.12.2.)

Зарегистрированную глобальную переменную v можно аннулировать, вызвав caml_remove_global_root(&v).

Если содержимое глобальной переменной v редко изменяется после регистрации, лучшая производительность может быть достигнута путем вызова caml_register_generational_global_root(&v) для регистрации v (после её инициализации с допустимым значением value, но до выделения памяти или вызова функций GC) и caml_remove_generational_global_root(&v) для аннулирования регистрации. В этом случае вы не должны изменять значение v напрямую, но вы должны использовать caml_modify_generational_global_root(&v,x), чтобы установить его в x. Сборщик мусора использует гарантию, что v не изменяется между вызовами caml_modify_generational_global_root, для того чтобы сканировать его реже. Это улучшает производительность, если изменения v происходят реже, чем малые сборки.

Примечание:

Макросы CAML используют идентификаторы (локальные переменные, идентификаторы типов, теги структур), которые начинаются с caml__. Не используйте ни один идентификатор, начинающийся с caml__ в ваших программах.

5.2 Интерфейс низкого уровня

Теперь мы приводим правила GC, соответствующие функциям выделения низкого уровня caml_alloc_small и caml_alloc_shr. Вы можете проигнорировать эти правила, если придерживаетесь упрощённой функции выделения caml_alloc.

Правило 5 После выделения структурированного блока (блока с тегом меньше No_scan_tag) с помощью функций низкого уровня, все поля этого блока должны быть заполнены корректными значениями до следующей операции выделения. Если блок был выделен с помощью caml_alloc_small, заполнение выполняется путём прямого присваивания полям блока:
        Field(v, n) = vn;
Если блок был выделен с помощью caml_alloc_shr, заполнение выполняется через функцию caml_initialize:
        caml_initialize(&Field(v, n), vn);

Следующее выделение может вызвать сборку мусора. Сборщик мусора предполагает, что все структурированные блоки содержат корректные значения. Недавно созданные блоки содержат случайные данные, которые, как правило, не представляют собой корректные значения.

Если вам действительно нужно выделить память до того, как поля получат своё окончательное значение, сначала инициализируйте её постоянным значением (например, Val_unit), затем выделите память, а затем измените поля на корректное значение (см. правило 6).

Правило 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.

Вот та же функция, написанная с использованием функций выделения низкого уровня. Заметим, что ячейки cons являются небольшими блоками и могут быть выделены с помощью 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-код, включая finalizers, обработчики сигналов и другие потоки, работающие в той же области. Вместо этого их выполнение откладывается до более позднего безопасного момента.

Функция caml_process_pending_actions из <caml/signals.h> выполняет все ожидающие обработчики сигналов и finalizers, вызовы Memprof, прерывистые переключения systhread и запрошенные сборки мусора малого и большого масштаба. В частности, она может генерировать асинхронные исключения и вызывать изменения в куче OCaml из той же области. Рекомендуется вызывать её регулярно в безопасные моменты внутри длительных неблокирующих C-кодов.

Предоставлен вариант caml_process_pending_actions_exn, который возвращает исключение вместо непосредственного его поднятия в OCaml-код. Его результат должен быть проверен с помощью Is_exception_result, и за ним должно следовать Extract_exception, если это необходимо. Обычно он используется для очистки перед повторным поднятием:

    CAMLlocal1(exn);
    ...
    exn = caml_process_pending_actions_exn();
    if(Is_exception_result(exn)) {
      exn = Extract_exception(exn);
      ...cleanup...
      caml_raise(exn);
    }

Правильное использование исключительного возврата, особенно при наличии сборки мусора, подробно описано в разделе ‍22.7.1.

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

(При передаче файла .c команда ocamlc просто вызывает компилятор 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 -ltermcap или -cclib -ltermcap вместо -cclib -lcurses.)

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 с двумя аргументами, в curried стиле) к a и b.
  • caml_callback3(f, a, b, c) применяет функциональное значение f (функция OCaml с тремя аргументами, в curried стиле) к a, b и c.
  • caml_callbackN(f, n, args) применяет функциональное значение f к n аргументам, содержащимся в массиве значений C args.

Если функция f не возвращается, а вызывает исключение, которое выходит за пределы области применения, то это исключение передаётся следующему окружающему OCaml-коду, минуя код C. То есть, если функция OCaml f вызывает функцию C g, которая вызывает функцию OCaml h, которая вызывает исключение, то выполнение g прерывается, и исключение передаётся обратно в f.

Если код C хочет перехватить исключения, выходящие за пределы функции OCaml, он может использовать функции caml_callback_exn, caml_callback2_exn, caml_callback3_exn, caml_callbackN_exn. Эти функции принимают те же аргументы, что и их аналоги без _exn, но перехватывают выходящие исключения и возвращают их в код C. Возвращаемое значение v функций caml_callback*_exn должно быть проверено с помощью макроса Is_exception_result(v). Если макрос возвращает «false», исключение не возникло, и v — это значение, возвращённое функцией OCaml. Если Is_exception_result(v) возвращает «true», исключение вышло за пределы, и его значение (описание исключения) можно восстановить с помощью Extract_exception(v).

Предупреждение:

Если функция OCaml вернулась с исключением, Extract_exception должна быть применена к результату исключения до вызова функции, которая может вызвать сборку мусора. В противном случае, если v доступно во время сбора мусора, среда выполнения может аварийно завершиться, так как v не содержит действительное значение.

Пример:

    CAMLprim value call_caml_f_ex(value closure, value arg)
    {
      CAMLparam2(closure, arg);
      CAMLlocal2(res, tmp);
      res = caml_callback_exn(closure, arg);
      if(Is_exception_result(res)) {
        res = Extract_exception(res);
        tmp = caml_alloc(3, 0); /* Safe to allocate: res contains valid value. */
        ...
      }
      CAMLreturn (res);
    }

7.2 Получение или регистрация замыканий 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.3 Регистрация исключений 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 (описанным в разделе ‍22.4.5), чтобы фактически вызвать исключение. Например, вот функция C, которая вызывает исключение Error с заданным аргументом:

    void raise_error(char * msg)
    {
        caml_raise_with_string(*caml_named_value("test exception"), msg);
    }

7.4 Главная программа на C

В обычном режиме работы смешанная программа OCaml/C начинает выполнение с кода инициализации OCaml, который затем может продолжить вызывать функции C. Мы говорим, что главной программой является код OCaml. В некоторых приложениях желательно, чтобы код C играл роль главной программы, вызывая функции OCaml при необходимости. Это можно сделать следующим образом:

  • Часть программы на C должна предоставить функцию main, которая переопределит стандартную функцию main, предоставляемую системой выполнения OCaml. Выполнение начнется в определённой пользователем функции main, точно так же, как и в обычной C-программе.
  • В какой-то момент код C должен вызвать caml_main(argv), чтобы инициализировать код OCaml. Аргумент argv — это массив строк C (тип char **), завершающийся нулевым указателем, который представляет аргументы командной строки, как переданные вторым аргументом функции main. Массив OCaml Sys.argv будет инициализирован из этого параметра. Для компилятора байткода argv[0] и argv[1] также используются для поиска файла, содержащего байткод.
  • Вызов caml_main инициализирует систему выполнения OCaml, загружает байткод (в случае компилятора байткода) и выполняет код инициализации программы OCaml. Как правило, этот код инициализации регистрирует функции обратного вызова с помощью Callback.register. После завершения кода инициализации OCaml управление возвращается коду C, который вызвал caml_main.
  • Затем код C может вызвать функции OCaml, используя механизм обратного вызова (см. раздел ‍22.7.1).

7.5 Встраивание кода OCaml в код C

Компилятор байткода в режиме пользовательского времени выполнения (ocamlc -custom) обычно добавляет байткод к исполняемому файлу, содержащему пользовательское время выполнения. Это имеет два последствия. Во-первых, завершающая стадия компоновки должна выполняться с помощью ocamlc. Во-вторых, библиотека времени выполнения OCaml должна иметь возможность найти имя исполняемого файла из аргументов командной строки. При использовании caml_main(argv), как в разделе ‍22.7.4, это означает, что argv[0] или argv[1] должны содержать имя исполняемого файла.

Альтернативой является встраивание байткода в код C. Для этой цели предоставляются опции -output-obj и -output-complete-obj для ocamlc. Они заставляют компилятор ocamlc выводить файл объекта C (.o файл, .obj в Windows) содержащий байткод для части программы OCaml, а также функцию caml_startup. Файл объекта C, созданный ocamlc -output-complete-obj, также содержит библиотеки времени выполнения и автосвязи. Файл объекта C, созданный ocamlc -output-obj или ocamlc -output-complete-obj, затем может быть связан с кодом C с помощью стандартного компилятора C или сохранён в библиотеке C.

Функция caml_startup должна вызываться из основной программы C для инициализации времени выполнения OCaml и выполнения кода инициализации OCaml. Как и 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 файл, .dll в Windows), которая содержит код 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.
  • другие системы: вам может потребоваться добавить одну или обе из -lm и -ldl, в зависимости от вашей ОС и компилятора C.
Стек отслеживания ошибок.

Когда байткод OCaml, сгенерированный ocamlc -g, встроен в программу C, информация об отладке не включена, и поэтому невозможно вывести стек отслеживания ошибок при необработанных исключениях. Это не так, когда машинный код, созданный ocamlopt -g, встроен в программу C: информация о стеке отслеживания ошибок доступна, но механизм отслеживания ошибок должен быть включён программно. Это можно сделать со стороны OCaml, вызвав Printexc.record_backtrace true в инициализации одного из модулей OCaml. Это также можно сделать со стороны C, вызвав caml_record_backtraces(1); в коде склеивания OCaml-C. (caml_record_backtraces объявлен в backtrace.h)

Выгрузка времени выполнения.

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

Начиная с версии 4.05, можно использовать функцию caml_shutdown для корректной остановки времени выполнения, что эквивалентно следующему:

  • Выполнение функций, зарегистрированных с помощью Stdlib.at_exit.
  • Вызов завершения выделенных пользовательских блоков (см. раздел ‍22.9). Например, Stdlib.in_channel и Stdlib.out_channel представляют собой пользовательские блоки, содержащие дескрипторы файлов, которые должны быть освобождены.
  • Выгрузка зависимых общих библиотек, загруженных средой выполнения, включая плагины dynlink.
  • Освобождение блоков памяти, выделенных средой выполнения с помощью malloc. Внутри C-примитивов рекомендуется использовать функции caml_stat_* из memory.h для управления статическими (то есть, неподвижными) блоками кучи памяти, так как все блоки, выделенные с помощью этих функций, автоматически освобождаются функцией caml_shutdown. Для обеспечения совместимости со старыми C-заглушками, которые неправильно использовали caml_stat_*, это поведение включено только в том случае, если среда выполнения запускается со специализированной функцией caml_startup_pooled.

Поскольку общая библиотека может иметь несколько клиентов одновременно, для удобства caml_startup (и caml_startup_pooled) могут вызываться многократно, при условии, что каждый такой вызов связан с соответствующим вызовом caml_shutdown (вложенным образом). Среда выполнения будет выгружена, как только не останется открытых вызовов caml_startup.

После выгрузки среды выполнения её нельзя запустить снова без перезагрузки общей библиотеки и повторной инициализации её статических данных. Поэтому в настоящее время эта возможность полезна только для создания перезагружаемых общих библиотек.

Обработка сигналов Unix.

В зависимости от целевой платформы и операционной системы, система времени выполнения нативного кода может установить обработчики сигналов для одного или нескольких сигналов SIGSEGV, SIGTRAP и SIGFPE при вызове caml_startup и сбросить эти сигналы до их стандартного поведения при вызове caml_shutdown. Основная программа на языке C не должна пытаться обрабатывать эти сигналы самостоятельно.

8 Расширенный пример с обратными вызовами

Этот раздел иллюстрирует возможности обратных вызовов, описанные в разделе ‍22.7. Мы собираемся упаковать некоторые функции OCaml таким образом, чтобы их можно было связать с C-кодом и вызывать из C, как любые C-функции. Функции OCaml определены в следующей исходной файле OCaml mod.ml:

(* File mod.ml -- some "useful" OCaml functions *)

let rec fib n = if n < 2 then 1 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

(Также можно использовать ocamlopt -output-obj вместо ocamlc -custom -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

(На некоторых машинах, возможно, потребуется указать -ltermcap или -lcurses -ltermcap вместо -lcurses.)

9 Расширенная тема: пользовательские блоки

Блоки с тегом Custom_tag содержат произвольные пользовательские данные и указатель на C-структуру типа struct custom_operations, которая связывает предоставленные пользователем функции завершения, сравнения, хэширования, сериализации и десериализации с этим блоком.

9.1 Структура struct custom_operations

Структура struct custom_operations определена в <caml/custom.h> и содержит следующие поля:

  • char *identifier
    Нуль-терминированная строка символов, используемая в качестве идентификатора для операций сериализации и десериализации.
  • void (*finalize)(value v)
    Поле finalize содержит указатель на функцию C, которая вызывается, когда блок становится недоступным и должен быть освобожден. Блок передается в качестве первого аргумента функции. Поле finalize также может быть равно custom_finalize_default, чтобы указать, что для блока не задана функция завершения.
  • 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> и перечисленные в разделе ‍22.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> и перечисленные в разделе ‍22.9.4. Затем она должна восстановить часть данных пользовательского блока и сохранить её по указателю, переданному в качестве аргумента dst. Наконец, она возвращает размер в байтах части данных пользовательского блока. Этот размер должен совпадать с результатом wsize_32 операции serialize для 32-битной архитектуры или wsize_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. Не используйте CAMLparam для регистрации параметров этих функций и не используйте CAMLreturn для возврата результата. Не вызывайте исключения (для сигнализации об ошибке во время десериализации используйте caml_deserialize_error). Не удаляйте глобальные корни. При необходимости, действуйте осмотрительно. В функциях serialize и deserialize разрешено (и даже рекомендуется) использовать соответствующие функции из раздела ‍22.9.4.

9.2 Выделение пользовательских блоков

Пользовательские блоки должны быть выделены с помощью caml_alloc_custom или caml_alloc_custom_mem:

caml_alloc_custom(ops, size, used, max)

возвращает новый пользовательский блок с местом для 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.

caml_alloc_custom_mem(ops, size, used)

Используйте эту функцию, когда ваш пользовательский блок содержит только память вне кучи (память, выделенная с помощью 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>, предназначены для записи и чтения содержимого пользовательских блоков портативным способом. Эти функции обрабатывают преобразования порядка байтов, например, когда данные записываются на машине с порядком байтов little-endian и читаются на машине с порядком байтов big-endian.

Функция Действие
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 Дополнительная тема: Bigarrays и интерфейс OCaml-C

Этот раздел объясняет, как код C-заглушки, который связывает код C или Fortran с кодом OCaml, может использовать Bigarrays.

10.1 Файл включения

Файл включения <caml/bigarray.h> должен быть включен в файл C-заглушки. Он объявляет функции, константы и макросы, обсуждаемые ниже.

10.2 Доступ к OCaml bigarray из C или Fortran

Если v — это значение OCaml value, представляющее Bigarray, выражение Caml_ba_data_val(v) возвращает указатель на часть данных массива. Этот указатель имеет тип void * и может быть приведён к соответствующему типу C для массива (например, double [], char [][10] и т. д.).

Различные характеристики OCaml Bigarray могут быть проконсультированы из C следующим образом:

Выражение 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_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 в OCaml Bigarray

Указатель p на уже выделенный массив C или Fortran может быть обернут и возвращен в OCaml в виде Bigarray с помощью функций caml_ba_alloc или caml_ba_alloc_dims.

  • caml_ba_alloc(kind | layout, numdims, p, dims)

    Возвращает OCaml Bigarray, оборачивающий данные, на которые указывает 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 между OCaml и C. Это делается путем аннотации аргументов и/или результата атрибутом [@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 функцией C caml_c_call, которая является частью среды выполнения OCaml.

Для небольших функций, которые вызываются многократно, эта косвенность может сильно повлиять на производительность. Однако это не требуется, если известно, что функция C не выделяет память, не вызывает исключения и не освобождает блокировку домена (см. раздел ‍22.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_unregister() должна вызываться перед завершением потока, чтобы дезактивировать его в системе выполнения OCaml. Возвращает 1 при успехе, 0 при ошибке. Если вызывающий поток не был ранее зарегистрирован, ничего не делает и возвращает 0.

12.2 Параллельное выполнение длительных функций C с systhreads

Домены — это единицы параллелизма для программ OCaml. При использовании библиотеки systhreads несколько потоков могут быть привязаны к одному домену. Однако в любой момент времени не более одного из этих потоков может выполнять код OCaml или код C, использующий систему выполнения OCaml, в данном домене. Технически это обеспечивается «блокировкой домена», которую любой поток должен удерживать, выполняя такой код в пределах домена.

Когда OCaml вызывает код C, реализующий примитив, блокировка домена удерживается, поэтому код C имеет полный доступ к возможностям системы выполнения. Однако ни один другой поток в том же домене не может выполнить код OCaml одновременно с кодом C примитива. См. также главу ‍9.6 для поведения с несколькими доменами.

Если примитив C работает долго или выполняет потенциально блокирующие операции ввода-вывода, он может явно освободить блокировку домена, что позволит другим потокам OCaml в том же домене выполнять операции параллельно с его операциями. Код C должен повторно получить блокировку домена перед возвратом в OCaml. Это достигается с помощью следующих функций, объявленных в файле заголовков <caml/threads.h>.

  • caml_release_runtime_system() Вызывающий поток освобождает блокировку домена и другие ресурсы OCaml, позволяя другим потокам запускать код OCaml параллельно с выполнением вызывающего потока.
  • caml_acquire_runtime_system() Вызывающий поток повторно получает блокировку домена и другие ресурсы OCaml. Он может заблокироваться, пока ни один другой поток в том же домене не использует систему выполнения OCaml.

Эти функции проверяют наличие ожидающих сигналов, вызывая асинхронные обратные вызовы (раздел ‍22.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 вызывает gethostbyname для поиска IP-адреса имени хоста. Функция gethostbyname может блокироваться на длительное время, поэтому мы выбираем освободить систему выполнения OCaml во время ее выполнения.

CAMLprim stub_gethostbyname(value vname)
{
  CAMLparam1 (vname);
  CAMLlocal1 (vres);
  struct hostent * h;
  char * name;

  /* Copy the string argument to a C string, allocated outside the
     OCaml heap. */
  name = caml_stat_strdup(String_val(vname));
  /* Release the OCaml run-time system */
  caml_release_runtime_system();
  /* Resolve the name */
  h = gethostbyname(name);
  /* Free the copy of the string, which we might as well do before
     acquiring the runtime system to benefit from parallelism. */
  caml_stat_free(name);
  /* Re-acquire the OCaml run-time system */
  caml_acquire_runtime_system();
  /* Encode the relevant fields of h as the OCaml value vres */
  ... /* Omitted */
  /* Return to OCaml */
  CAMLreturn (vres);
}

Макрос 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 Windows Unicode

В этом разделе содержатся некоторые общие рекомендации по написанию C-заглушек, использующих API Windows Unicode.

Система OCaml под Windows может быть настроена во время сборки в одном из двух режимов:

  • Режим совместимости: Предполагается, что все имена путей, переменные среды, аргументы командной строки и т. д. со стороны OCaml закодированы с использованием текущей 8-битной кодовой страницы системы.
  • Режим Unicode: Предполагается, что все имена путей, переменные среды, аргументы командной строки и т. д. со стороны OCaml закодированы с использованием UTF-8.

Далее мы говорим, что строка имеет кодировку OCaml, если она закодирована в UTF-8 в режиме Unicode, в текущей кодовой странице в режиме совместимости или является произвольной строкой под Unix. Строка имеет платформенную кодировку, если она закодирована в UTF-16 под Windows или является произвольной строкой под Unix.

С точки зрения автора C-заглушек, проблемы взаимодействия с API Windows Unicode двояки:

  • API Windows использует кодировку UTF-16 для поддержки Unicode. Система выполнения выполняет необходимые преобразования, чтобы программисту OCaml нужно было иметь дело только с кодировкой OCaml. C-заглушки, которые вызывают API Windows Unicode, должны использовать определенные функции системы выполнения для выполнения необходимых преобразований совместимым способом.
  • При написании заглушек, которые должны компилироваться как под Windows, так и под Unix, заглушки должны быть написаны таким образом, чтобы они позволяли выполнять необходимые преобразования под Windows, но также работали под Unix, где обычно ничего особенного не нужно для поддержки Unicode.

Нативный тип символов C под Windows — WCHAR, двухбайтовый, а под Unix — char, однобайтовый. Тип char_os определён в <caml/misc.h> и обозначает конкретный тип символов 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
« Профилирование (ocamlprof)Оптимизация с Flambda »
Авторские права © 2024 Institut National de Recherche en Informatique et en Automatique

© 1995-2024 INRIA.
https://ocaml.org/manual/5.2/intfc.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API