GrabDuck

Разработка Air Native Extensions (ANE) для OS X

:

Привет всем хаброюзерам. Хотел бы поделиться опытом создания нативных расширений для OS X.

AIR — просто потрясающая в своей кроссплатформенности среда. Пока дело не доходит до использования каких-то уникальных для платформы фишек. Именно с этой проблемой я столкнулся, когда передо мной была поставлена задача превратить браузерную flash-игру в десктопную для OS X. Всё это с использованием среды AIR мной было сделано за несколько часов и я не буду описывать этот процесс, так как в гугле на эту тему полно информации. Самое интересное началось тогда, когда появилась необходимость подключить к игре различные сервисы Apple, такие как GameCenter, In-App-Purchase и т.д. И здесь я столкнулся с трудностями. Дело в том, что есть куча готовых ANE, в том числе и бесплатных. Но вся беда в том, что все эти решения работают только для iOS. Для OS X же нет ни то, что готовых библиотек, но даже информацию по созданию этих библиотек приходилось собирать по крупицам с пары-тройки интернет ресурсов многолетней давности, постоянно натыкаясь на какие-то подводные камни или даже айсберги.

Сейчас же я хочу собрать все накопленные знания и опыт в одном месте и поделиться с вами, чтобы хоть немного уменьшить ту боль, через которую вам придётся пройти, если всё таки вы тоже решитесь на создание нативных библиотек для мака. Хотя после четырёх разработанных расширений для OS X они не кажутся такими уж сложными и мудрёными.

Итак. Для работы я использовал:
AIR 16;
Flex 4.6.0;
Adobe Flash Builder 4.6 или IntelliJ IDEA 14(Flash Builder был использован для написания библиотеки, хотя тоже самое можно сделать и в IntelliJ IDEA. Но сам проект я разрабатывал в IntelliJ IDEA. Тут дело вкуса, полагаю);
Xcode 6.1.1;
OS X Yosemite(10.10.1);

Весь процесс создания ANE я разделю на 3 части.

Часть первая. Objective-C


Я считаю, что логичнее начинать создание нативных расширений с написания самого нативного кода, хотя в любом случае, скорее всего вам придётся возвращаться к изменению нативного кода не раз.

Начинаем с создания нового проекта в Xcode. File -> New -> Project… (Cmd+Shift+N). Далее выбираем OX X -> Framework & Library -> Cocoa Framwork.

Придумываем имя для нашего фреймворка. Имя может быть любым, в дальнейшем, оно никак не будет использоваться в нашей будущей нативной библиотеке.

После этого мы имеем пустой проект с одним заголовочным файлом.

Если нативная библиотека планируется для реализации несложных одиночных функций, которые так или иначе необходимо выполнить в Objective-C, то мы можем обойтись без заголовочного файла, используя только файл реализации (*.m). Но я опишу работу с полноценным классом.

Перед написанием кода необходимо добавить в проект библиотеку Adobe AIR.framework. Жмём правой кнопкой по проекту, и выбираем Add files to "...". Надеюсь у вас уже есть свежая версия среды AIR, ведь именно в ней хранится библиотека, которая нам нужна. Найти её можно здесь: ../AIR_FOLDER/runtimes/air/mac/Adobe AIR.framework.

После этого проект будет выглядеть как-то так:

Также нужно установить 32х битную целевую платформу (i386) для проекта (не для цели). На момент написания статьи Adobe AIR.framework работал только для 32х битных платформ. В тех же настройках проекта в Build Settings ищем automatic reference, и устанавливаем Objective-C Automatic Reference Counting на значение No.

Я ещё меняю пути выходных файлов, чтобы они были там же, где и исходники. Кому как удобнее.

В первую очередь нам необходимо определить инициализаторы(initializers) контекста и самой библиотеки (опционально можно также определить финализаторы(finalizers)).

Для начала определим инициализатор контекста. Он будет вызываться, как ни странно, при инициализации контекста в as3 части, но об этом позже. Очень важно, при использовании нескольких нативных библиотек в проекте, называть инициализаторы уникальными именами. Также в инициализаторе контекста определяются функции, которые будут доступны из as3 кода.

Итак. Объявляем инициализатор контекста следующим образом:

FREContext AirCtx = nil; //Глобальная переменная контекста

