Автономная кроссплатформенная монолитная программа на Java

в 15:15, , рубрики: Avian, c++, cross-platform, embedded, java, jni, jvm, makefile, Песочница, метки: , , , , , , ,

Я люблю 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 вы сможете запускать).
Автономная кроссплатформенная монолитная программа на Java

Разумеется, я не волшебник. Я только нашел один волшебный артефакт. Называется он Avian, лежит по адресу oss.readytalk.com/avian/ и представляет собой легковесную, но полноценную стороннюю реализацию JVM, о которой Oracle, возможно, даже не слышали. Он поддерживает кучу платформ и архитектур, имеет лицензию «бери и делай, что хочешь», и — нет, я не имею ни малейшего отношения к этому проекту, я даже не контрибьютор, я только научился им пользоваться и хочу поделиться этим могучим знанием с уважаемыми обитателями Хабра. Стоит также отметить, что он является JIT-компилятором, то есть имеет конкурентно-высокую производительность (хотя я пока что не измерял ее).

Avian можно встроить в ваше приложение вместе с его весьма урезанной, но терпимой по функционалу стандартной библиотекой классов, причем «потяжелеет» программа всего только на мегабайт с гаком. Давайте соберем такую программу вместе.

0. Среда

Для построения нам прежде всего понадобятся утилиты командной строки unix-разработчика, в частности — компилятор g++. Тут сложнее всего придется Windows-пользователям. Сам я под Windows использую MinGW32 с ее замечательной средой MSYS, эмулирующей unix-терминал. Компиляторы, входящие в MinGW32 32-битные, что в некотором роде ограничивает полученную программу. Я рассматриваю ее здесь исключительно потому, что привык к ней. Убежден, что собрать проект с использованием 64-битного gcc под Windows не составит труда.

Здесь и далее я спрятал платформозависимые инструкции под спойлеры для удобства.

Windows

Чтобы скачать MinGW32, идем на www.mingw.org/ и там, в разделе downloads (появляется страница SourceForge), кликаем на главную жирную ссылку Download mingw-get-inst-yyyymmdd.exe.

Это онлайн-инсталлятор, который притащит с того же сайта все необходимые нам компоненты. При установке вы выбираете целевую папку (пусть это будет C:MinGW), а также устанавливаемые компоненты. Нам обязательно потребуются: C Compiler, C++ Compiler, MinGW Developer Toolkit.

После установки всего перечисленного вы запускаете терминал MSYS через меню программ. Все дальнейшие действия мы будем делать из этого терминала, так как он, во-первых, поддерживает формат 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:MinGWmsys1.0homeusername. В нашем случае, раз мы хотим максимально дистанцироваться от платформы, это даже преимущество.

Допустим, скачанный архив называется 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 из их специального вспомогательного репозитария win32. Для этого вам потребуется (о, боже!) установленный в системе git. Вам придется устанавливать git в виде пакета msysgit под windows, который надо скачать из code.google.com/p/msysgit/. Я мог бы рассказать, как собрать версию git для mingw, чтобы работало всё вместе (сам я так и живу), но это уведет читателей слишком далеко от темы. Укажу цель: необходимо из папки avian выполнить команду

> git clone git://oss.readytalk.com/win32.git ../win32

которая положит рядом с папкой avian папку win32 со всеми библиотеками, которые могут понадобиться 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 и качаем нужный дистрибутив, а затем устанавливаем. Причем, так как мы используем mingw32, ставить будем 32-битную JDK.

OS X

Наиболее «родной» для OS X является JDK фирмы Apple. Вам надо зайти по своему Apple ID на сайт developer.apple.com/downloads, там в поиске ввести Java и скачать Java for OS X нужной вам версии.

Linux

В Linux обходимся привычной мантрой

> sudo apt-get install openjdk-7-jdk

(Возможно, в вашем дистрибутиве не будет OpenJDK или пакет будет называться как-то иначе. Но Linux есть Linux — ищите и обрящете.)

Для того, чтобы увидеть, куда вы только что положили ваше java-окружение для разработчиков, make читает переменную среды JAVA_HOME, которую нам сейчас надо правильно задать. Эта же самая переменная с тем же значением потребуется нам впоследствии для того, чтобы собирать наш собственный проект.

Windows

Ваш путь в MinGW32 под Windows, скорее всего, будет что-то вроде /c/Program Files (x86)/Java/jdk1.7.0_07/ (вы его указывали при установке JDK7).

OS X

Под OS X этот путь всегда (насколько мне известно) выглядит, как /Library/Java/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()</foo> мы фактически вызываем функцию из native-библиотеки, передав ей указатель на среду <code>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 win32 && cd win32

В эту папку необходимо скопировать libavian.a, который мы собрали ранее. У меня это выглядит так:

> cp ../../../avian/build/windows-i386/libavian.a ./

Кроме того, в системе Windows, где нет zlib, в эту же папку придется скопировать еще и libz.a:

> cp ../../../win32/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/win32 && cd tools/win32

Имя win32 назначено внутренней папке по тому же принципу, что и в прошлый раз. Кидаем сюда binaryToObject. (в Windows он, разумеется, имеет расширение exe)

> cp ../../../avian/build/windows-i386/binaryToObject/binaryToObject.exe ./

Можно запустить его и увидеть usage:

usage: c:UsersimizusProjectscrossbasecrossbasetoolswin32binaryToObject.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)

SRC = src
BIN = bin
OBJ = obj

DEBUG_OPTIMIZE = -O3 #-O0 -g

ifeq ($(UNAME), Darwin)	# OS X
  PLATFORM_ARCH = darwin x86_64
  PLATFORM_LIBS = osx
  PLATFORM_GENERAL_INCLUDES = -I/System/Library/Frameworks/JavaVM.framework/Headers
  PLATFORM_GENERAL_LINKER_OPTIONS = -framework Carbon
  PLATFORM_CONSOLE_OPTION = 
  EXE_EXT=
  STRIP_OPTIONS=-S -x
  RDYNAMIC=-rdynamic
else ifeq ($(UNAME), Linux)	# linux
  PLATFORM_ARCH = linux x86_64
  PLATFORM_LIBS = linux
  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 i386
  PLATFORM_LIBS = win32
  PLATFORM_GENERAL_INCLUDES = -I"$(JAVA_HOME)/include" -I"$(JAVA_HOME)/include/win32"
  PLATFORM_GENERAL_LINKER_OPTIONS = -lmingw32 -lmingwthrd -lws2_32 -mwindows -static-libgcc -static-libstdc++
  PLATFORM_CONSOLE_OPTION = -mconsole
  EXE_EXT=.exe
  STRIP_OPTIONS=—strip-all
  RDYNAMIC=
endif

JAVA_CLASSES = $(BIN)/java/crossbase/Application.class
NATIVE_OBJECTS = $(OBJ)/main.o

all: $(BIN)/crossbase

$(BIN)/java/%.class: $(SRC)/java/%.java
        if [ ! -d "$(dir $@)" ]; then mkdir -p "$(dir $@)"; fi
        "$(JAVA_HOME)/bin/javac" -sourcepath "$(SRC)/java" -classpath "$(BIN)/java" -d $(BIN)/java $<

$(OBJ)/%.o: $(SRC)/cpp/%.cpp
        mkdir -p $(OBJ)
        g++ $(DEBUG_OPTIMIZE) -D_JNI_IMPLEMENTATION_ -c $(PLATFORM_GENERAL_INCLUDES) $< -o $@

$(BIN)/crossbase: $(JAVA_CLASSES) $(NATIVE_OBJECTS)
        mkdir -p $(BIN);

        # 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 $(NATIVE_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 табуляцией выделяются команды внутри правила сборки, а пробел синтаксическим элементом не является. Я не буду расписывать всё досконально, тем более что этот сборочный файл далек от совершенства (например, поиск исходных файлов лучше бы было осуществлять автоматически, а не вбивать все объектники в переменную NATIVE_OBJECTS). Перечислю лишь то, что важно.

  • -static-libgcc и -static-libstdc++ необходимы в mingw, чтобы собираемый файл содержал в себе стандартные библиотеки C и C++. В противном случае он будет слинкован с ними динамически и потребует таскать за собой пару DLL.
  • -mconsole нужен в системе Windows, чтобы система выдала программе консольный ввод-вывод при запуске. Этот параметр для GUI-приложения надо убрать.
  • Опция -rdynamic не поддерживается gcc под Windows в силу особенностей платформы.

Пробежимся вскользь по основному правилу сборки — $(BIN)/crossbase: $(JAVA_CLASSES) $(NATIVE_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), но потом решил, что она будет слишком уж длинной и увесистой. Если господам читателям интересно, напишу об этом отдельно. Благодарю за внимание!

Автор: bigfatbrowncat

Источник

Поделиться

* - обязательные к заполнению поля