Developing Air Native Extensions (ANE) for OS X

Hello to all habroyuzery. I would like to share the experience of creating native extensions for OS X.

AIR is simply an awesome cross-platform environment. Until it comes to using some kind of chips unique to the platform. It was this problem that I encountered when I was given the task of turning a browser flash game into a desktop one for OS X. All this using the AIR environment was done by me in a few hours and I will not describe this process, since in Google this The topic is full of information. The most interesting thing began when it became necessary to connect various Apple services to the game, such as GameCenter, In-App-Purchase, etc. And here I ran into difficulties. The fact is that there are a bunch of ready-made ANEs, including free ones. But the trouble is that all these solutions work only for iOS. For OS X, there’s neither ready-made libraries,

Now I want to collect all the accumulated knowledge and experience in one place and share with you to at least slightly reduce the pain that you have to go through, if you decide to create native libraries for poppy too. Although after the four developed extensions for OS X they do not seem so complicated and sophisticated.

So. For work I used:
AIR 16;
Flex 4.6.0;
Adobe Flash Builder 4.6 or IntelliJ IDEA 14 (Flash Builder was used to write the library, although the same can be done in IntelliJ IDEA. But the project itself I developed in IntelliJ IDEA. It's a matter of taste, I suppose);
Xcode 6.1.1;
OS X Yosemite (10.10.1);

I will divide the entire process of creating ANE into 3 parts.

Part one. Objective-c


I think it’s more logical to start creating native extensions by writing the most native code, although in any case, most likely you will have to return to changing the native code more than once.

We start by creating a new project in Xcode. File -> New -> Project ... (Cmd + Shift + N). Next, select OX X -> Framework & Library -> Cocoa Framwork.



We come up with a name for our framework. The name can be anything, in the future, it will not be used in any way in our future native library.



After that, we have an empty project with one header file.



If the native library is planned to implement simple single functions that somehow need to be performed in Objective-C, then we can do without a header file using only the implementation file (* .m). But I will describe working with a full-fledged class.

Before writing code, you need to add the Adobe AIR.framework library to the project. Right-click on the project, and select Add files to "...". I hope you already have a fresh version of the AIR environment, because it stores the library that we need. You can find it here: ../AIR_FOLDER/runtimes/air/mac/Adobe AIR.framework.

After that, the project will look something like this:



You also need to install a 32-bit target platform (i386) for the project (not for the purpose). At the time of writing, Adobe AIR.framework worked only for 32-bit platforms. In the same project settings in Build Settings, look for automatic reference, and set Objective-C Automatic Reference Counting to No.



I also change the paths of the output files so that they are in the same place as the source. To whom it is more convenient.



First of all, we need to define the initializers of the context and the library itself (optionally, you can also define finalizers).

First, define a context initializer. It will be called, oddly enough, when initializing the context in the as3 part, but more on that later. When using several native libraries in a project, it is very important to call initializers unique names. The context initializer also defines the functions that will be available from as3 code.

So. We declare a context initializer as follows:

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()");
}

It is worth noting that the NSLog function, which outputs a message to the console, will also output a message in the form of trace in the IDE console in which you are developing the main project.

Now we define the initializer of the library itself. In it, we indicate a link to the initializer and context finalizer. We will use it later in the assembly of the library:

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()");
}

Next, we describe our only function available from the action script code. Inside this function, we can call various native Objective-C methods, including using the iOS SDK:

FREObject (init) (FREContext context, void* functionData, uint32_t argc, FREObject argv[]){
    NSLog(@"[MyANE.Obj-C] Hello World!");
    return nil;
}

For convenience, you can use the directive:

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

Using the directive described above, you can define a function much easier and shorter:

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

Make the build (Command + B). As a result, in the path that we indicated at the very beginning, the framework should have appeared, with a name identical to the name that we indicated, again at the beginning.

The simplest Objective-C library is ready. The only thing it can do is print a string in trace. But to demonstrate the work will do. Now we need to create the second half of our ANE - AS3 library.

Source
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)
{
}




Part two. Action script


To create a library for Action Script, you can use any IDE, with the possibility of developing in ActionScript. But I used the standard IDE for such purposes - Flash Builder.

Creating a library is very simple: File -> Create -> Flex Library Project.

We call our library, and be sure to connect the Adobe AIR libraries. In fact, we do this for one single class, which will allow us to work with context.



Immediately create a new ActionScript class (you can, and it will be even more convenient to create it in the default package), inheriting it from flash.events.EventDispatcher (in general, you can inherit from anything, but you can not inherit at all, but the EventDispatcher class will allow dispatch events to the instance, which is very useful when working with the iOS SDK, where some of the requested data (list of GC friends, list of available IAPs) does not come immediately). This will be our main class, which we will use when working with the library.

First we need to get an instance of the context. This is done as follows:

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

The static createExtensionContext method creates an extensionContext instance. Here we must pass the id of our extension, in this case "my.awesome.native.extension", as well as the context type. Type must be specified only in case of several library implementations. If one implementation is planned, then null can be passed as a type.

At the same time in the project can be used only one (singleton) instance, the context of one, specific type. Personally, after a bunch of created native extensions, I never had the need for multiple implementations of this extension. So in this case, having one single implementation, we will basically have one instance for the entire ANE. Therefore, the constructor needs to be called once, and in the future just get the already created object.

The easiest option to implement is to call a certain static function that will return an instance of the object, or create a new one through the constructor if there is none.

First, we describe the constructor (which we will never call from the project):

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');	// Вызываем ошибку, если пытаемся вызвать конструктор
}
}

You also need to verify that ANE is trying to run on a Mac.

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

Now we describe the function that we will access each time we need to get an instance of our library.

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

