Я люблю desktop-приложения. Признаваться в этом нынче, похоже, стыднее, чем в связях с иностранной разведкой, но это так. Нет, это не значит, что я не люблю интернет-технологии. Более того, некоторые я не только уважаю, а даже более-менее знаю. Но, тем не менее, я скучаю по тем временам, когда программа писалась на одном компьютере, потом компилировалась и запускалась на других, разных компьютерах. Тогда везде (почти) была одна система — Windows с одной и той же API, почти не было проблем совместимости на уровне приложений, никто не материл разработчиков браузеров — все берегли нервы на разработчиков WinAPI, которые умудрялись создавать конфликты даже внутри нее одной. Но это я, конечно, иронизирую, а если серьезно — иногда и сейчас хочется написать просто desktop-приложение, да так, чтобы работало оно на всех популярных системах. Трудно? Если подумать и покопать, то не очень.
Еще я люблю языки высокого уровня с аккуратной архитектурой и строгой типизацией. Мои фавориты — Java и C#. Оба они предоставляют разработчику множество преимуществ по сравнению с C++, оба избавляют от ряда забот. Чем приходится платить? Тем, что таскаешь за собой тяжелую колоду, которая называется Oracle JVM, .NET или mono. Все три колоды весят сотни мегабайт и лицензию имеют такую, что каждый пользователь вынужден качать эту штуку сам, не путая при этом разрядность своего компьютера, а главное — программа на Java не может быть совместима со всеми версиями JVM разом, не так ли? И вот — мы приходим к тому, что просто скинуть программку другу (или миллиону друзей) и не заботиться о том, что она у него не запустится, не выходит. Приходится делать хитрые сетапы, вбивать костыли, и это я еще не упомянул .NET — однажды я видел у друга сразу 3 установленных версии, причем все три были нужны разным приложениям…
Стоп! А давайте напишем программу на Java, но так, чтобы она не требовала установки на машину какой-либо JVM, чтобы одним касанием собиралась под Windows, Linux и OS X и чтобы при этом занимала совсем чуть-чуть; так, чтобы никто даже не понял, что она написана, скажем, не на C. Невозможно? Совсем наоборот! (И нет, я имею в виду не gcj, который лишает Java всех ее прелестей. Рефлексия будет работать и даже сторонние jar вы сможете запускать).

