NB: читая этот топик, желательно не есть — можно поперхнуться от неожиданности.
NB: менее ценные куски кода пришлось вынести на пастебин, из-за того что хабр обрезает пост. Следите за ссылками в тексте.
Многие причисляют OCaml к маргинальным и даже эзотерическим языкам. Возможно они и правы, хотя множество людей с ними не согласны. Для меня знакомство с ним началось с полгода назад, когда мне в очередной раз захотелось научиться чему-то новому и я подумал, что хоть один функциональный язык надо освоить. Из множества языков я выбрал Objective Caml. Язык покорил меня человеческим синтаксисом и идеей: есть все функциональные радости жизни, но если хочешь императивный стиль и ООП — бери, их есть у меня! Оказалось, что разработчики прекрасно понимали, что для разных задач нужны разные средства. Три дня чтения мануала для C++ и Perl-программистов и я уже вполне мог читать код и писать хеллоуворлды. На этом моё знакомство с языком закончилось, потому что изучать язык не на реальной задаче — дело глупое.
Вернулся я к OCaml с пару недель назад, когда возникла одна интересная задача. Я посмотрел на неё и понял, что в принципе её можно целиком решить на PHP, или написать, как я люблю, расширение PHP на C++, но функциональный язык подошёл бы гораздо больше. Итак, у нас имеется основная программа на PHP, из которой мы хотим вызывать функции OCaml, получая от этого очевидный профит и незамутненную радость идиота и Мичурина в одном флаконе. В данной статье я приведу лишь простой код для демонстрации принципа — без хитрых оптимизаций и всего такого, что сделает наш код намного быстрее, но ухудшит его читаемость. Кроме того, я постарался максимально разделить обертки для PHP и OCaml, поэтому конвертация данных идёт не напрямую PHP←→OCaml, а по пути PHP←→C←→OCaml, что дополнительно снижает скорость. Зато так намного понятнее будет принцип, а с оптимизацией, надеюсь, справятся все желающие.
Задачу, которую мы решаем, определим следующим образом: необходим принять от PHP массив структур, отфильтровать его некоторым образом, например, по какому-то значению, а затем оставшийся массив вернуть.
Приступим. Для начала напишем нашу OCaml-часть, которая будет выполнять всю работу. Определим в файле ocamlpart.ml типы данных, с которыми будем работать. Пусть это будут некоторые химические группы:
Обратите внимание, у нас есть аналог структуры C (group), аналоги enum (group_position, cycle_type) и гибридный тип, который может быть как enum'ом, так и произвольным числом (group_type). Теперь добавим функцию для фильтрации:
Данная функция будет фильтровать список групп по значению поля grouptype — по сути это аналог того, что в PHP называется функцией array_filter.
Наконец, добавим регистрацию функции-фильтра для использования в C:
Этот блок выполнится при инициализации OCaml-части из C, предоставив нам замечательный именной callback.
Теперь нам надо для этого написать обертку.
Первым делом мы подготовим общий cpart.h файл, в котором будут общие части для OCaml- и PHP-обёрток:
Мы определили соответствия типов из OCaml и объявили две функции — для инициализации OCaml и основную действующую функцию-обертку, которая будет фильтровать массив данных.
В действительности у нас будет больше функций, но только эти мы планируем в будущем показывать для PHP. Теперь напишем файл ccamlpart.cc с оберткой для Ocaml. Сперва заголовки:
Поскольку мы задействуем C++ с его вектором, и код нам придётся компилировать как плюсовый, то не забываем об extern для стандартных функций OCaml. Теперь определим вспомогательные функции для конверсии типов между OCaml и C (тут).
Здесь нам потребовалась специальная функция для конверсии гибридного OCaml-типа group_type — его enum-значения хранятся как простые целые числа, а вот NeighbourGroup является уже структурой с одним полем.
Еще стоит посмотреть на три макроса: CAMLparamX, CAMLlocalX и CAMLreturn*. Первый из них используется для корректной работы сборщика мусора, и принимает на вход все OCaml-переменные, переданные в функцию. Второй используется для объявления локальных OCaml-переменных. А третий макрос опять-таки необходим не только для возврата значения, но и корректной работы сборщика мусора.
И, наконец, наши действующие функции для PHP:
Незатейливо из вектора конструируем OCaml-лист: он устроен буквально по всем канонам — структура, у которой первое поле является элементом, а второе — указателем на продолжение списка. Потом скармливаем этот список OCaml и результат превращаем обратно в вектор.
Соберем всё это воедино в статическую библиотеку мэйкфайлом:
Теперь, наконец, напишем PHP-расширение. Для этого создадим в папке с проектом отдельную папку php — расширение создаст свой Makefile, затирающий тот, который мы использовали выше, поэтому мы отделяем одно от другого. Создадим в ней файл cphppart.h. В файле мы объявили стандартные функции модуля, класс Chemlib и глобальную функцию get_group_of_caml. Мы опять вынуждены использовать std::vector, поэтому не забываем про extern для PHP-инклюдов. Теперь в файле cphppart.cc реализуем непосредственно расширение. Для начала стандартная информация о расширении, классе и его функциях (смотреть тут). Затем функция инициализации модуля — именно в ней класс регистрируется в PHP, а также получает помимо методов еще и свойства:
Как видно, мы задали классу все те же свойства, что и в C для структуры group, кроме того, объявили удобные константы типа Chemlib::OrgGroup. Добавим дефолтных функций, функционалом которые каждый может наполнить по вкусу (тут). Приправим вспомогательными внутренними функциями для конверсии группы между C и PHP:
NB: менее ценные куски кода пришлось вынести на пастебин, из-за того что хабр обрезает пост. Следите за ссылками в тексте.
Многие причисляют OCaml к маргинальным и даже эзотерическим языкам. Возможно они и правы, хотя множество людей с ними не согласны. Для меня знакомство с ним началось с полгода назад, когда мне в очередной раз захотелось научиться чему-то новому и я подумал, что хоть один функциональный язык надо освоить. Из множества языков я выбрал Objective Caml. Язык покорил меня человеческим синтаксисом и идеей: есть все функциональные радости жизни, но если хочешь императивный стиль и ООП — бери, их есть у меня! Оказалось, что разработчики прекрасно понимали, что для разных задач нужны разные средства. Три дня чтения мануала для C++ и Perl-программистов и я уже вполне мог читать код и писать хеллоуворлды. На этом моё знакомство с языком закончилось, потому что изучать язык не на реальной задаче — дело глупое.
Вернулся я к OCaml с пару недель назад, когда возникла одна интересная задача. Я посмотрел на неё и понял, что в принципе её можно целиком решить на PHP, или написать, как я люблю, расширение PHP на C++, но функциональный язык подошёл бы гораздо больше. Итак, у нас имеется основная программа на PHP, из которой мы хотим вызывать функции OCaml, получая от этого очевидный профит и незамутненную радость идиота и Мичурина в одном флаконе. В данной статье я приведу лишь простой код для демонстрации принципа — без хитрых оптимизаций и всего такого, что сделает наш код намного быстрее, но ухудшит его читаемость. Кроме того, я постарался максимально разделить обертки для PHP и OCaml, поэтому конвертация данных идёт не напрямую PHP←→OCaml, а по пути PHP←→C←→OCaml, что дополнительно снижает скорость. Зато так намного понятнее будет принцип, а с оптимизацией, надеюсь, справятся все желающие.
Задачу, которую мы решаем, определим следующим образом: необходим принять от PHP массив структур, отфильтровать его некоторым образом, например, по какому-то значению, а затем оставшийся массив вернуть.
OCaml
Приступим. Для начала напишем нашу OCaml-часть, которая будет выполнять всю работу. Определим в файле ocamlpart.ml типы данных, с которыми будем работать. Пусть это будут некоторые химические группы:
- type group_position = UndefPos | LeftPos | RightPos;;
- type cycle_type = UndefCycle | NoneCycle | AliphaticCycle | AromaticCycle | HeteroCycle;;
- type group_type = OrgGroup | InorgGroup | NeighbourGroup of int;;
-
- type group = {
- name : string;
- position : group_position;
- cycle : cycle_type;
- grouptype : group_type;
- link : int;
- };;
Обратите внимание, у нас есть аналог структуры C (group), аналоги enum (group_position, cycle_type) и гибридный тип, который может быть как enum'ом, так и произвольным числом (group_type). Теперь добавим функцию для фильтрации:
- let filter_org = List.filter (fun r -> if r.grouptype = OrgGroup then true else false)
Данная функция будет фильтровать список групп по значению поля grouptype — по сути это аналог того, что в PHP называется функцией array_filter.
Наконец, добавим регистрацию функции-фильтра для использования в C:
- let _ =
- Callback.register "filter_organic" filter_org;
- ;;
Этот блок выполнится при инициализации OCaml-части из C, предоставив нам замечательный именной callback.
Теперь нам надо для этого написать обертку.
C/C++
Первым делом мы подготовим общий cpart.h файл, в котором будут общие части для OCaml- и PHP-обёрток:
- #include <vector>
-
- typedef enum _group_position {
- UndefPos = 0,
- LeftPos,
- RightPos
- } group_position;
-
- typedef enum _cycle_type {
- UndefCycle = 0,
- NoneCycle,
- AliphaticCycle,
- AromaticCycle,
- HeteroCycle
- } cycle_type;
-
- typedef enum _group_pos {
- OrgGroup = -2,
- InorgGroup = -1
- } group_pos;
-
- typedef struct _group {
- char *name;
- group_position position;
- cycle_type cycle;
- int group_type;
- int link;
- } group;
-
- void init_ocaml ();
-
- std::vector<group> filter_org (std::vector<group> g);
Мы определили соответствия типов из OCaml и объявили две функции — для инициализации OCaml и основную действующую функцию-обертку, которая будет фильтровать массив данных.
В действительности у нас будет больше функций, но только эти мы планируем в будущем показывать для PHP. Теперь напишем файл ccamlpart.cc с оберткой для Ocaml. Сперва заголовки:
- #include "cpart.h"
- #ifdef __cplusplus
- extern "C"
- {
- #endif
- #include <caml/mlvalues.h>
- #include <caml/alloc.h>
- #include <caml/callback.h>
- #include <caml/fail.h>
- #include <caml/memory.h>
- #ifdef __cplusplus
- }
- #endif
- #include <string.h>
Поскольку мы задействуем C++ с его вектором, и код нам придётся компилировать как плюсовый, то не забываем об extern для стандартных функций OCaml. Теперь определим вспомогательные функции для конверсии типов между OCaml и C (тут).
Здесь нам потребовалась специальная функция для конверсии гибридного OCaml-типа group_type — его enum-значения хранятся как простые целые числа, а вот NeighbourGroup является уже структурой с одним полем.
Еще стоит посмотреть на три макроса: CAMLparamX, CAMLlocalX и CAMLreturn*. Первый из них используется для корректной работы сборщика мусора, и принимает на вход все OCaml-переменные, переданные в функцию. Второй используется для объявления локальных OCaml-переменных. А третий макрос опять-таки необходим не только для возврата значения, но и корректной работы сборщика мусора.
И, наконец, наши действующие функции для PHP:
- void init_ocaml ()
- {
- char *argv[1];
- argv[0] = NULL;
- caml_main(argv);
- }
-
- std::vector<group> filter_org (std::vector<group> g)
- {
- CAMLparam0();
- static value *closure_f = NULL;
- if (closure_f == NULL)
- closure_f = caml_named_value("filter_organic");
- CAMLlocal3( cli, cons, cb_res);
- cli = Val_emptylist;
- for (std::vector<group>::iterator i = g.begin(); i != g.end(); ++i)
- {
- cons = caml_alloc(2, 0);
- Store_field( cons, 0, camlgroup_of_group(&(*i)));
- Store_field( cons, 1, cli);
- cli = cons;
- }
- cb_res = caml_callback(*closure_f, cli);
- std::vector<group> result;
- while (cb_res != Val_emptylist)
- {
- result.push_back(group_of_camlgroup(Field(cb_res, 0)));
- cb_res = Field(cb_res, 1);
- }
- return result;
- }
Незатейливо из вектора конструируем OCaml-лист: он устроен буквально по всем канонам — структура, у которой первое поле является элементом, а второе — указателем на продолжение списка. Потом скармливаем этот список OCaml и результат превращаем обратно в вектор.
Соберем всё это воедино в статическую библиотеку мэйкфайлом:
- ocamlpart.o: ocamlpart.ml
- ocamlopt -g -output-obj $^ -o $@
-
- ccamlpart.o: ccamlpart.cc
- g++ -g -c -o $@ -I"`ocamlc -where`" $^
-
- libchempart.a: ocamlpart.o ccamlpart.o
- ar rcs $@ $^
-
- all: libchempart.a
-
- clean:
- rm -f *.o *.a *.cmi *.cmx
PHP
Теперь, наконец, напишем PHP-расширение. Для этого создадим в папке с проектом отдельную папку php — расширение создаст свой Makefile, затирающий тот, который мы использовали выше, поэтому мы отделяем одно от другого. Создадим в ней файл cphppart.h. В файле мы объявили стандартные функции модуля, класс Chemlib и глобальную функцию get_group_of_caml. Мы опять вынуждены использовать std::vector, поэтому не забываем про extern для PHP-инклюдов. Теперь в файле cphppart.cc реализуем непосредственно расширение. Для начала стандартная информация о расширении, классе и его функциях (смотреть тут). Затем функция инициализации модуля — именно в ней класс регистрируется в PHP, а также получает помимо методов еще и свойства:
- PHP_MINIT_FUNCTION(chemlib)
- {
- init_ocaml();
- zend_class_entry chemlib_ce;
- INIT_CLASS_ENTRY(chemlib_ce, PHP_CHEMLIB_CLASS_NAME, chemlib_class_functions);
- chemlib_class_entry = zend_register_internal_class(&chemlib_ce TSRMLS_CC);
- zend_declare_property_string(chemlib_class_entry, (char*)"name", 4, (char*)"", ZEND_ACC_PUBLIC TSRMLS_CC);
- zend_declare_property_long(chemlib_class_entry, (char*)"position", 8, 0, ZEND_ACC_PUBLIC TSRMLS_CC);
- zend_declare_property_long(chemlib_class_entry, (char*)"cycle", 5, 0, ZEND_ACC_PUBLIC TSRMLS_CC);
- zend_declare_property_long(chemlib_class_entry, (char*)"group_type", 10, 0, ZEND_ACC_PUBLIC TSRMLS_CC);
- zend_declare_property_long(chemlib_class_entry, (char*)"link", 4, 0, ZEND_ACC_PUBLIC TSRMLS_CC);
-
- zend_declare_class_constant_long(chemlib_class_entry, (char*)"UndefPos", 8, 0 TSRMLS_CC);
- zend_declare_class_constant_long(chemlib_class_entry, (char*)"LeftPos", 7, 1 TSRMLS_CC);
- zend_declare_class_constant_long(chemlib_class_entry, (char*)"RightPos", 8, 2 TSRMLS_CC);
-
- zend_declare_class_constant_long(chemlib_class_entry, (char*)"UndefCycle", 10, 0 TSRMLS_CC);
- zend_declare_class_constant_long(chemlib_class_entry, (char*)"NoneCycle", 9, 1 TSRMLS_CC);
- zend_declare_class_constant_long(chemlib_class_entry, (char*)"AliphaticCycle", 14, 2 TSRMLS_CC);
- zend_declare_class_constant_long(chemlib_class_entry, (char*)"AromaticCycle", 13, 3 TSRMLS_CC);
- zend_declare_class_constant_long(chemlib_class_entry, (char*)"HeteroCycle", 11, 4 TSRMLS_CC);
-
- zend_declare_class_constant_long(chemlib_class_entry, (char*)"OrgGroup", 8, -2 TSRMLS_CC);
- zend_declare_class_constant_long(chemlib_class_entry, (char*)"InorgGroup", 10, -1 TSRMLS_CC);
- };
-
Как видно, мы задали классу все те же свойства, что и в C для структуры group, кроме того, объявили удобные константы типа Chemlib::OrgGroup. Добавим дефолтных функций, функционалом которые каждый может наполнить по вкусу (тут). Приправим вспомогательными внутренними функциями для конверсии группы между C и PHP:
- zval *phpgroup_of_group (group *gr)
- {
- zval *res;
- ALLOC_INIT_ZVAL(res);
- object_init_ex(res, chemlib_class_entry);
- zend_update_property_string(Z_OBJCE_P(res), res, (char*)"name", 4, gr->name TSRMLS_CC);
- zend_update_property_long(Z_OBJCE_P(res), res, (char*)"position", 8, gr->position TSRMLS_CC);
- zend_update_property_long(Z_OBJCE_P(res), res, (char*)"cycle", 5, gr->cycle TSRMLS_CC);
- zend_update_property_long(Z_OBJCE_P(res), res, (char*)"group_type", 10, gr->group_type TSRMLS_CC);
- zend_update_property_long(Z_OBJCE_P(res), res, (char*)"link", 4, gr->link TSRMLS_CC);
- return res;
-
- }
-
- group group_of_phpgroup (zval *gr)
- {
- group res,def;
- zval *x = zend_read_property(chemlib_class_entry, gr, (char*)"name", 4, 1 TSRMLS_CC);
- if (Z_TYPE_P(x) != IS_STRING)
- return def;
- res.name = estrdup(Z_STRVAL_P(x));
- x = zend_read_property(chemlib_class_entry, gr, (char*)"position", 8, 1 TSRMLS_CC);
- if (Z_TYPE_P(x) != IS_LONG)
- return def;
- res.position = (group_position)Z_LVAL_P(x);
- x = zend_read_property(chemlib_class_entry, gr, (char*)"cycle", 5, 1 TSRMLS_CC);
- if (Z_TYPE_P(x) != IS_LONG)
- return def;
li style="font-weight: n