Так получилось, что недавно на работе мне понадобилось портировать старенькое нативное приложение под Android. Приложение написано в основном на C/C++. Захотелось мне это проделать грамотно и цивилизованно. Собственно об этом под катом
Приложение было написано в основном на C и C++. Но местами даже использоался ассемблер. Поскольку сам я предпочитаю всегда пользоваться объектыным языком, возникло желание сделать привязку Java кода к плюсовыой библиотеке.
Но тут возникли сложности. Официально поддерживаемый способ привязки нативного кода и в оригинальной jvm, и в Далвике — JNI, а он как известно реализуется через C-функции. Пришлось в связи с этим немного поизвращаться и дважды конвертировать поток выполнения: из Ява-объектов в сишный коллбэк и из сишного кода — в плюсовые объекты.
Конечно же я смотрел в сторону готовых решений, но как правило их лицензия не позволяла использовать оные в проприетарном продукте. Задачу облегчало то, что Ява-часть и интерфейс для неё в нативе я делал сам, а потому мог упростить себе жизнь.
В итоге вот что я изобрел (конечно, вы можете заметить в этом старое колесо). Все Ява объекты, при привязке их к нативу, выстраиваются в иерархию, то бишь наследуются от одного класса-предка NativeObject. Этот класс несёт в себе основную логику по обслуживаеию общих для всех привязанных классов функций. Мапится он на класс с аналогичным названием в плюсовой части. Первоочередные методы для реализации:
-
native long initNative(String klass);
— возвращает указатель на нативный объект и тут же его сохраняет в специальном поле, это общий у многих подход.
-
native void finalizeNative();
— сообщает о необходимости деаллоцировать нативный объект.
-
native void setFields(Object ... params);
— служит для оптимизации работы с нативной средой.
Последний метод необходим, так как обратые методы для доступа к полям объекта в 3 раза менее производительны, чем прямой вызов. После этого в каждом классе-наследнике реализуем constructor()
(само собой) и статический инициализатор для загрузки метаданных класса в натив, он тоже маппится
В C++ реализуем конструктор с заранее обговоренным для всех наследников списком параметров, среди них проще всего сразу передать ссылку на Ява-объект. В общем для всех хидере у нас подготовлена шаблонная функция-фабрика для нативных объектов. Её тоже определяем (специализируем).
Теперь весь процесс создания объекта протекает так:
- Загружается ява-класс
- Вызывается статический инициализатор
- Инициализируются метаданные класса в нативе: заводятся постоянные ссылки на сам класс, нужные нам первичне поля и т.п.
- Вызывается ява-конструктор, зовет конструктор NativeObject
- Вызывается initNative
- Через фабрику инстанцируется плюсовый объект
- Указатель на него сохраняется
После этого у нас готов к работе Ява-объект, его нативные методы автоматом идут через JNI, а там в свою очередь достается указатель на нативный объект, он кастуется и вызывает соответствующий метод C++.
Вот так в общих чертах работает привязка.
Теперь ещё расскажу немного про другие аспекты работы с плюсами в Java в целом и в Андроиде в частности.
В Яве принято ошибочные ситуации посылать по каналу исключений. Поэтому си-стайл обработки ошибок исходно очень трудно совмещать с таким подходом. Естественно напрашивалась поддержка исключений в нативе. Что и было сделано. К сожалению плюсы в Андроиде поддерживаются пока очень плохо. Те же исключения не поддерживается умолчальной бибилиотекой. Поэтому сразу рекомендую переходить на gnu-libstdc++. Если пользоваться ГНУ инструментарием, то её лицензия вполне подходит для любых проектов.
Так вот, все ява исключения в нативе перехватываются и оборачиваются наследниками std::exception. Аналогично, все сишные ошибки оборачиваются в std::exception и посылаются наружу, Здесь на подходе к сишным колбэкам их полет прерывает зенитная установка catch и превращает в Java exception. Стоит напомнить тем, кто пишет в основном на яве, что с плюсовыми исключениями нужно обращаться очень аккуратно, иначе зенитные установки подорвут сами себя.
И напоследок, хотелось поругать Андроидную поддержу, вернее её отсутствие, для строковых классов с широкими символами. Тут пришлось просто переопределить std::wstring через перевключение стандартных заголовков. После этой хирургии проблем с широкими строками пока не было.
Ну и естественно при использовании плюсов значительно упрощается управление ресурсами ява-машины, а это не только память, но и ссылки на объекты — аналоги различных хэндлов в разных системах. Очень помогло использование буста для управления временем жизни: как вы знаете объекты в яве должны жить пока их не убьёт сборщик мусора.
Всего хорошего.
Автор: mahairod