Pull to refresh

OCaml и PHP — эзотерика для вашего удобства

Reading time9 min
Views2.7K
NB: читая этот топик, желательно не есть — можно поперхнуться от неожиданности.
NB: менее ценные куски кода пришлось вынести на пастебин, из-за того что хабр обрезает пост. Следите за ссылками в тексте.

Многие причисляют OCaml к маргинальным и даже эзотерическим языкам. Возможно они и правы, хотя множество людей с ними не согласны. Для меня знакомство с ним началось с полгода назад, когда мне в очередной раз захотелось научиться чему-то новому и я подумал, что хоть один функциональный язык надо освоить. Из множества языков я выбрал Objective Caml. Язык покорил меня человеческим синтаксисом и идеей: есть все функциональные радости жизни, но если хочешь императивный стиль и ООП — бери, их есть у меня! Оказалось, что разработчики прекрасно понимали, что для разных задач нужны разные средства. Три дня чтения мануала для C++ и Perl-программистов и я уже вполне мог читать код и писать хеллоуворлды. На этом моё знакомство с языком закончилось, потому что изучать язык не на реальной задаче — дело глупое.

Вернулся я к OCaml с пару недель назад, когда возникла одна интересная задача. Я посмотрел на неё и понял, что в принципе её можно целиком решить на PHP, или написать, как я люблю, расширение PHP на C++, но функциональный язык подошёл бы гораздо больше. Итак, у нас имеется основная программа на PHP, из которой мы хотим вызывать функции OCaml, получая от этого очевидный профит и незамутненную радость идиота и Мичурина в одном флаконе. В данной статье я приведу лишь простой код для демонстрации принципа — без хитрых оптимизаций и всего такого, что сделает наш код намного быстрее, но ухудшит его читаемость. Кроме того, я постарался максимально разделить обертки для PHP и OCaml, поэтому конвертация данных идёт не напрямую PHP←→OCaml, а по пути PHP←→C←→OCaml, что дополнительно снижает скорость. Зато так намного понятнее будет принцип, а с оптимизацией, надеюсь, справятся все желающие.
Задачу, которую мы решаем, определим следующим образом: необходим принять от PHP массив структур, отфильтровать его некоторым образом, например, по какому-то значению, а затем оставшийся массив вернуть.

OCaml


Приступим. Для начала напишем нашу OCaml-часть, которая будет выполнять всю работу. Определим в файле ocamlpart.ml типы данных, с которыми будем работать. Пусть это будут некоторые химические группы:
  1. type group_position = UndefPos | LeftPos | RightPos;;
  2. type cycle_type = UndefCycle | NoneCycle | AliphaticCycle | AromaticCycle | HeteroCycle;;
  3. type group_type = OrgGroup | InorgGroup | NeighbourGroup of int;;
  4.  
  5. type group = {
  6.         name : string;
  7.         position : group_position;
  8.         cycle : cycle_type;
  9.         grouptype : group_type;
  10.         link : int;  
  11. };;      


Обратите внимание, у нас есть аналог структуры C (group), аналоги enum (group_position, cycle_type) и гибридный тип, который может быть как enum'ом, так и произвольным числом (group_type). Теперь добавим функцию для фильтрации:
  1. let filter_org = List.filter (fun r -> if r.grouptype = OrgGroup then true else false)

Данная функция будет фильтровать список групп по значению поля grouptype — по сути это аналог того, что в PHP называется функцией array_filter.
Наконец, добавим регистрацию функции-фильтра для использования в C:
  1. let _ =
  2.         Callback.register "filter_organic" filter_org;
  3. ;;

Этот блок выполнится при инициализации OCaml-части из C, предоставив нам замечательный именной callback.
Теперь нам надо для этого написать обертку.

C/C++


Первым делом мы подготовим общий cpart.h файл, в котором будут общие части для OCaml- и PHP-обёрток:
  1. #include <vector>
  2.  
  3. typedef enum _group_position {
  4.         UndefPos = 0,
  5.         LeftPos,
  6.         RightPos
  7. } group_position;
  8.  
  9. typedef enum _cycle_type {
  10.         UndefCycle = 0,
  11.         NoneCycle,
  12.         AliphaticCycle,
  13.         AromaticCycle,
  14.         HeteroCycle
  15. } cycle_type;
  16.  
  17. typedef enum _group_pos {
  18.         OrgGroup = -2,
  19.         InorgGroup = -1
  20. } group_pos;
  21.  
  22. typedef struct _group {
  23.         char *name;
  24.         group_position position;
  25.         cycle_type cycle;
  26.         int group_type;
  27.         int link;
  28. } group;
  29.  
  30. void init_ocaml ();
  31.  
  32. std::vector<group> filter_org (std::vector<group> g);

