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