Разработка 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 и нативным кодом, про эвенты и многое другое(хоть это уже и более общие понятия, по которым найти информацию немного проще).

ссылка на оригинал статьи http://habrahabr.ru/post/252217/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *