[Из песочницы] Разработка 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.

2b1ad5388e0040ae9800074ef74cec04.png

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

4dc8f68fb88e4a2d82c195774488c346.png

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

123387303c374d0ebd11a89f7c3ce603.png

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

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

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

ac904149a3384cb49fb7b48a8e29c6e2.png

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

da11e609940a4b8a814697f707ba7ec7.png9c24181aadf64cebb5908115af19785c.png

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

120cc12b30de4847b4a944f3f08eb900.png

В первую очередь нам необходимо определить инициализаторы (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 #import

//! 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. По сути делаем мы это для одного единственного класса, который позволит нам работать с контекстом.

65e8874ae8544b668717dba6f15b6723.png

Сразу создаём новый класс 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 файл:

my.awesome.native.extension 1.0.0 MyANE.framework MyAwesomeNativeExtensionInitializer MyAwesomeNativeExtensionFinalizer Здесь: id — id расшинерия, который должен совпадать с id, который мы указывали при создании экземпляра контекста в as3 части.nativeLibrary — собранный фреймворк из Objective-Cinitializer, finalizer — инициализатор и финализатор библиотеки (не контекста), который также был описан в Ojbective-C части.Также рекомендуется делать реализацию для дефолтной платформы, в которой отсутствует нативный код. Что же, последуем рекомендациям, это не сложно.

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

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

bb0c4525df4f41c4b0d667e47435be96.png, где

_out — собственно папка для сборки.default — реализация для платформы по-умолчаниюlibrary.sfw — swf, полученная путём разархивирования собранной as3-части mac — реализация для платформы maclibrary.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 процесс аналогичный, в некоторых случаях даже проще): Разархивировать *.ane файл как обычный zip-архив в папку, которая имеет название в точности, как id нашего расширения + .ane в конце. В нашем случае это будет «my.awesome.native.extension.ane». Эту папку лучше скопировать в новую директорию внутри проекта. К примеру у меня это libs-ane, в которой уже лежат разархивированные расширения. В IntelliJ IDEA, в настройках проекта НЕ добавляем эту директорию в зависимости. В другую директорию внутри проекта добавляем собранную as3 библиотеку. У меня эта директория называется libs-swc. Эту директорию уже добавляем в зависимости проекта. Тип связи Merged. В параметрах запуска ADL необходимо добавить следующую опцию -extdir /ABSOLUTE_PATH_TO_PROJECT/libs-ane. В IntelliJ IDEA эти параметры находятся в Run→Edit Configurations→AIR Debug Launcher Options.82cc8fe2a0ab4505b239ae0582674ed0.png В дескрипторе проекта добавить id нативного расширения в блоке «extensions» my.awesome.native.extension Теперь мы можем при отладке использовать нативные расширения. Но есть ещё кое-что. Как вы наверное знаете, в iOS SDK есть ряд классов, которые будут корректно работать только при запуске их из Finder. Для этого при помощи той же IntelliJ IDEA можно собрать нативный бандл и использовать его. Но проблема в том, что предыдущий метод интеграции нативного расширения не позволит нам осуществить сборку бандла. Но сборка нам всё же может пригодиться, поэтому нам нужно ещё немного поработать. Помните наш *.ane? Так вот именно сейчас настало его время.Все *.ane необходимо добавить в очередную отдельную директорию, опять же внутри проекта. У меня эта папка называется anes.В IntelliJ IDEA, в настройках проекта также добавляем эту директорию в зависимости. Тип связи станет ANE и изменить его невозможно (именно поэтому невозможно одноверменно собирать бандл и работать в режиме отладки). В дальнейшем для отладки — убираем из зависимостей эту директорию, для сборки бандла — добавляем. Но в любом случае нам нужно, чтобы anes была внешней библиотекой. Для этого я использую дополнительный build-config.xml файл, в котором описываю дополнительные параметры билда. В этом build-config.xml необходимо указать директорию anes, как путь внешней библиотеки. Простейший вариант может выглядеть так: 16.0.0 23

${flexlib}/libs/player/{targetPlayerMajorVersion}.{targetPlayerMinorVersion}/playerglobal.swc anes true libs-swc

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

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

d768812d18eb43a2ae3e4e31dffb1893.png

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

3e939cecab114a8f81d69f567f8594a3.png

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

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

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

© Habrahabr.ru