Pull to refresh

Самопальная привязка С++ классов к JVM через JNI

Reading time3 min
Views9K
Так получилось, что недавно на работе мне понадобилось портировать старенькое нативное приложение под 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 через перевключение стандартных заголовков. После этой хирургии проблем с широкими строками пока не было.

Ну и естественно при использовании плюсов значительно упрощается управление ресурсами ява-машины, а это не только память, но и ссылки на объекты — аналоги различных хэндлов в разных системах. Очень помогло использование буста для управления временем жизни: как вы знаете объекты в яве должны жить пока их не убьёт сборщик мусора.

Всего хорошего.
Tags:
Hubs:
Total votes 15: ↑14 and ↓1+13
Comments16

Articles