Мы определили соответствия типов из OCaml и объявили две функции — для инициализации OCaml и основную действующую функцию-обертку, которая будет фильтровать массив данных.
В действительности у нас будет больше функций, но только эти мы планируем в будущем показывать для PHP. Теперь напишем файл ccamlpart.cc с оберткой для Ocaml. Сперва заголовки:
  1. #include "cpart.h"
  2. #ifdef __cplusplus
  3. extern "C"
  4. {
  5. #endif
  6. #include <caml/mlvalues.h>
  7. #include <caml/alloc.h>
  8. #include <caml/callback.h>
  9. #include <caml/fail.h>
  10. #include <caml/memory.h>
  11. #ifdef __cplusplus
  12. }
  13. #endif
  14. #include <string.h>

Поскольку мы задействуем C++ с его вектором, и код нам придётся компилировать как плюсовый, то не забываем об extern для стандартных функций OCaml. Теперь определим вспомогательные функции для конверсии типов между OCaml и C (тут).
Здесь нам потребовалась специальная функция для конверсии гибридного OCaml-типа group_type — его enum-значения хранятся как простые целые числа, а вот NeighbourGroup является уже структурой с одним полем.
Еще стоит посмотреть на три макроса: CAMLparamX, CAMLlocalX и CAMLreturn*. Первый из них используется для корректной работы сборщика мусора, и принимает на вход все OCaml-переменные, переданные в функцию. Второй используется для объявления локальных OCaml-переменных. А третий макрос опять-таки необходим не только для возврата значения, но и корректной работы сборщика мусора.
И, наконец, наши действующие функции для PHP:
  1. void init_ocaml ()
  2. {      
  3.         char *argv[1];
  4.         argv[0] = NULL;
  5.         caml_main(argv);
  6. }              
  7.  
  8. std::vector<group> filter_org (std::vector<group> g)
  9. {      
  10.         CAMLparam0();
  11.         static value *closure_f = NULL;
  12.         if (closure_f == NULL)
  13.                 closure_f = caml_named_value("filter_organic");
  14.         CAMLlocal3( cli, cons, cb_res);
  15.         cli = Val_emptylist;
  16.         for (std::vector<group>::iterator i = g.begin(); i != g.end(); ++i)
  17.         {
  18.                 cons = caml_alloc(2, 0);
  19.                 Store_field( cons, 0, camlgroup_of_group(&(*i)));
  20.                 Store_field( cons, 1, cli);
  21.                 cli = cons;
  22.         }      
  23.         cb_res = caml_callback(*closure_f, cli);
  24.         std::vector<group> result;
  25.         while (cb_res != Val_emptylist)
  26.         {
  27.                 result.push_back(group_of_camlgroup(Field(cb_res, 0)));
  28.                 cb_res = Field(cb_res, 1);
  29.         }
  30.         return result;
  31. }

Незатейливо из вектора конструируем OCaml-лист: он устроен буквально по всем канонам — структура, у которой первое поле является элементом, а второе — указателем на продолжение списка. Потом скармливаем этот список OCaml и результат превращаем обратно в вектор.

Соберем всё это воедино в статическую библиотеку мэйкфайлом:
  1. ocamlpart.o: ocamlpart.ml
  2.         ocamlopt -g -output-obj $^ -o $@
  3.  
  4. ccamlpart.o: ccamlpart.cc
  5.         g++ -g -c -o $@ -I"`ocamlc -where`" $^
  6.  
  7. libchempart.a: ocamlpart.o ccamlpart.o
  8.         ar rcs $@ $^
  9.  
  10. all: libchempart.a
  11.  
  12. clean:
  13.         rm -f *.o *.a *.cmi *.cmx


PHP