void MyAwesomeNativeExtensionContextInitializer(void* extData, const uint8_t* ctxType, FREContext ctx,
                                     uint32_t* numFunctionsToTest, const FRENamedFunction** functionsToSet)
{
    NSLog(@"[MyANE.Obj-C] Entering ContextInitinalizer()");
    
    *numFunctionsToTest = 1; //Количество функций, которые будут доступны из as3 кода. Очень важно чтобы число соответствовало реальному числу функций. Добавляем новую функцию - увеличиваем значение.
    
    FRENamedFunction* func = (FRENamedFunction*) malloc(sizeof(FRENamedFunction) * *numFunctionsToTest);
    
    func[0].name = (const uint8_t*) "initLibrary";		// Имя функции, по которому мы будем обращаться к ней из as3.
    func[0].functionData = NULL;				// Всегда NULL. Так и не нашёл случаев применения без NULL.
    func[0].function = &init;					// Ссылка на FREObject(функцию) в ojbective-c коде

//    Прочие функции

//    func[n].name = (const uint8_t*) "name";
//    func[n].functionData = data;
//    func[n].function = &function;
    
    *functionsToSet = func;
    
    AirCtx = ctx;
    
    NSLog(@"[MyANE.Obj-C] Exiting ContextInitinalizer()");
}

Стоит отметить, что функция NSLog, которая выводит сообщение в консоль, также будет выводить сообщение в виде trace в консоли IDE, в которой вы разрабатываете основной проект.

Теперь определим инициализатор самой библиотеки. В нём мы укажем ссылку на инциализатор и финализатор контекста. Его же мы будем использовать в дальнейшем при сборке библиотеки:

void MyAwesomeNativeExtensionInitializer(void** extDataToSet, FREContextInitializer* ctxInitializerToSet, FREContextFinalizer* ctxFinalizerToSet )
{
    NSLog(@"[MyANE.Obj-C] Entering ExtInitializer()");
    
    *extDataToSet = NULL;
    *ctxInitializerToSet = &MyAwesomeNativeExtensionContextInitializer;	// Инициализатор контекста
    *ctxFinalizerToSet = &MyAwesomeNativeExtensionContextFinalizer;	// Финализатор контекста(опционально)
    
    NSLog(@"[MyANE.Obj-C] Exiting ExtInitializer()");
}

Далее опишем нашу единственную функцию, доступную из кода action script. Внутри этой функции можем вызывать различные нативные методы Objective-C, в том числе используя iOS SDK:
FREObject (init) (FREContext context, void* functionData, uint32_t argc, FREObject argv[]){
    NSLog(@"[MyANE.Obj-C] Hello World!");
    return nil;
}

Для удобства можно использовать директиву:
#define DEFINE_ANE_FUNCTION(fn) FREObject (fn)(FREContext context, void* functionData, uint32_t argc, FREObject argv[])

Используя директиву, описанную выше, определить функцию можно намного проще и короче:
DEFINE_ANE_FUNCTION(init){
    NSLog(@"[MyANE.Obj-C] Hello World!");
    return nil;
}

Делаем билд(Command+B). В результате в пути, который мы указывали в самом начале должен был появиться фреймфорк, с именем, идентичными имени, которое мы указывали, опять же вначале.

Простейшая Objective-C библиотека готова. Единственное, что она может делать — это выводить в trace строку. Но для демонстрации работы сойдёт. Теперь нам нужно создать вторую половину нашей ANE — AS3 библиотеку.

Исходный код
MyANE.h
#import <Adobe AIR/Adobe AIR.h>
#import <Foundation/Foundation.h>

//! Project version number for MyANE.
FOUNDATION_EXPORT double MyANEVersionNumber;

//! Project version string for MyANE.
FOUNDATION_EXPORT const unsigned char MyANEVersionString[];

@interface MyANE : NSObject

@end


MyANE.m
#import "MyANE.h"

#define DEFINE_ANE_FUNCTION(fn) FREObject (fn)(FREContext context, void* functionData, uint32_t argc, FREObject argv[])

@implementation MyANE

@end

FREContext AirCtx = nil;

DEFINE_ANE_FUNCTION(init){
    NSLog(@"[MyANE.Obj-C] Hello World!");
    return nil;
}

void MyAwesomeNativeExtensionContextInitializer(void* extData, const uint8_t* ctxType, FREContext ctx,
                                     uint32_t* numFunctionsToTest, const FRENamedFunction** functionsToSet)
{
    NSLog(@"[MyANE.Obj-C] Entering ContextInitinalizer()");
    
    *numFunctionsToTest = 1;
    
    FRENamedFunction* func = (FRENamedFunction*) malloc(sizeof(FRENamedFunction) * *numFunctionsToTest);
    
    func[0].name = (const uint8_t*) "initLibrary";
    func[0].functionData = NULL;
    func[0].function = &init;
    
    *functionsToSet = func;
    
    AirCtx = ctx;
    
    NSLog(@"[MyANE.Obj-C] Exiting ContextInitinalizer()");
}