At this point, we have completed the initialization. Now you can use the methods from Objective-C. You can call a function from the native code by the method of the context instance class call (), which must be passed as an argument one of the names of the functions specified in the context initializer in the native code, as well as the parameters of the function. In this example, we have described only one function called “initLibrary”. She does not accept any parameters, well, we will not pass anything.

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

Save the project. The library is automatically compiled, and by default, it is placed in the bin directory, at the root of the project.
Thus, we provided the most basic functionality. Now you can go to the last part.

Source
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");
	}
}



Part three. Library assembly


Finally, we have 2 pieces of the native library. All you need is to connect them into a full-fledged ANE.

First, we need a handle in which we describe our extension. It will be the following * .xml file:

my.awesome.native.extension1.0.0MyANE.frameworkMyAwesomeNativeExtensionInitializerMyAwesomeNativeExtensionFinalizer

Here:
id - id of the resolver, which must match the id that we specified when creating the context instance in the as3 part.
nativeLibrary is a compiled framework from the Objective-C
initializer, finalizer is the initializer and finalizer of the library (not context), which was also described in the Ojbective-C part.

It is also recommended to make an implementation for the default platform, in which there is no native code. Well, follow the recommendations, it is not difficult.

The last piece of our library is ready, and now we can begin to build. And here the fun begins.

For convenience, I would advise you to create a separate folder for the assembly, otherwise there will simply be confusion and porridge, which is already enough here. I use the following folder structure:


where
  • _out - the actual folder for the assembly.
    • default - default implementation for the platform
      • library.sfw - swf obtained by unzipping the assembled as3-part
    • mac - implementation for the mac platform
      • library.sfw - swf obtained by unzipping the assembled as3-part
      • MyANE.framework - a compiled Objective-C part
    • extension.xml - extension descriptor
    • MakeANE.sh is just a script for quickly building a library
  • ActionScript3 and Objective-C are project parts folders for the library.

Separately by library.sfw. Yes, this is a piece of a piece of the library, which should be separate, but at the same time, the assembled as3-piece is also necessary for us. To get it, you need to unzip the assembled as3 library as a regular zip archive (preserving this same as3 library).

Now all we need to do is build the extension using the AIR Developer Tool (ADT). You can find it here: ../AIR_FOLDER/bin/adt

For assembly, I use the following script (from the _out folder):
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.

Now we have a finished MyANE.ane file, which is a compiled native library. But even this is not the end. The real fun begins when we try to use the native library in an OS X project. Again, there are a lot of tutorials and various FAQs for iOS, but as it turned out, for OS X it is necessary to perform other rituals with a tambourine, and not only.

The last part. Integration of the native library into the project


So, we have our own written library. Here it is, the finished * .ane file. Take and use. But no. In order to use the native library in OS X during development, it is not needed. But of course, our efforts were not in vain. We just need to do the following (I will describe the process for IntelliJ IDEA, but for Flash Builder the process is similar, in some cases even easier):

  1. Unzip the * .ane file as a regular zip archive into a folder that has the name exactly like the id of our extension + .ane at the end. In our case, it will be "my.awesome.native.extension.ane". It is better to copy this folder to a new directory inside the project. For example, I have this libs-ane, which already contains unzipped extensions.
  2. In IntelliJ IDEA, in the project settings, DO NOT add this directory depending.
  3. In another directory inside the project, add the assembled as3 library. I have this directory called libs-swc.
  4. We already add this directory depending on the project. Link Type Merged.
  5. In the ADL startup options, you need to add the following option -extdir / ABSOLUTE_PATH_TO_PROJECT / libs-ane. In IntelliJ IDEA, these options are in Run-> Edit Configurations-> AIR Debug Launcher Options.
  6. In the project descriptor, add the id of the native extension in the "extensions" block
    my.awesome.native.extension

Now we can use native extensions for debugging. But there is one more thing. As you probably know, in the iOS SDK there are a number of classes that will only work correctly when they are launched from the Finder. To do this, using the same IntelliJ IDEA, you can build a native bundle and use it. But the problem is that the previous method of integrating the native extension will not allow us to build the bundle. But the assembly can still be useful to us, so we need to work a little more. Remember our * .ane? So right now its time has come.

  1. All * .ane must be added to the next separate directory, again inside the project. I have this folder called anes.
    In IntelliJ IDEA, in the project settings, we also add this directory depending. The type of connection will become ANE and it is impossible to change it (that is why it is impossible to collect bundles and work in debug mode simultaneously). Further for debugging - we remove this directory from dependencies, for assembly of a bundle - we add.
  2. But in any case, we need anes to be an external library. To do this, I use an additional build-config.xml file, which describes additional build parameters. In this build-config.xml, you need to specify the anes directory as the path of the external library. The simplest option might look like this:

    16.0.023${flexlib}/libs/player/{targetPlayerMajorVersion}.{targetPlayerMinorVersion}/playerglobal.swcanestruelibs-swc

    To use an additional build-config file, you must add it in the project settings. Project Structure -> Additional compiler configuration file.



    Well, or even simpler, there you can add a parameter to the Additional compiler options: "-external-library-path path-element anes"



Now you can collect the native bundle. This is done simply by Build-> Package AIR Application. As a target, I use * .app.



Well, at the exit, we get a ready-made, native bundle, with a working draft that will use ANE.

That's all. Thank you for your attention, I hope this article is useful to someone. This is my first article on Habré, so I would love to hear constructive criticism and advice on how to improve the article. I will also definitely answer questions in the comments, and, if possible, supplement the article.

If there is interest in this topic, then I would also like to talk about the exchange of various data between as3 and the native code, about events and much more (although these are already more general concepts on which it is a bit easier to find information).

Also popular now: