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

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

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

Всего хорошего.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 16

    +1
    Кстати, а libc++ имеет шансы собраться на Андроиде? Там хорошая поддержка C++11, да и либа почище в плане кода чем libstdcpp.
      0
      Насколько я заметил, эта библиотека и под обычными системами ещё толком не работает. Есть ли смысл еёпортировать на Андроид? Там у них более приоритетные версии есть. Хотя при желании конечно запортировать можно все, что угодно. Чем собственно сейчас и занимаюсь.
        0
        Под макосью она используется в с++11 режиме и вполне радует.
          0
          Если очень хочется, можно портировать, кто бы только взялся?
          Если у исходников нормальная система сборки, желательно make — она лучше всего согласуется с *.mk, то построить её под Android, думаю не составит особого труда. 4 аналогичных примера уже есть в составе NDK
            0
            Там cmake, с ним вроде бы особых сложностей для Андроида нет, но она вроде как собирается clang'ом, поэтому видимо придется вначале его ещё собрать.
            Хотя есть впечатление, что оно должно просто взять и заработать. Ибо когда еще давно в libcxx не была заявлена еще поддержка linux, она прекрасно собиралась и работала.
      0
      Можно подробнее о работе с Java-исключениями в native?
        0
        А что конкретно интересует? Обработка исключений в ява машине стандартна: после вызова каждого метода JNI environment делаем checkException() и по желанию describeException(). Это всё подробно описано в JNI specification.
        Здесь я только акцентировал внимание на способе удобной обёртки ява ошибок в плюсовые и обратно, поскольку частенько приходится менять контекст выполнения с байткода в натив и обратно.
          0
          Интересует, как именно
          все ява исключения в нативе перехватываются и оборачиваются наследниками std::exception
          , особенно если исключение нельзя обработать и приходится посылать из native-метода обратно в Java.
          Я подобную задачу когда-то решал, но получилось многовато некрасивого кода. Думал, может у вас вышло лучше.
            0
            Я не вижу здесь каких то сложностей. Если произошло исключение, мы его фиксируем, получаем на него ссылку, затем возбуждаем исключение в нативе, вылетаем на уровень JNI и заново бросаем ява-исключение с сохраненным исключением в качестве причины
        0
        Можете примерами кода показать связку? А то я читал статью, но мысль не ухватил (хотя сам занимаюсь тем же самым).
          0
          Вообще говоря это коммерческий продукт, но возможно что-то можно выложить
            0
            Давайте лучше так, что конкретно хотелось бы уточнить?
              0
              «конвертировать поток выполнения», «Все Ява объекты, при привязке их к нативу» — что вы имеете ввиду?
              «обратые методы для доступа к полям объекта в 3 раза менее производительны, чем прямой вызов» — чьи методы? к полям какого объекта? прямой вызов откуда?

              После этого у нас готов к работе Ява-объект, его нативные методы автоматом идут через JNI, а там в свою очередь достается указатель на нативный объект, он кастуется и вызывает соответствующий метод C++.

              покажите структуру этого Ява-объекта в коде .java хотя-бы с одним нативным методом, что значит нативные методы автоматом идут через JNI? Где достается указатель на нативный объект?
                +1
                Поток выполнения (control flow) — стандартное понятие. В нашем случае он происходит на 2-х уровнях: на верхнем уровне байткода и нижнем нативном. При переходе из одного в другой приходиться все данные преобразовывать из одного представления в другое, это я и называю конвертированием.

                «при привязке их к нативу» — имеется в виду использование ява-объекта с нативными методами. В моем случае реализация этих методов находится в соответствующих плюсовых объектах. То есть ява-объекты считаются привязанными к нативу, если им в соответствие есть нативная функия. Всё просто.

                Пример:
                    class Foo extends NativeObject{
                        native void exec();
                        native int calc();
                    }
                

                Привязывается на функции
                    JNIEXPORT void Java_Foo_exec();
                    JNIEXPORT jint Java_Foo_calc();
                

                А они в свою очередь обращаются к методам
                    class Foo: public NativeObject{
                        void exec();
                        int calc();
                    }
                


                Эта привязка называется прямым вызовом (на developerworks, т.к. jvm непосредственно зовет эти методы с передачей им аргументов). Обратный вызов — из натива в jvm — производится через явовские коллбэки. Подробнее можно прочитать здесь

                Указатель на нативный объект достается в функциях Java_Foo_*одним коллбэком. Это пока что одно из неэффективных решений. По хорошему этот указатель можно сразу передавать в качестве параметра, но я не хотел захламлять интерфейсы в яве.
                  0
                  Вот теперь все встало на свои места. Спасибо!
            0
            -

            Only users with full accounts can post comments. Log in, please.