void MyAwesomeNativeExtensionContextFinalizer(FREContext ctx) {

}

void MyAwesomeNativeExtensionInitializer(void** extDataToSet, FREContextInitializer* ctxInitializerToSet, FREContextFinalizer* ctxFinalizerToSet )
{
    NSLog(@"[MyANE.Obj-C] Entering ExtInitializer()");
    
    *extDataToSet = NULL;
    *ctxInitializerToSet = &MyAwesomeNativeExtensionContextInitializer;
    *ctxFinalizerToSet = &MyAwesomeNativeExtensionContextFinalizer;
    
    NSLog(@"[MyANE.Obj-C] Exiting ExtInitializer()");
}

void MyAwesomeNativeExtensionFinalizer(void* extData)
{

}


Часть вторая. Action Script


Для создания библиотеки на Action Script можно использовать любую IDE, с возможностью разработки на ActionScript. Но я использовал стандартную для подобных целей IDE — Flash Builder.

Создаётся библиотека очень просто: Файл -> Создать -> Проект библиотеки Flex.

Обзываем нашу библиотеку, и обязательно подключаем библиотеки Adobe AIR. По сути делаем мы это для одного единственного класса, который позволит нам работать с контекстом.

Сразу создаём новый класс ActionScript(можно, и даже удобнее будет создать его в пакете по умолчанию), наследуя его от flash.events.EventDispatcher(в общем-то наследовать можно от чего угодно, а можно и вовсе не наследовать, но класс EventDispatcher позволит экземпляру диспатчить эвенты, что очень полезно при работе с iOS SDK, где некоторые запрошенные данные(список друзей GC, список доступных IAP) приходят не сразу). Это и будет наш основной класс, который мы будем использовать при работе с библиотекой.

В начале нам необходимо получить экзмепляр контекста. Делается это следующим образом:

var extCtx:ExtensionContext = ExtensionContext.createExtensionContext("my.awesome.native.extension", null);	

Статичный метод createExtensionContext создаёт экзмепляр ExtensionContext. Здесь мы должны передать id нашего расширения, в данном случае «my.awesome.native.extension», а также тип контекста. Тип необходимо указывать только в случае нескольких реализаций библиотеки. Если же планируется одна реализация, то в качестве типа можно передать null.

Одновременно в проекте может использоваться только один(singleton) экземпляр, контекста одного, конкретного типа. Лично у меня, после кучи созданных нативных расширений, так и не возникало необходимости в множественной реализации этого самого расширения. Вот и в данном случае, имея одну единственную реализацию, у нас будет в принципе один экзмепляр на всю ANE. Поэтому конструктор нужно вызвать один раз, а в дальнейшем просто получать уже созданный объект.

Самый простой вариант реализовать это — обращаться к некой статичной функции, которая будет возвращать экзмепляр объекта, или создавать новый, через конструктор, если такового нет.

Для начала опишем конструктор(который мы никогда не будем вызывать из проекта):

private static var _instance:MyANE;	// Статичный экземпляр класса
private var extCtx:ExtensionContext;	// Контекст

public function MyANE(target:IEventDispatcher=null) {
	if (!_instance) {
		if (this.isSupported) {
			extCtx = ExtensionContext.createExtensionContext("my.awesome.native.extension", null);		// Создание контекста
			if (extCtx != null) {
				trace('[MyANE.AS3] extCtx is okay');
			}
			else {
				trace('[MyANE.AS3] extCtx is null.');
			}
		}
		_instance = this;
	}
	else {
		throw Error('[MyANE.AS3] This is a singleton, use getInstance, do not call the constructor directly');	// Вызываем ошибку, если пытаемся вызвать конструктор
}
}

Также необходимо проверять, что ANE пытается запуститься на Mac.
public function get isSupported():Boolean {
	return Capabilities.manufacturer.indexOf('Macintosh') > -1;
}

Теперь опишем функцию, к которой мы будем обращаться каждый раз, когда нам будет необходимо получить экземпляр нашей библиотеки.
public static function getInstance():MyANE {
	return _instance != null ? _instance : new MyANE();
}

На этом этапе мы закончили инициализацию. Теперь можно использовать методы из Objective-C. Вызвать функцию из нативного кода можно методом класса экземпляра контекста call(), которому в качестве аргумента необходимо передать одно из имен функций, указанных в инициализаторе контекста в нативном коде, а также параметры функции. В этом примере у нас была описана только одна функция с именем «initLibrary». Она не принимает никаких параметров, ну мы и не передадим ничего.
public function init():void
{
	extCtx.call("initLibrary");
}