Теперь, наконец, напишем PHP-расширение. Для этого создадим в папке с проектом отдельную папку php — расширение создаст свой Makefile, затирающий тот, который мы использовали выше, поэтому мы отделяем одно от другого. Создадим в ней файл cphppart.h. В файле мы объявили стандартные функции модуля, класс Chemlib и глобальную функцию get_group_of_caml. Мы опять вынуждены использовать std::vector, поэтому не забываем про extern для PHP-инклюдов. Теперь в файле cphppart.cc реализуем непосредственно расширение. Для начала стандартная информация о расширении, классе и его функциях (смотреть тут). Затем функция инициализации модуля — именно в ней класс регистрируется в PHP, а также получает помимо методов еще и свойства:
  1. PHP_MINIT_FUNCTION(chemlib)
  2. {
  3.         init_ocaml();
  4.         zend_class_entry chemlib_ce;
  5.         INIT_CLASS_ENTRY(chemlib_ce, PHP_CHEMLIB_CLASS_NAME, chemlib_class_functions);
  6.         chemlib_class_entry = zend_register_internal_class(&chemlib_ce TSRMLS_CC);
  7.         zend_declare_property_string(chemlib_class_entry, (char*)"name", 4, (char*)"", ZEND_ACC_PUBLIC TSRMLS_CC);
  8.         zend_declare_property_long(chemlib_class_entry, (char*)"position", 8, 0, ZEND_ACC_PUBLIC TSRMLS_CC);
  9.         zend_declare_property_long(chemlib_class_entry, (char*)"cycle", 5, 0, ZEND_ACC_PUBLIC TSRMLS_CC);
  10.         zend_declare_property_long(chemlib_class_entry, (char*)"group_type", 10, 0, ZEND_ACC_PUBLIC TSRMLS_CC);
  11.         zend_declare_property_long(chemlib_class_entry, (char*)"link", 4, 0, ZEND_ACC_PUBLIC TSRMLS_CC);
  12.  
  13.         zend_declare_class_constant_long(chemlib_class_entry, (char*)"UndefPos", 8, 0 TSRMLS_CC);
  14.         zend_declare_class_constant_long(chemlib_class_entry, (char*)"LeftPos", 7, 1 TSRMLS_CC);
  15.         zend_declare_class_constant_long(chemlib_class_entry, (char*)"RightPos", 8, 2 TSRMLS_CC);
  16.  
  17.         zend_declare_class_constant_long(chemlib_class_entry, (char*)"UndefCycle", 10, 0 TSRMLS_CC);
  18.         zend_declare_class_constant_long(chemlib_class_entry, (char*)"NoneCycle", 9, 1 TSRMLS_CC);
  19.         zend_declare_class_constant_long(chemlib_class_entry, (char*)"AliphaticCycle", 14, 2 TSRMLS_CC);
  20.         zend_declare_class_constant_long(chemlib_class_entry, (char*)"AromaticCycle", 13, 3 TSRMLS_CC);
  21.         zend_declare_class_constant_long(chemlib_class_entry, (char*)"HeteroCycle", 11, 4 TSRMLS_CC);
  22.  
  23.         zend_declare_class_constant_long(chemlib_class_entry, (char*)"OrgGroup", 8, -2 TSRMLS_CC);
  24.         zend_declare_class_constant_long(chemlib_class_entry, (char*)"InorgGroup", 10, -1 TSRMLS_CC);
  25. };
  26.  

Как видно, мы задали классу все те же свойства, что и в C для структуры group, кроме того, объявили удобные константы типа Chemlib::OrgGroup. Добавим дефолтных функций, функционалом которые каждый может наполнить по вкусу (тут). Приправим вспомогательными внутренними функциями для конверсии группы между C и PHP:
  1. zval *phpgroup_of_group (group *gr)
  2. {
  3.         zval *res;
  4.         ALLOC_INIT_ZVAL(res);
  5.         object_init_ex(res, chemlib_class_entry);
  6.         zend_update_property_string(Z_OBJCE_P(res), res, (char*)"name", 4, gr->name TSRMLS_CC);
  7.         zend_update_property_long(Z_OBJCE_P(res), res, (char*)"position", 8, gr->position TSRMLS_CC);
  8.         zend_update_property_long(Z_OBJCE_P(res), res, (char*)"cycle", 5, gr->cycle TSRMLS_CC);
  9.         zend_update_property_long(Z_OBJCE_P(res), res, (char*)"group_type", 10, gr->group_type TSRMLS_CC);
  10.         zend_update_property_long(Z_OBJCE_P(res), res, (char*)"link", 4, gr->link TSRMLS_CC);
  11.         return res;
  12.  
  13. }
  14.  
  15. group group_of_phpgroup (zval *gr)
  16. {
  17.         group res,def;
  18.         zval *x = zend_read_property(chemlib_class_entry, gr, (char*)"name", 4, 1 TSRMLS_CC);
  19.         if (Z_TYPE_P(x) != IS_STRING)
  20.                 return def;
  21.         res.name = estrdup(Z_STRVAL_P(x));
  22.         x = zend_read_property(chemlib_class_entry, gr, (char*)"position", 8, 1 TSRMLS_CC);
  23.         if (Z_TYPE_P(x) != IS_LONG)
  24.                 return def;
  25.         res.position = (group_position)Z_LVAL_P(x);
  26.         x = zend_read_property(chemlib_class_entry, gr, (char*)"cycle", 5, 1 TSRMLS_CC);
  27.         if (Z_TYPE_P(x) != IS_LONG)
  28.                 return def;
  29. li style="font-weight: n
Tags:
Hubs:
Total votes 63: ↑52 and ↓11+41
Comments50

Articles