Разумеется, я не волшебник. Я только нашел один волшебный артефакт. Называется он Avian, лежит по адресу oss.readytalk.com/avian и представляет собой легковесную, но полноценную стороннюю реализацию JVM, о которой Oracle, возможно, даже не слышали. Он поддерживает кучу платформ и архитектур, имеет лицензию «бери и делай, что хочешь», и — нет, я не имею ни малейшего отношения к этому проекту, я даже не контрибьютор, я только научился им пользоваться и хочу поделиться этим могучим знанием с уважаемыми обитателями Хабра. Стоит также отметить, что он является JIT-компилятором, то есть имеет конкурентно-высокую производительность (хотя я пока что не измерял ее).
Avian можно встроить в ваше приложение вместе с его весьма урезанной, но терпимой по функционалу стандартной библиотекой классов, причем «потяжелеет» программа всего только на мегабайт с гаком. Давайте соберем такую программу вместе.
Для построения нам прежде всего понадобятся утилиты командной строки unix-разработчика, в частности — компилятор g++. Тут сложнее всего придется Windows-пользователям. Раньше я под Windows использовал MinGW32 с ее замечательной средой MSYS, эмулирующей unix-терминал. Компиляторы, входящие в MinGW32 32-битные, что в некотором роде ограничивает полученную программу. В комментариях мне подсказали, что уже давно существует удобная mingw-w64, которая регулярно обновляется и в которой есть не только MSYS, но даже и git.
Здесь и далее я спрятал платформозависимые инструкции под спойлеры для удобства.
Итогом в каждой операционной системе должно быть то, что вы вводите в командной строке
и в ответ видите что-то вроде
Такое сообщение означает, что компилятор готов к бою и жаждет получить исходные файлы.
Для начала предложим нашему g++ собрать Avian.
Открываем: oss.readytalk.com/avian. Выбираем ссылку status. На открывшейся странице скачиваем Avian 0.6. Несмотря на скромный номер версии, программа совершенно стабильна (во всяком случае, мне ни разу не удалось уронить ее, а в их багтрекере значатся весьма заковыристые баги, означающие высокую стабильность того, что есть).
Распакуем исходник Avian, скачанный нами, в некоторую папку (пусть это будет ~/Projects).
Под Windows папка "~" в среде MSYS присоединяется не к домашней папке пользователя, а к папке C:\MinGW64\msys\home\username. В нашем случае, раз мы хотим максимально дистанцироваться от платформы, это даже преимущество.
Допустим, скачанный архив называется avian-0.6.tar.bz2 и лежит в ~/Downloads, тогда распаковываем его в текущую папку, набрав
В Windows путь необходимо указывать в формате mingw с прямыми слешами в виде /c/Users/username/Downloads/avian-0.6.tar.bz2. Разумеется, вы можете воспользоваться одним из ста альтернативных способов распаковать архив — главное, чтобы он попал в текущую папку. В итоге в ней появится распакованная из архива подпапка avian. Зайдем в нее:
Теперь можно попробовать запустить команду make, но, если запустить сборку прямо сейчас, скорее всего, мы получим сообщение о том, что не найден zlib. Что-нибудь вроде zlib.h: No such file or directory.
Однако, установив zlib и снова набрав
Для того, чтобы увидеть, куда вы только что положили ваше java-окружение для разработчиков, make читает переменную среды JAVA_HOME, которую нам сейчас надо правильно задать. Эта же самая переменная с тем же значением потребуется нам впоследствии для того, чтобы собирать наш собственный проект.
И вот, наконец, звёздный час — мы собираем Avian:
Если вы всё проделали правильно, вы увидите последовательность строчек вида compiling build/<имя_вашей_платформы>/<какой-то файл>, а затем linking build/<имя_вашей_платформы>/<какой-то файл>. По окончании сборочного процесса мы получим много файлов в папке build/<имя_вашей_платформы>, но заинтересуют нас отсюда только:
Мы скомпилировали весь необходимый сторонний код. Теперь займёмся созданием своего собственного.
Задача в том, чтобы сделать программу, которая будет написана на Java, которая при этом будет содержать как можно меньше платформозависимых закладок и которая будет собираться в один exe-файл, не требующий специальной установки сам по себе и работающий на любой «чистой» системе без установки каких-либо зависимостей.
Начнем, пожалуй, с теории. Обсудим, как JVM взаимодействует с системой. Любая виртуальная машина создается, в первую очередь, для того, чтобы абстрагироваться от внешней среды. Поэтому неудивительно, что самым узким местом в реализации VM является как раз вызов системных функций. Современная программа не может даже «чихнуть» без участия ОС. Читать/писать на диск — системная функция. Вывод текста в консоль — системная функция. Нарисовать окошко на экране — а вы сами как думаете?
Фактически, единственное, что приложение может делать «внутри себя» — это расчеты и принятие решений. Именно эти действия — арифметика и логика — являются функциями VM. Как только надо сделать что-то еще, она зовёт внешнюю среду. Но как? В случае Java для этого существует JNI (Java Native Interface). Суть его весьма проста. Программа, написанная на Java, содержит в себе заголовок функции, помеченный модификатором native. Например,
Такая функция понимается компилятором Java как функция, вызываемая из загруженных библиотек обычного (не виртуального) кода. В одной из этих библиотек должно быть что-то типа
При вызове в Java-коде функции
Зачем был весь этот ликбез? Затем, что в данный момент перед нами стоит весьма занятная, почти обратная, задача. Нам надо запустить native-исполняемый файл, который, будучи статически слинкован с библиотекой libavian.a, будет содержать JVM прямо внутри себя. Помимо этого, он будет содержать внутри себя все необходимые java-классы, включая и «точку входа» — класс вида
Звучит это всё довольно пугающе, однако задача эта вполне простая. Необходимо написать довольно несложный код на C, который вытащит библиотеку классов Avian (с добавленным в нее нашим классом
Сейчас мы притащим и разложим по полочкам все нужные нам для дальнейшей работы компоненты. То, что я буду описывать здесь — это мой собственный подход. Разумеется, вы вольны сделать всё иначе, так как вам заблагорассудится. Но если вы хотите в итоге получить в точности то, что я выложил на GitHub (ссылка будет в конце), постарайтесь делать всё в точности.
Создаем папку crossbase где захотим (я создал ее в Projects, рядом с avian и win32)
Внутри создаем подпапку libs
Внутри создаем подпапку с именем вашей текущей OS. Им должно быть «linux», «win32» или «osx».
В эту папку необходимо скопировать libavian.a, который мы собрали ранее. У меня это выглядит так:
Кроме того, в системе Windows, где нет zlib, в эту же папку придется скопировать еще и libz.a:
Таким образом, мы собрали минимум необходимых нам библиотек. Этого хватит для простейшей программы.
Помимо библиотек, нам понадобится classpath.jar, который также был собран вместе с avian.
И теперь пришло время раскрыть назначение таинственного binaryToObject. Он нужен нам, чтобы преобразовать наш jar-файл в специальный объектный файл, который затем будет передан линковщику и добавлен им в нашу программу. Так как эта процедура должна выполняться при каждой сборке, его тоже надо утащить в наш новый проект.
(мы снова в папке crossbase, где мы создали lib)
Имя win-x86_64 назначено внутренней папке по тому же принципу, что и в прошлый раз. Кидаем сюда binaryToObject. (в Windows он, разумеется, имеет расширение exe)
Можно запустить его и увидеть usage:
А теперь приступим к написанию кода. Создадим новый исходный файл на C++ (вы можете воспользоваться любым текстовым редактором, какой вам нравится, я использую eclipse, в котором можно редактировать и C++, и Java в рамках одного проекта, хотя для этого его придется немного настроить).
Внутри создаем файл main.cpp со следующим содержанием (приведу его целиком, а потом объясню, что там к чему):
Этот код знаком и понятен всем, кто писал кроссплатформенные динамические библиотеки с использованием gcc. Суть его в том, что в разных операционных системах по-разному описываются функции, которые должны быть экспортированы из библиотеки. «Причем тут динамическая библиотека, ведь мы же исполняемый файл собираем?» — спросите вы. В ответ я напомню, что взаимодействие Avian с платформозависимым кодом осуществляется через механизм JNI, подразумевающий вызов функции из библиотеки. Иными словами, для вашего Java-кода исполняемый файл это не только пусковая программа, а еще и динамическая библиотека функций.
Следующая часть — это странноватая магия:
Давайте разберемся. Мы декларируем некую экспортную функцию (посмотрим на
На самом деле, как мы увидим ниже, всё довольно просто, если знаешь, что делать. Так как Avian разрабатывался для встраивания его в приложения, авторы предусмотрели возможность добавления библиотеки классов непосредственно в исполняемый файл с последующей ее загрузкой оттуда. Для этого надо всего лишь преобразовать библиотеку в объектный файл. Да-да, я тоже поначалу удивился, но это очень элегантная идея. В объектном файле, содержащем наш jar, когда мы его создадим, будет декларировано 2 символа, указывающих на начало (
Наконец-то мы дошли до точки входа — функции
Поехали с начала фцнкции:
Здесь как всегда отличился Windows. Когда повсеместно было принято решение переходить от старых неудобных однобайтных кодировок к более сложным, все ОС перешли к удобной UTF-8, а любимое детище Microsoft перешло на фиксированную двухбайтную. При этом они вообще не позаботились о том, какая кодировка используется, например, в именах файлов. Но кодировка нас сейчас тоже не очень заботит. Нам надо передать строку параметров в Java (в которой тоже принят двухбайтный
Далее следует создание виртуальной машины Java:
Еще мы вытаскиваем указатель на объект
Дальнейший код читается как стих Маяковского, если только немного знать JNI.
Возьмём класс
Если мы что-нибудь не смогли, выдаем ошибку пользователю.
Вот, собственно, и весь «пусковой механизм». Теперь нам надо создать нашу программу на Java. Она, как минимум, должна содержать класс
Создадим в нашей папке crossbase/src подпапку java, в ней — подпапку crossbase (это — имя пакета), а внутри создадим файл Application.java следующего содержания:
Если вы хоть немного знаете Java, то, думаю, комментарии здесь излишни. Скажу только, что в стандартной библиотеке классов Avian нету средств форматирования строк (которые никто не мешает тихонько утянуть, к примеру, из OpenJDK).
Теперь перейдем к задаче сборки нашего проекта. Я использую make, потому что он есть всегда и везде, где есть gcc. А еще он достаточно мощный, чтобы написать на нем почти любую автоматизированную систему сборки. Нет, правда. Можно по пальцам перечислить, что мне не удавалось сделать на make и это едва ли были жизненно важные вещи. Наш Makefile будет лежать прямо в папке crossbase и выглядеть он будет вот так:
Будьте осторожны! Не путайте табуляции с пробелами, в make табуляцией выделяются команды внутри правила сборки, а пробел синтаксическим элементом не является. Присмотримся немного, как он работает. Единственная более-менее мозгодробительная конструкция — назначение вот этих переменных:
Здесь мы с помощью unix-команды find отыскиваем все файлы
Эти правила объясняют, как скомпилировать исходный файл в целевой. И, наконец, взглянем на цель
Здесь мы видим зависимость от всех найденных файлов. То есть makefile написан таким образом, чтобы собирать все java и cpp файлы, предложенные ему в правильных папках.
Остальные существенные моменты:
Пробежимся вскользь по основному правилу сборки —
Зайдя в папку crossbase/bin, запускаем из консоли наш crossbase, передав ему параметры.
Получившийся у нас проект лежит на моём GitHub-е.
Мне трудно оценить пользу от этой статьи. Если я хотя бы получу за нее инвайт, это будет значить, что она, по крайней мере, не безынтересна. Скажу только, что при кажущейся сложности, этот метод прекрасно окупается по сравнению с написанием программы, скажем, на чистом C++. Java становится очень удобна при разрастании проекта хотя бы до пары десятков классов. Даже если быть предельно аккуратном при написании кода на C++, всё равно остаются лазейки для чудовищно сложновылавливаемых ошибок. Поэтому управляющий код (не требующий суперпроизводительности) я бы всем советовал писать на Java. Код же, требующий максимальной скорости, можно написать на C++, а затем очень легко и аккуратно обернуть C++ класс Java классом. Возможно я еще напишу, как сделать это красиво и не напороться на грабли.
Изначально я планировал сделать в статье главу, посвященную добавлению к этому «бутерброду» кроссплатформенного пользовательского интерфейса SWT (того, который используется в Eclipse), но потом решил, что она будет слишком уж длинной и увесистой. Если господам читателям интересно, напишу об этом отдельно. Благодарю за внимание!
P.S.
Получив от хабровчан много отзывов, я доработал статью и программу. Спасибо всем за советы и поправки.
Еще я люблю языки высокого уровня с аккуратной архитектурой и строгой типизацией. Мои фавориты — Java и C#. Оба они предоставляют разработчику множество преимуществ по сравнению с C++, оба избавляют от ряда забот. Чем приходится платить? Тем, что таскаешь за собой тяжелую колоду, которая называется Oracle JVM, .NET или mono. Все три колоды весят сотни мегабайт и лицензию имеют такую, что каждый пользователь вынужден качать эту штуку сам, не путая при этом разрядность своего компьютера, а главное — программа на Java не может быть совместима со всеми версиями JVM разом, не так ли? И вот — мы приходим к тому, что просто скинуть программку другу (или миллиону друзей) и не заботиться о том, что она у него не запустится, не выходит. Приходится делать хитрые сетапы, вбивать костыли, и это я еще не упомянул .NET — однажды я видел у друга сразу 3 установленных версии, причем все три были нужны разным приложениям…
Стоп! А давайте напишем программу на Java, но так, чтобы она не требовала установки на машину какой-либо JVM, чтобы одним касанием собиралась под Windows, Linux и OS X и чтобы при этом занимала совсем чуть-чуть; так, чтобы никто даже не понял, что она написана, скажем, не на C. Невозможно? Совсем наоборот! (И нет, я имею в виду не gcj, который лишает Java всех ее прелестей. Рефлексия будет работать и даже сторонние jar вы сможете запускать).

Разумеется, я не волшебник. Я только нашел один волшебный артефакт. Называется он Avian, лежит по адресу oss.readytalk.com/avian и представляет собой легковесную, но полноценную стороннюю реализацию JVM, о которой Oracle, возможно, даже не слышали. Он поддерживает кучу платформ и архитектур, имеет лицензию «бери и делай, что хочешь», и — нет, я не имею ни малейшего отношения к этому проекту, я даже не контрибьютор, я только научился им пользоваться и хочу поделиться этим могучим знанием с уважаемыми обитателями Хабра. Стоит также отметить, что он является JIT-компилятором, то есть имеет конкурентно-высокую производительность (хотя я пока что не измерял ее).
Avian можно встроить в ваше приложение вместе с его весьма урезанной, но терпимой по функционалу стандартной библиотекой классов, причем «потяжелеет» программа всего только на мегабайт с гаком. Давайте соберем такую программу вместе.
0. Среда
Для построения нам прежде всего понадобятся утилиты командной строки unix-разработчика, в частности — компилятор g++. Тут сложнее всего придется Windows-пользователям. Раньше я под Windows использовал MinGW32 с ее замечательной средой MSYS, эмулирующей unix-терминал. Компиляторы, входящие в MinGW32 32-битные, что в некотором роде ограничивает полученную программу. В комментариях мне подсказали, что уже давно существует удобная mingw-w64, которая регулярно обновляется и в которой есть не только MSYS, но даже и git.
Здесь и далее я спрятал платформозависимые инструкции под спойлеры для удобства.
Windows
Чтобы скачать MinGW, идем на sourceforge.net/projects/mingwbuilds и там скачиваем два архива: x64-X.X.X-release-posix-seh-revX.7z и external-binary-packages/msys+7za+wget+svn+git+mercurial+cvs-revX.7z
После того, как они скачаны, распаковываем оба из них в удобные нам директории. Далее необходимо указать среде MSYS, где находится MinGW. Для этого идем в
После установки всего перечисленного вы открываете терминал MSYS. Для этого необходимо запустить файл
После того, как они скачаны, распаковываем оба из них в удобные нам директории. Далее необходимо указать среде MSYS, где находится MinGW. Для этого идем в
msys/etc
и там в файле fstab
прописываем путь к /mingw
так, как это показано в файле fstab.sample
У меня получилось вот так:c:/mingw64 /mingw
После установки всего перечисленного вы открываете терминал MSYS. Для этого необходимо запустить файл
msys\msys.bat
(рядом с ним лежат две иконки). Все дальнейшие действия мы будем делать из этого терминала, так как он, во-первых, поддерживает формат unix-команд и unix-пути, а, во-вторых, в нем прописаны все необходимые параметры окружения.OS X
Под OS X вам придется скачать XCode 4 и в его настройках, в разделе Downloads, установить Command Line Tools. Затем вы просто открываете окно терминала через Launcher.
Linux
В linux, основанном на Debian, просто откроем терминал и пишем:
После этого необходимо закрыть окно терминала и открыть его снова, чтобы загрузились новые параметры среды.
> sudo apt-get install build-essential
После этого необходимо закрыть окно терминала и открыть его снова, чтобы загрузились новые параметры среды.
Итогом в каждой операционной системе должно быть то, что вы вводите в командной строке
> g++
и в ответ видите что-то вроде
g++: fatal error: no input files
Такое сообщение означает, что компилятор готов к бою и жаждет получить исходные файлы.
1. Avian
Для начала предложим нашему g++ собрать Avian.
Открываем: oss.readytalk.com/avian. Выбираем ссылку status. На открывшейся странице скачиваем Avian 0.6. Несмотря на скромный номер версии, программа совершенно стабильна (во всяком случае, мне ни разу не удалось уронить ее, а в их багтрекере значатся весьма заковыристые баги, означающие высокую стабильность того, что есть).
Распакуем исходник Avian, скачанный нами, в некоторую папку (пусть это будет ~/Projects).
> cd ~/Projects
Под Windows папка "~" в среде MSYS присоединяется не к домашней папке пользователя, а к папке C:\MinGW64\msys\home\username. В нашем случае, раз мы хотим максимально дистанцироваться от платформы, это даже преимущество.
Допустим, скачанный архив называется avian-0.6.tar.bz2 и лежит в ~/Downloads, тогда распаковываем его в текущую папку, набрав
> tar -xjf ~/Downloads/avian-0.6.tar.bz2
В Windows путь необходимо указывать в формате mingw с прямыми слешами в виде /c/Users/username/Downloads/avian-0.6.tar.bz2. Разумеется, вы можете воспользоваться одним из ста альтернативных способов распаковать архив — главное, чтобы он попал в текущую папку. В итоге в ней появится распакованная из архива подпапка avian. Зайдем в нее:
> cd avian
Теперь можно попробовать запустить команду make, но, если запустить сборку прямо сейчас, скорее всего, мы получим сообщение о том, что не найден zlib. Что-нибудь вроде zlib.h: No such file or directory.
Windows
Под Windows придется слегка попотеть и воспользоваться методом, предложенным авторами Avian, а именно — подсунуть библиотеку zlib из их специального вспомогательного репозитария win64. Для этого вам потребуется установленный в системе git. К счастью, git входит в установленную нами сборку msys. К сожалению, на момент написания этой статьи сборка сделана криво — в ней не хватает файла msys-crypto-0.9.8.dll, который придется найти в Google и положить рядом с его бесполезным братом msys-crypto-1.0.0.dll, которым эта сборка укомплектована.
Далее необходимо из папки avian выполнить команду
которая положит рядом с папкой avian папку win64 со всеми библиотеками, которые могут понадобиться avian-у, в частности, с zlib.
Далее необходимо из папки avian выполнить команду
> git clone git://oss.readytalk.com/win64.git ../win64
которая положит рядом с папкой avian папку win64 со всеми библиотеками, которые могут понадобиться avian-у, в частности, с zlib.
OS X
Под OS X, насколько я помню, эта библиотека устанавливается автоматически (возможно, с инструментарием разработчика, который мы уже поставили).
Linux
Под linux эта проблема решается простым
> sudo apt-get install zlib1g-dev
Однако, установив zlib и снова набрав
make
, мы получим еще одну ошибку — сборщик Avian не находит программу /bin/javac. Java-разработчики, вероятно, узнают эту программу — это компилятор Java. Так как Avian — только виртуальная машина, компилятор мы по-прежнему используем официальный — от Oracle. При сборке самой VM он нужен для того, чтобы собрать из исходных java-файлов классы маленькой стандартной библиотеки Avian, такие, например, как System
, ArrayList
или HashMap
. Соответственно, на машине разработчика всё равно должна стоять JDK — как при сборке Avian, так и при сборке приложений, которые будут его использовать. Причем ставить желательно JDK7, с которой совместим Avian 0.6. Пользователю ваших приложений она, как и JRE, будет уже не нужна (собственно, ради этого и стараемся).Windows
В Windows идём на сайт Oracle и качаем нужный дистрибутив, а затем устанавливаем.
OS X
В OS X, как и в Windows, идём на сайт Oracle и качаем нужный дистрибутив, а затем устанавливаем.
Linux
В Linux обходимся привычной мантрой
(Возможно, в вашем дистрибутиве не будет OpenJDK или пакет будет называться как-то иначе. Но Linux есть Linux — ищите и обрящете.)
> sudo apt-get install openjdk-7-jdk
(Возможно, в вашем дистрибутиве не будет OpenJDK или пакет будет называться как-то иначе. Но Linux есть Linux — ищите и обрящете.)
Для того, чтобы увидеть, куда вы только что положили ваше java-окружение для разработчиков, make читает переменную среды JAVA_HOME, которую нам сейчас надо правильно задать. Эта же самая переменная с тем же значением потребуется нам впоследствии для того, чтобы собирать наш собственный проект.
Windows
Ваш путь в MinGW под Windows, скорее всего, будет что-то вроде /c/Program\ Files/Java/jdk1.7.0_07/ (вы его указывали при установке JDK7).
OS X
Под OS X ваша JDK7 установится в /Library/Java/JavaVirtualMachines/jdk1.7.0_17.jdk/Contents/Home
Linux
У меня в Linux это выглядит так:
> update-java-alternatives -l
java-1.7.0-openjdk-amd64 1071 /usr/lib/jvm/java-1.7.0-openjdk-amd64
> export JAVA_HOME=/usr/lib/jvm/java-1.7.0-openjdk-amd64
И вот, наконец, звёздный час — мы собираем Avian:
> make
Если вы всё проделали правильно, вы увидите последовательность строчек вида compiling build/<имя_вашей_платформы>/<какой-то файл>, а затем linking build/<имя_вашей_платформы>/<какой-то файл>. По окончании сборочного процесса мы получим много файлов в папке build/<имя_вашей_платформы>, но заинтересуют нас отсюда только:
- classpath.jar — та самая маленькая библиотека базовых классов, которую вы будете использовать в своих программах. Существует возможность собрать Avian с использованием библиотеки OpenJDK, но, во-первых, как я понимаю, у нее менее добрая лицензия, а во-вторых, она существенно тяжелее.
- binaryToObject(.exe) — необходимая для встраивания Avian в ваши приложения утилита, назначение которой будет раскрыто позже.
- libavian.a — самый главный файл. Это и есть, собственно, встраиваемая виртуальная машина. У меня он занимает аж 23 мегабайта, но не пугайтесь. Он заметно похудеет после некоторых несложных и, что важно, совершенно безвредных и автоматических манипуляций, суть которых также будет раскрыта позднее.
2. Кроссплатформенный независимый монолитный привет на Java
Мы скомпилировали весь необходимый сторонний код. Теперь займёмся созданием своего собственного.
Задача в том, чтобы сделать программу, которая будет написана на Java, которая при этом будет содержать как можно меньше платформозависимых закладок и которая будет собираться в один exe-файл, не требующий специальной установки сам по себе и работающий на любой «чистой» системе без установки каких-либо зависимостей.
2.1. Немного про JNI
Начнем, пожалуй, с теории. Обсудим, как JVM взаимодействует с системой. Любая виртуальная машина создается, в первую очередь, для того, чтобы абстрагироваться от внешней среды. Поэтому неудивительно, что самым узким местом в реализации VM является как раз вызов системных функций. Современная программа не может даже «чихнуть» без участия ОС. Читать/писать на диск — системная функция. Вывод текста в консоль — системная функция. Нарисовать окошко на экране — а вы сами как думаете?
Фактически, единственное, что приложение может делать «внутри себя» — это расчеты и принятие решений. Именно эти действия — арифметика и логика — являются функциями VM. Как только надо сделать что-то еще, она зовёт внешнюю среду. Но как? В случае Java для этого существует JNI (Java Native Interface). Суть его весьма проста. Программа, написанная на Java, содержит в себе заголовок функции, помеченный модификатором native. Например,
package packagename;
{
class ClassName
{
void native foo();
}
}
Такая функция понимается компилятором Java как функция, вызываемая из загруженных библиотек обычного (не виртуального) кода. В одной из этих библиотек должно быть что-то типа
extern "C" JNIEXPORT void JNICALL Java_packagename_ClassName_foo(JNIEnv * env, jobject caller)
{
…
}
При вызове в Java-коде функции
foo()
мы фактически вызываем функцию из native-библиотеки, передав ей указатель на среду JNIEnv
— объект, позволяющий «общаться» с данными и кодом внутри VM, и указатель на объект, из которого вызвана функция, — jobject caller
(если бы функция была статической, вместо дескриптора объекта здесь бы был дескриптор класса jclass caller_class
). Людям, хорошо знакомым с Java, но не изучавшим JNI, можно объяснить этот принцип взаимодействия так: JNI позволяет внешнему native-коду выполнять рефлексию над программой на Java. Если хотите изучить эту технологию подробнее, милости прошу в специальный раздел на официальном сайте Oracle.2.2. JNI «наборот»
Зачем был весь этот ликбез? Затем, что в данный момент перед нами стоит весьма занятная, почти обратная, задача. Нам надо запустить native-исполняемый файл, который, будучи статически слинкован с библиотекой libavian.a, будет содержать JVM прямо внутри себя. Помимо этого, он будет содержать внутри себя все необходимые java-классы, включая и «точку входа» — класс вида
class Application
{
public static void main(String... args)
{
…
}
}
Звучит это всё довольно пугающе, однако задача эта вполне простая. Необходимо написать довольно несложный код на C, который вытащит библиотеку классов Avian (с добавленным в нее нашим классом
Application
) изнутри собственного бинарного файла и скормит ее JVM вместе с параметрами командной строки с помощью всё того же JNI. Затем мы линкуем этот C-файл специальным образом, чтобы всё оказалось на своих местах, и наслаждаемся результатом.2.3. Новый проект и библиотеки
Сейчас мы притащим и разложим по полочкам все нужные нам для дальнейшей работы компоненты. То, что я буду описывать здесь — это мой собственный подход. Разумеется, вы вольны сделать всё иначе, так как вам заблагорассудится. Но если вы хотите в итоге получить в точности то, что я выложил на GitHub (ссылка будет в конце), постарайтесь делать всё в точности.
Создаем папку crossbase где захотим (я создал ее в Projects, рядом с avian и win32)
> mkdir crossbase && cd crossbase
Внутри создаем подпапку libs
> mkdir lib && cd lib
Внутри создаем подпапку с именем вашей текущей OS. Им должно быть «linux», «win32» или «osx».
> mkdir win-x86_64 && cd win-x86_64
В эту папку необходимо скопировать libavian.a, который мы собрали ранее. У меня это выглядит так:
> cp ../../../avian/build/windows-i386/libavian.a ./
Кроме того, в системе Windows, где нет zlib, в эту же папку придется скопировать еще и libz.a:
> cp ../../../win-x86_64/lib/libz.a ./
Таким образом, мы собрали минимум необходимых нам библиотек. Этого хватит для простейшей программы.
Помимо библиотек, нам понадобится classpath.jar, который также был собран вместе с avian.
> cd ..
> mkdir java && cd java
> cp ../../../avian/build/windows-i386/classpath.jar ./
И теперь пришло время раскрыть назначение таинственного binaryToObject. Он нужен нам, чтобы преобразовать наш jar-файл в специальный объектный файл, который затем будет передан линковщику и добавлен им в нашу программу. Так как эта процедура должна выполняться при каждой сборке, его тоже надо утащить в наш новый проект.
> cd ../..
(мы снова в папке crossbase, где мы создали lib)
> mkdir -p tools/win-x86_64 && cd tools/win-x86_64
Имя win-x86_64 назначено внутренней папке по тому же принципу, что и в прошлый раз. Кидаем сюда binaryToObject. (в Windows он, разумеется, имеет расширение exe)
> cp ../../../avian/build/windows-i386/binaryToObject/binaryToObject.exe ./
Можно запустить его и увидеть usage:
usage: c:\Users\imizus\Projects\crossbase\crossbase\tools\win32\binaryToObject.exe <input file> <output file> <start name> <end name> <platform> <architecture> [<alignment> [{writable|executable}...]]
2.4. Код программы
А теперь приступим к написанию кода. Создадим новый исходный файл на C++ (вы можете воспользоваться любым текстовым редактором, какой вам нравится, я использую eclipse, в котором можно редактировать и C++, и Java в рамках одного проекта, хотя для этого его придется немного настроить).
> mkdir -p src/cpp && cd src/cpp
Внутри создаем файл main.cpp со следующим содержанием (приведу его целиком, а потом объясню, что там к чему):
#include <stdint.h>
#include <string.h>
#ifdef __MINGW32__
#include <windows.h>
#endif
#include <jni.h>
#if (defined __MINGW32__)
# define EXPORT __declspec(dllexport)
#else
# define EXPORT __attribute__ ((visibility("default"))) \
__attribute__ ((used))
#endif
#if (! defined __x86_64__) && (defined __MINGW32__)
# define SYMBOL(x) binary_boot_jar_##x
#else
# define SYMBOL(x) _binary_boot_jar_##x
#endif
extern "C"
{
extern const uint8_t SYMBOL(start)[];
extern const uint8_t SYMBOL(end)[];
EXPORT const uint8_t* bootJar(unsigned* size)
{
*size = SYMBOL(end) - SYMBOL(start);
return SYMBOL(start);
}
} // extern "C"
int main(int argc, const char** argv)
{
#ifdef __MINGW32__
// For Windows: Getting command line as a wide string
int wac = 0;
wchar_t** wav;
wav = CommandLineToArgvW(GetCommandLineW(), &wac);
#else
// For other OS: Getting command line as a plain string (encoded in UTF8)
int wac = argc;
const char** wav = argv;
#endif
JavaVMInitArgs vmArgs;
vmArgs.version = JNI_VERSION_1_2;
vmArgs.nOptions = 1;
vmArgs.ignoreUnrecognized = JNI_TRUE;
JavaVMOption options[vmArgs.nOptions];
vmArgs.options = options;
options[0].optionString = const_cast<char*>("-Xbootclasspath:[bootJar]");
JavaVM* vm;
void* env;
JNI_CreateJavaVM(&vm, &env, &vmArgs);
JNIEnv* e = static_cast<JNIEnv*>(env);
jclass c = e->FindClass("crossbase/Application");
if (not e->ExceptionCheck())
{
jmethodID m = e->GetStaticMethodID(c, "main", "([Ljava/lang/String;)V");
if (not e->ExceptionCheck())
{
jclass stringClass = e->FindClass("java/lang/String");
if (not e->ExceptionCheck())
{
jobjectArray a = e->NewObjectArray(wac - 1, stringClass, 0);
if (not e->ExceptionCheck())
{
for (int i = 1; i < wac; ++i)
{
#ifdef __MINGW32__
// For Windows: Sending wide string to Java
int arglen = wcslen(wav[i]);
jstring arg = e->NewString((jchar*) (wav[i]), arglen);
#else
// For other OS: Sending UTF8-encoded string to Java
int arglen = strlen(wav[i]);
jstring arg = e->NewStringUTF((char*) (wav[i]));
#endif
e->SetObjectArrayElement(a, i - 1, arg);
}
e->CallStaticVoidMethod(c, m, a);
}
}
}
}
int exitCode = 0;
if (e->ExceptionCheck())
{
exitCode = -1;
e->ExceptionDescribe();
}
vm->DestroyJavaVM();
return exitCode;
}
__MINGW32__
— символ препроцессора, который (как неожиданно!) автоматически задается внутри среды MinGW32. Он позволяет нам отличить Windows, который, как вы, думаю, уже успели заметить, сильно непохож на все прочие системы. В частности, только под Windows нам понадобится специальная системная API, которую мы подключаем строкой #include <windows.h>
. На остальных платформах мы обходимся стандартными библиотеками POSIX и ANSI C++. Зачем понадобится? Станет ясно чуть позже. Будем просматривать код по порядку.#if (defined __MINGW32__)
# define EXPORT __declspec(dllexport)
#else
# define EXPORT __attribute__ ((visibility("default"))) \
__attribute__ ((used))
#endif
Этот код знаком и понятен всем, кто писал кроссплатформенные динамические библиотеки с использованием gcc. Суть его в том, что в разных операционных системах по-разному описываются функции, которые должны быть экспортированы из библиотеки. «Причем тут динамическая библиотека, ведь мы же исполняемый файл собираем?» — спросите вы. В ответ я напомню, что взаимодействие Avian с платформозависимым кодом осуществляется через механизм JNI, подразумевающий вызов функции из библиотеки. Иными словами, для вашего Java-кода исполняемый файл это не только пусковая программа, а еще и динамическая библиотека функций.
Следующая часть — это странноватая магия:
#if (! defined __x86_64__) && (defined __MINGW32__)
# define SYMBOL(x) binary_boot_jar_##x
#else
# define SYMBOL(x) _binary_boot_jar_##x
#endif
extern "C"
{
extern const uint8_t SYMBOL(start)[];
extern const uint8_t SYMBOL(end)[];
EXPORT const uint8_t* bootJar(unsigned* size)
{
*size = SYMBOL(end) - SYMBOL(start);
return SYMBOL(start);
}
} // extern "C"
Давайте разберемся. Мы декларируем некую экспортную функцию (посмотрим на
extern "C"
и директиву EXPORT
, которую мы только что ввели. Имя функции — bootJar
. Запомним это имя и посмотрим, что она делает. Если мысленно разобрать директивы препроцессора, то увидим, что она вычисляет расстояние между некими _binary_boot_jar_start и _binary_boot_jar_end (В MinGW32 они не будут иметь подчерк вначале). Сами эти символы декларированы как extern
, то есть их должен подставить линковщик. Загадочная деятельность, не правда ли?На самом деле, как мы увидим ниже, всё довольно просто, если знаешь, что делать. Так как Avian разрабатывался для встраивания его в приложения, авторы предусмотрели возможность добавления библиотеки классов непосредственно в исполняемый файл с последующей ее загрузкой оттуда. Для этого надо всего лишь преобразовать библиотеку в объектный файл. Да-да, я тоже поначалу удивился, но это очень элегантная идея. В объектном файле, содержащем наш jar, когда мы его создадим, будет декларировано 2 символа, указывающих на начало (
_binary_boot_jar_start
) и конец (_binary_boot_jar_end
) этого jar-файла. А функция bootJar
будет использована Avian-ом, чтобы узнать, где он начинается и какую длину имеет. Забегая вперед, скажу, что имя этой функции передается строкойoptions[0].optionString = const_cast<char*>("-Xbootclasspath:[bootJar]");
Наконец-то мы дошли до точки входа — функции
main
. В ее задачу входит:- Считать строку параметров
- Загрузить Avian, передав ему библиотеку классов
- Вызвать функцию main из класса crossbase.Application, передав ей параметры командной строки
- Красиво вылететь с ошибкой, если что-либо из вышеперечисленного не удастся
Поехали с начала фцнкции:
#ifdef __MINGW32__
// For Windows: Getting command line as a wide string
int wac = 0;
wchar_t** wav;
wav = CommandLineToArgvW(GetCommandLineW(), &wac);
#else
// For other OS: Getting command line as a plain string (encoded in UTF8)
int wac = argc;
const char** wav = argv;
#endif
Здесь как всегда отличился Windows. Когда повсеместно было принято решение переходить от старых неудобных однобайтных кодировок к более сложным, все ОС перешли к удобной UTF-8, а любимое детище Microsoft перешло на фиксированную двухбайтную. При этом они вообще не позаботились о том, какая кодировка используется, например, в именах файлов. Но кодировка нас сейчас тоже не очень заботит. Нам надо передать строку параметров в Java (в которой тоже принят двухбайтный
char
). Поэтому для Windows мы вызываем API-функцию (ради которой мы и тащили windows.h), которая выдаст нам строку параметров в правильной двухбайтной кодировке. Так мы получим возможность, например, открывать файлы с кириллицей в названии. Во всех прочих системах мы просто читаем параметры из аргументов функции main
.Далее следует создание виртуальной машины Java:
JavaVMInitArgs vmArgs;
vmArgs.version = JNI_VERSION_1_2;
vmArgs.nOptions = 1;
vmArgs.ignoreUnrecognized = JNI_TRUE;
JavaVMOption options[vmArgs.nOptions];
vmArgs.options = options;
options[0].optionString = const_cast<char*>("-Xbootclasspath:[bootJar]");
JavaVM* vm;
void* env;
JNI_CreateJavaVM(&vm, &env, &vmArgs);
JNIEnv* e = static_cast<JNIEnv*>(env);
Еще мы вытаскиваем указатель на объект
JNIEnv
, который будем использовать, чтобы командовать только что созданной Java-машиной.Дальнейший код читается как стих Маяковского, если только немного знать JNI.
jclass c = e->FindClass("crossbase/Application");
if (not e->ExceptionCheck())
{
jmethodID m = e->GetStaticMethodID(c, "main", "([Ljava/lang/String;)V");
if (not e->ExceptionCheck())
{
jclass stringClass = e->FindClass("java/lang/String");
if (not e->ExceptionCheck())
{
jobjectArray a = e->NewObjectArray(wac - 1, stringClass, 0);
if (not e->ExceptionCheck())
{
for (int i = 1; i < wac; ++i)
{
#ifdef __MINGW32__
// For Windows: Sending wide string to Java
int arglen = wcslen(wav[i]);
jstring arg = e->NewString((jchar*) (wav[i]), arglen);
#else
// For other OS: Sending UTF8-encoded string to Java
int arglen = strlen(wav[i]);
jstring arg = e->NewStringUTF((char*) (wav[i]));
#endif
e->SetObjectArrayElement(a, i - 1, arg);
}
e->CallStaticVoidMethod(c, m, a);
}
}
}
}
int exitCode = 0;
if (e->ExceptionCheck())
{
exitCode = -1;
e->ExceptionDescribe();
}
Возьмём класс
crossbase/Application
. Если смогли, найдем в нем статический метод main
с сигнатурой ([Ljava/lang/String;)V
. Если смогли, достанем из стандартной библиотеки класс java/lang/String
. Если смогли, создадим массив объектов этого класса (они и будут параметрами). Если смогли, то во всех операционных системах создаем java-строку из каждого параметра, заданного в кодировке UTF-8
, а в Windows создаем напрямую, используя двухбайтное представление.Если мы что-нибудь не смогли, выдаем ошибку пользователю.
Вот, собственно, и весь «пусковой механизм». Теперь нам надо создать нашу программу на Java. Она, как минимум, должна содержать класс
crossbase.Application
с методом public static void main(String... args)
. Создадим в нашей папке crossbase/src подпапку java, в ней — подпапку crossbase (это — имя пакета), а внутри создадим файл Application.java следующего содержания:
package crossbase;
public class Application
{
public static void main(String... args)
{
System.out.println("This is a crossplatform monolith application with Java code inside. Freedom to Java apps!");
for (int i = 0; i < args.length; i++)
{
System.out.println("args[" + i + "] = " + args[i]);
}
}
}
Если вы хоть немного знаете Java, то, думаю, комментарии здесь излишни. Скажу только, что в стандартной библиотеке классов Avian нету средств форматирования строк (которые никто не мешает тихонько утянуть, к примеру, из OpenJDK).
2.5. Сборка
Теперь перейдем к задаче сборки нашего проекта. Я использую make, потому что он есть всегда и везде, где есть gcc. А еще он достаточно мощный, чтобы написать на нем почти любую автоматизированную систему сборки. Нет, правда. Можно по пальцам перечислить, что мне не удавалось сделать на make и это едва ли были жизненно важные вещи. Наш Makefile будет лежать прямо в папке crossbase и выглядеть он будет вот так:
UNAME := $(shell uname)
ARCH := $(shell uname -m)
SRC = src
BIN = bin
OBJ = obj
JAVA_SOURCE_PATH = $(SRC)/java
JAVA_CLASSPATH = $(BIN)/java
CPP_SOURCE_PATH = $(SRC)/cpp
OBJECTS = $(OBJ)
DEBUG_OPTIMIZE = -O3 #-O0 -g
ifeq ($(UNAME), Darwin) # OS X
PLATFORM_ARCH = darwin x86_64
PLATFORM_LIBS = osx-x86_64
PLATFORM_GENERAL_INCLUDES = -I"$(JAVA_HOME)/include" -I"$(JAVA_HOME)/include/darwin"
PLATFORM_GENERAL_LINKER_OPTIONS = -framework Carbon
PLATFORM_CONSOLE_OPTION =
EXE_EXT=
STRIP_OPTIONS=-S -x
RDYNAMIC=-rdynamic
else ifeq ($(UNAME) $(ARCH), Linux x86_64) # linux on PC
PLATFORM_ARCH = linux x86_64
PLATFORM_LIBS = linux-x86_64
PLATFORM_GENERAL_INCLUDES = -I"$(JAVA_HOME)/include" -I"$(JAVA_HOME)/include/linux"
PLATFORM_GENERAL_LINKER_OPTIONS = -lpthread -ldl
PLATFORM_CONSOLE_OPTION =
EXE_EXT=
STRIP_OPTIONS=--strip-all
RDYNAMIC=-rdynamic
else ifeq ($(OS), Windows_NT) # Windows
PLATFORM_ARCH = windows x86_64
PLATFORM_LIBS = win-x86_64
PLATFORM_GENERAL_INCLUDES = -I"$(JAVA_HOME)/include" -I"$(JAVA_HOME)/include/win32"
PLATFORM_GENERAL_LINKER_OPTIONS = -static -lmingw32 -lmingwthrd -lws2_32 -mwindows -static-libgcc -static-libstdc++
PLATFORM_CONSOLE_OPTION = -mconsole
EXE_EXT=.exe
STRIP_OPTIONS=--strip-all
RDYNAMIC=
endif
JAVA_FILES = $(shell cd $(JAVA_SOURCE_PATH); find . -name \*.java | awk '{ sub(/.\//,"") }; 1')
JAVA_CLASSES := $(addprefix $(JAVA_CLASSPATH)/,$(addsuffix .class,$(basename $(JAVA_FILES))))
CPP_FILES = $(shell cd $(CPP_SOURCE_PATH); find . -name \*.cpp | awk '{ sub(/.\//,"") }; 1')
CPP_OBJECTS := $(addprefix $(OBJECTS)/,$(addsuffix .o,$(basename $(CPP_FILES))))
all: $(BIN)/crossbase
$(JAVA_CLASSPATH)/%.class: $(JAVA_SOURCE_PATH)/%.java
@echo $(PLATFORM_GENERAL_INCLUDES)
if [ ! -d "$(dir $@)" ]; then mkdir -p "$(dir $@)"; fi
"$(JAVA_HOME)/bin/javac" -sourcepath "$(JAVA_SOURCE_PATH)" -classpath "$(JAVA_CLASSPATH)" -d "$(JAVA_CLASSPATH)" $<
$(OBJ)/%.o: $(SRC)/cpp/%.cpp
@echo $(PLATFORM_GENERAL_INCLUDES)
mkdir -p $(OBJ)
g++ $(DEBUG_OPTIMIZE) -D_JNI_IMPLEMENTATION_ -c $(PLATFORM_GENERAL_INCLUDES) $< -o $@
$(BIN)/crossbase: $(JAVA_CLASSES) $(CPP_OBJECTS)
mkdir -p $(BIN);
@echo $(PLATFORM_GENERAL_INCLUDES)
# Extracting libavian objects
( \
cd $(OBJ); \
mkdir -p libavian; \
cd libavian; \
ar x ../../lib/$(PLATFORM_LIBS)/libavian.a; \
)
# Making the java class library
cp lib/java/classpath.jar $(BIN)/boot.jar; \
( \
cd $(BIN); \
"$(JAVA_HOME)/bin/jar" u0f boot.jar -C java .; \
)
# Making an object file from the java class library
tools/$(PLATFORM_LIBS)/binaryToObject $(BIN)/boot.jar $(OBJ)/boot.jar.o _binary_boot_jar_start _binary_boot_jar_end $(PLATFORM_ARCH); \
g++ $(RDYNAMIC) $(DEBUG_OPTIMIZE) -Llib/$(PLATFORM_LIBS) $(OBJ)/boot.jar.o $(CPP_OBJECTS) $(OBJ)/libavian/*.o $(PLATFORM_GENERAL_LINKER_OPTIONS) $(PLATFORM_CONSOLE_OPTION) -lm -lz -o $@
strip $(STRIP_OPTIONS) $@$(EXE_EXT)
clean:
rm -rf $(OBJ)
rm -rf $(BIN)
.PHONY: all
Будьте осторожны! Не путайте табуляции с пробелами, в make табуляцией выделяются команды внутри правила сборки, а пробел синтаксическим элементом не является. Присмотримся немного, как он работает. Единственная более-менее мозгодробительная конструкция — назначение вот этих переменных:
JAVA_FILES = $(shell cd $(JAVA_SOURCE_PATH); find . -name \*.java | awk '{ sub(/.\//,"") }; 1')
JAVA_CLASSES := $(addprefix $(JAVA_CLASSPATH)/,$(addsuffix .class,$(basename $(JAVA_FILES))))
CPP_FILES = $(shell cd $(CPP_SOURCE_PATH); find . -name \*.cpp | awk '{ sub(/.\//,"") }; 1')
CPP_OBJECTS := $(addprefix $(OBJECTS)/,$(addsuffix .o,$(basename $(CPP_FILES))))
Здесь мы с помощью unix-команды find отыскиваем все файлы
.java
в папке $(JAVA_SOURCE_PATH)
. Эти файлы нам предстоит компилировать. Далее мы откусываем от них расширение и заменяем его на .class
, а пусть заменяем на $(JAVA_CLASSPATH)
, получая таким образом имена файлов классов, которые надо получить, т.е. имена целей. Аналогично мы поступаем с файлами .cpp
и .o
. Далее в makefile мы видим следующие правила сборки:$(JAVA_CLASSPATH)/%.class: $(JAVA_SOURCE_PATH)/%.java
@echo $(PLATFORM_GENERAL_INCLUDES)
if [ ! -d "$(dir $@)" ]; then mkdir -p "$(dir $@)"; fi
"$(JAVA_HOME)/bin/javac" -sourcepath "$(JAVA_SOURCE_PATH)" -classpath "$(JAVA_CLASSPATH)" -d "$(JAVA_CLASSPATH)" $<
$(OBJ)/%.o: $(SRC)/cpp/%.cpp
@echo $(PLATFORM_GENERAL_INCLUDES)
mkdir -p $(OBJ)
g++ $(DEBUG_OPTIMIZE) -D_JNI_IMPLEMENTATION_ -c $(PLATFORM_GENERAL_INCLUDES) $< -o $@
Эти правила объясняют, как скомпилировать исходный файл в целевой. И, наконец, взглянем на цель
$(BIN)/crossbase: $(JAVA_CLASSES) $(CPP_OBJECTS)
...
Здесь мы видим зависимость от всех найденных файлов. То есть makefile написан таким образом, чтобы собирать все java и cpp файлы, предложенные ему в правильных папках.
Остальные существенные моменты:
-static-libgcc
и-static-libstdc++
необходимы в mingw, чтобы собираемый файл содержал в себе стандартные библиотеки C и C++. В противном случае он будет слинкован с ними динамически и потребует таскать за собой пару DLL.-mconsole
нужен в системе Windows, чтобы система выдала программе консольный ввод-вывод при запуске. Этот параметр для GUI-приложения надо убрать.- Опция
-rdynamic
не поддерживается gcc под Windows в силу особенностей платформы.
Пробежимся вскользь по основному правилу сборки —
$(BIN)/crossbase: $(JAVA_CLASSES) $(CPP_OBJECTS)
. Сперва мы распаковываем все объектные файлы из libavian.a, чтобы впоследствии передать их линковщику поименно. Поведение странное, но не бессмысленное. В Windows это решает какую-то странную проблему с линковкой (я не разобрался достаточно хорошо). Далее мы берем наш classpath.jar, добавляем к нему наши скомпилированные классы из bin/java и пакуем всё вместе в bin/boot.jar. Затем мы вызываем binaryToObject, который создает из нашего boot.jar объектный файл obj/boot.jar.o с символами _binary_boot_jar_start и _binary_boot_jar_end (которые мы импортировали в main.o). И, наконец, мы линкуем всё это безобразие вместе. И, наконец, выполняем волшебную команду strip, в параметрах которой, на этот раз, отличилась OS X, где они не такие, как в MinGW и в Linux. Назначение команды — выкинуть из исполняемого файла всякие левые символы. До ее отработки crossbase весит более 9 мегабайт, после — менее полутора.3. Момент триумфа
Зайдя в папку crossbase/bin, запускаем из консоли наш crossbase, передав ему параметры.
> ./crossbase Привет Хабр!
This is a crossplatform monolith application with Java code inside. Freedom to Java apps!
args[0] = Привет
args[1] = Хабр!
Получившийся у нас проект лежит на моём GitHub-е.
4. Итоги и смысл
Мне трудно оценить пользу от этой статьи. Если я хотя бы получу за нее инвайт, это будет значить, что она, по крайней мере, не безынтересна. Скажу только, что при кажущейся сложности, этот метод прекрасно окупается по сравнению с написанием программы, скажем, на чистом C++. Java становится очень удобна при разрастании проекта хотя бы до пары десятков классов. Даже если быть предельно аккуратном при написании кода на C++, всё равно остаются лазейки для чудовищно сложновылавливаемых ошибок. Поэтому управляющий код (не требующий суперпроизводительности) я бы всем советовал писать на Java. Код же, требующий максимальной скорости, можно написать на C++, а затем очень легко и аккуратно обернуть C++ класс Java классом. Возможно я еще напишу, как сделать это красиво и не напороться на грабли.
Изначально я планировал сделать в статье главу, посвященную добавлению к этому «бутерброду» кроссплатформенного пользовательского интерфейса SWT (того, который используется в Eclipse), но потом решил, что она будет слишком уж длинной и увесистой. Если господам читателям интересно, напишу об этом отдельно. Благодарю за внимание!
P.S.
Получив от хабровчан много отзывов, я доработал статью и программу. Спасибо всем за советы и поправки.