Сохраняем проект. Библиотека автоматически собриается, и по-умолчанию, помещается в директорию bin, в корне проекта.
Таким образом мы обеспечили самый базовый функционал. Теперь можно переходить к последней части.
Исходный код
package
{
	import flash.events.EventDispatcher;
	import flash.events.IEventDispatcher;
	import flash.external.ExtensionContext;
	import flash.system.Capabilities;
	
	public class MyANE extends EventDispatcher
	{
		private static var _instance:MyANE;	
		private var extCtx:ExtensionContext;

		public function MyANE(target:IEventDispatcher=null) {
			if (!_instance) {
				if (this.isSupported) {
					extCtx = ExtensionContext.createExtensionContext("my.awesome.native.extension", null);
					if (extCtx != null) {
						trace('[MyANE.AS3] extCtx is okay');
					}
					else {
						trace('[MyANE.AS3] extCtx is null.');
					}
				}
				_instance = this;
			}
			else {
				throw Error('[MyANE.AS3] This is a singleton, use getInstance, do not call the constructor directly');
		}
	}

	public function get isSupported():Boolean {
		return Capabilities.manufacturer.indexOf('Macintosh') > -1;
	}

	public static function getInstance():MyANE {
		return _instance != null ? _instance : new MyANE();
	}

	public function init():void
	{
		extCtx.call("initLibrary");
	}
}

Часть третья. Сборка библиотеки


Наконец у нас есть 2 куска нативной библиотеки. Всё что нужно — соединить их в полноценную ANE.

Для начала нам понадобится дескриптор, в котором мы опишем наше расширение. Он будет представлять из себя следующий *.xml файл:

<extension xmlns="http://ns.adobe.com/air/extension/3.9">
    <id>my.awesome.native.extension</id>
    <versionNumber>1.0.0</versionNumber>
    <platforms>
        <platform name="MacOS-x86">
            <applicationDeployment>
                <nativeLibrary>MyANE.framework</nativeLibrary>
                <initializer>MyAwesomeNativeExtensionInitializer</initializer>
                <finalizer>MyAwesomeNativeExtensionFinalizer</finalizer>
            </applicationDeployment>
        </platform>
        <platform name="default">
            <applicationDeployment/>
        </platform>
    </platforms>
</extension>

Здесь:
id — id расшинерия, который должен совпадать с id, который мы указывали при создании экземпляра контекста в as3 части.
nativeLibrary — собранный фреймворк из Objective-C
initializer, finalizer — инициализатор и финализатор библиотеки(не контекста), который также был описан в Ojbective-C части.

Также рекомендуется делать реализацию для дефолтной платформы, в которой отсутствует нативный код. Что же, последуем рекомендациям, это не сложно.

Последний кусочек нашей библиотеки готов, и теперь мы можем приступить к сборке. И вот тут начинается самое интересное.

Для удобства я бы советовал сделать отдельную папку для сборки, иначе будет просто путаница и каша, которой тут и без того хватает. Я ипсользую следующую структуру папок:


, где

  • _out — собственно папка для сборки.
    • default — реализация для платформы по-умолчанию
      • library.sfw — swf, полученная путём разархивирования собранной as3-части
    • mac — реализация для платформы mac
      • library.sfw — swf, полученная путём разархивирования собранной as3-части
      • MyANE.framework — собранная Objective-C-часть
    • extension.xml — дескриптор расширения
    • MakeANE.sh — просто скрипт для быстрой сборки библиотеки
  • ActionScript3 и Objective-C — папки проектов частей библиотеки.

Отдельно по library.sfw. Да, это кусок куска библиотеки, который должен быть отдельно, но при этом тот, собранный as3-кусок нам тоже необходим. Чтобы получить его, нужно разархивировать собранную as3 библиотеку как обычный zip-архив(сохранив эту самую as3 библиотеку).

Теперь всё, что нам нужно — это собрать расширение при помощи AIR Developer Tool (ADT). Найти его можно тут: ../AIR_FOLDER/bin/adt

Для сборки я использую следующий скрипт(из папки _out):
AIR_FOLDER/bin/adt -package -target ane MyANE.ane extension.xml -swc ../ActionSript3/bin/MyANE.swc -platform MacOS-x86 -C mac. -platform default -C default.

Теперь мы имеет готовый MyANE.ane файл, который и является собранной нативной библиотекой. Но даже это ещё не конец. Настоящее веселье начинается тогда, когда мы пытаемся использовать нативную библиотеку в OS X проекте. Опять же есть куча туториалов и всевозможных F.A.Q. для iOS, но, как оказалось, для OS X необходимо совершать иные ритуалы с бубном, и не только.

Часть последняя. Интеграция нативной библиотеки в проект


Итак, у нас есть собственноручно написанная библиотека. Вот он, готовый *.ane файл. Бери и пользуйся. Но нет. Для того, чтобы использовать нативную библиотеку в OS X во время разработки он не нужен. Но конечно-же наши усилия не были напрасными. Нам всего-то нужно сделать следующее(опишу процесс для IntelliJ IDEA, но для Flash Builder процесс аналогичный, в некоторых случаях даже проще):
  1. Разархивировать *.ane файл как обычный zip-архив в папку, которая имеет название в точности, как id нашего расширения + .ane в конце. В нашем случае это будет «my.awesome.native.extension.ane». Эту папку лучше скопировать в новую директорию внутри проекта. К примеру у меня это libs-ane, в которой уже лежат разархивированные расширения.
  2. В IntelliJ IDEA, в настройках проекта НЕ добавляем эту директорию в зависимости.
  3. В другую директорию внутри проекта добавляем собранную as3 библиотеку. У меня эта директория называется libs-swc.
  4. Эту директорию уже добавляем в зависимости проекта. Тип связи Merged.
  5. В параметрах запуска ADL необходимо добавить следующую опцию -extdir /ABSOLUTE_PATH_TO_PROJECT/libs-ane. В IntelliJ IDEA эти параметры находятся в Run->Edit Configurations->AIR Debug Launcher Options.
  6. В дескрипторе проекта добавить id нативного расширения в блоке «extensions»
    <extensions>
            <extensionID>my.awesome.native.extension</extensionID>
    </extensions>
    

Теперь мы можем при отладке использовать нативные расширения. Но есть ещё кое-что. Как вы наверное знаете, в iOS SDK есть ряд классов, которые будут корректно работать только при запуске их из Finder. Для этого при помощи той же IntelliJ IDEA можно собрать нативный бандл и использовать его. Но проблема в том, что предыдущий метод интеграции нативного расширения не позволит нам осуществить сборку бандла. Но сборка нам всё же может пригодиться, поэтому нам нужно ещё немного поработать. Помните наш *.ane? Так вот именно сейчас настало его время.
  1. Все *.ane необходимо добавить в очередную отдельную директорию, опять же внутри проекта. У меня эта папка называется anes.
    В IntelliJ IDEA, в настройках проекта также добавляем эту директорию в зависимости. Тип связи станет ANE и изменить его невозможно(именно поэтому невозможно одноверменно собирать бандл и работать в режиме отладки). В дальнейшем для отладки — убираем из зависимостей эту директорию, для сборки бандла — добавляем.
  2. Но в любом случае нам нужно, чтобы anes была внешней библиотекой. Для этого я использую дополнительный build-config.xml файл, в котором описываю дополнительные параметры билда. В этом build-config.xml необходимо указать директорию anes, как путь внешней библиотеки. Простейший вариант может выглядеть так:
    <?xml version="1.0"?>
    <flex-config>
    	<target-player>16.0.0</target-player>
    	<swf-version>23</swf-version>
    
    	<compiler>
    		<external-library-path>
    			<path-element>${flexlib}/libs/player/{targetPlayerMajorVersion}.{targetPlayerMinorVersion}/playerglobal.swc</path-element>
    			<path-element>anes</path-element>
    		</external-library-path>
    		<as3>true</as3>
    		<library-path>
    			<path-element>libs-swc</path-element>
    		</library-path>
    	</compiler>
    
    </flex-config>
    

    Чтобы использовать дополнительный билд-конфиг файл, необходимо добавить его в настройках проекта. Project Structure -> Additional compiler configuration file.

    Ну или ещё проще всё там же в Additional compiler options можно добавить параметр: "-external-library-path path-element anes"

Теперь можно собирать нативный бандл. Делается это просто Build->Package AIR Application. В качестве цели я использую *.app.

Ну и на выходе мы получим готовый, нативный бандл, с рабочим проектом, который будет использовать ANE.

Вот и всё. Спасибо за внимание, надеюсь эта статья для кого-то окажется полезной. Это моя первая статья на Хабре, поэтому очень хотелось бы услышать конструктивную критику и советы, как улучшить статью. Также обязательно буду отвечать на вопросы в комментариях, и, по возможности дополнять статью.

Если будет интерес к этой теме, то я бы хотел также рассказать про обмен различными данными между as3 и нативным кодом, про эвенты и многое другое(хоть это уже и более общие понятия, по которым найти информацию немного проще).