Привет всем хаброюзерам. Хотел бы поделиться опытом создания нативных расширений для 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 библиотеку.
#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
#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 — просто скрипт для быстрой сборки библиотеки
- default — реализация для платформы по-умолчанию
- 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.
- В дескрипторе проекта добавить id нативного расширения в блоке «extensions»
<extensions> <extensionID>my.awesome.native.extension</extensionID> </extensions>
Теперь мы можем при отладке использовать нативные расширения. Но есть ещё кое-что. Как вы наверное знаете, в iOS SDK есть ряд классов, которые будут корректно работать только при запуске их из Finder. Для этого при помощи той же IntelliJ IDEA можно собрать нативный бандл и использовать его. Но проблема в том, что предыдущий метод интеграции нативного расширения не позволит нам осуществить сборку бандла. Но сборка нам всё же может пригодиться, поэтому нам нужно ещё немного поработать. Помните наш *.ane? Так вот именно сейчас настало его время.
- Все *.ane необходимо добавить в очередную отдельную директорию, опять же внутри проекта. У меня эта папка называется anes.
В IntelliJ IDEA, в настройках проекта также добавляем эту директорию в зависимости. Тип связи станет ANE и изменить его невозможно(именно поэтому невозможно одноверменно собирать бандл и работать в режиме отладки). В дальнейшем для отладки — убираем из зависимостей эту директорию, для сборки бандла — добавляем. - Но в любом случае нам нужно, чтобы 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 и нативным кодом, про эвенты и многое другое(хоть это уже и более общие понятия, по которым найти информацию немного проще).
Автор: Ik0l