среда, 13 ноября 2013 г.

Автоматическое оборачивание OpenCV-based кода в Java

При разработке чего нибудь полезного с OpenCV под Android часто возникает необходимость пробросить какие-то свои функции и классы в Java. Пока функций совсем чуть-чуть это можно сделать руками, но в какой-то момент количество рутинного кода начинает зашкаливать и лопается терпение. Проблема обостряется, когда в Java хочется завернуть целый модуль. При проявлении проблемы возникает закономерный вопрос: "Нельзя ли это автоматизировать и как это сделано в самой OpenCV?" Подробности под катом.


История вопроса начинается не с Java, а с Python. В некоторый момент появилась необходимость сделать биндинги для Python, но заворачивать в Python все функции и классы вручную не хотелось. Во-первых, это гигантское количество работы на старте. Во-вторых, ручные обёртки - это кошмар для поддержки. Библиотека активно развивается и обёртки постоянно будут отставать от основного кода. Решением стали парсер заголовков библиотеки, специальная разметка функций, что экспортировать, а что нет, и генератор обёрток.

Первое - парсер заголовочных файлов. Он уже реализован и живёт в OpenCV в opencv/modules/python/src2/hdr_parser.py. Главная его задача - найти функции, классы, методы и константы в заголовочном файле C++. И с ней он справляется на отлично. Что полезно, написанный один раз он может быть использован много раз для любых биндингов или построения документации.

Второе - разметка заголовков. В OpenCV как и в любой мало-мальски серьёзной библиотеке есть служебная функциональность, которую не стоит экспортировать в другие языки, поэтому имеет смысл разметить что и как экспортировать наружу. В OpenCV разметка выполнена виде макросов, которые при компиляции разворачиваются в указания компилятору по экспорту символов библиотеки. Таким образом, всё что экспортируется публично всегда будет экспортировано в друге языки. Не буду вдаваться в подробности, просто приведу список макросов и комментарии по их использованию.

Таблица макросов для разметки экспортных функций в OpenCV.
CV_EXPORTSПубличный символ для экспорта. В биндинги не попадает.
CV_EXPORTS_WПубличный символ для экспорта. Попадает в биндинги.
CV_WRAPПубличный метод класса. Попадает в биндинги. Класс должен быть помечен как CV_EXPORTS_W.

Третье, самое важное, - генерация биндингов. Биндинги к большинству популярных языков программирования состоят из двух частей: некоторого описания классов и функций на целевом языке и библиотеки-переходника, который экспортирует необходимую функциональность в соответствии с правилами интерпретатора, компилятора и прочих, а так же преобразует сущности одного языка в сущности другого, если у них нет прямого соответствия. В случае с Java, нам нужны интерфейсы классов на Java и нативная JNI библиотека. И то и другое делает Java Generator в modules/java/generator/gen_java.py. Использовать его очень просто:

$ gen_java.py <парсер заголовков> <имя модуля> <заголовки С++>

Парсер заголовков тот самый, что когда-то был написан для Python'a. Имя модуля, это то что будет именем пакета в терминах Java. Скрипточек написан для OpenCV, поэтому он ко все именам к началу добавляет org.opencv. Если вы решили назвать модуль UserTracker, пакет для Java пакет для Java будет называться org.opencv.UserTracker.
Поупражнямся на OpenCV core:

$ ./opencv/modules/java/generator/gen_java.py ./opencv/modules/python/src2/hdr_parser.py core ./opencv/modules/core/include/opencv2/core/core.hpp

В консоль скипт напечатал список идентификаторов, которые обернуть в Java не получилось. Кроме вывода в консоль скрипт сгенерировал несколько файлов. *.cpp файл с тем же именем, что и модуль для Java содержит весь JNI код и готов для компиляции. *.java файлы для всех классов в заголовочном файле и ещё один класс одноимённый с модулем, содержащий все функции в виде статических методов и константы. *.txt файл содержит подробный отчёт: что завернулось в Java, а что нет.

Как мы видим, часть сущностей парсер пропустил, несмотря на правильную разметку. Всё дело в волшебных пузырьках, а если честно, то в плюсовых шаблонах. Для них нет прямого соответствия в Java, поэтому базовые конструкции с шаблонами приходится оборачивать вручную. Примером того является std::vector и cv::Algorithm. Первый уже более-менее обёрнут в Java, кроме случая std::vector<std::string> в виде производных от Mat. Со вторым дело пока плохо. Всё, что использует cv::Algorithm в интерфейсах в Java и другие языки не попадает, а жаль.

Получившиеся Java и *.cpp файлы совпадают с теми, что получаются при построении OpenCV с точностью до документации. Javadoc добавляет другой скрипт. Документация к бинднингам берётся из *.rst файлов и совпадает с публичной документацией на плюсовый аналог. Генератор лишь расставляет комментарии со специальными метками, чтобы было понятно, что куда приклеивать. Делается это только сугубо для удобства работы в Eclipse и других IDE понимающих Javadoc. Если вы пишите экстра модуль для OpenCV и у вас есть описание интерфейса в *.rst, то Javadoc вставится автоматически при построении.
Последним шагом после генерации обёрток и перед компиляцией надо разложить все исходники по местам. Всё что *.cpp надо положить в папку jni. Всё что *.java - разложить по папочкам в соответствии с именами пакетов. Не забывайте про org.opencv. Как и предыдущие действия, процесс раскладки имеет смысл автоматизировать. В OpenCV это делает cmake и make при построении.
На этом пока всё про генераторы обёрток. Надеюсь, что заметка окажется полезной.

Комментариев нет:

Отправить комментарий