In AIR 3.0, Adobe released a capability called Native Extensions. This allows you to directly extend the capabilities of the AIR Runtime on any platform supported by AIR (iOS, Android, Windows, OSX, etc…). In my consulting work for the NitroLM licensing and security project, I’ve had a chance to really dig into what it takes to develop a cross-platform native extension in C++.

I’m using Microsoft Visual C++ Express on Windows to create a .DLL and XCode 4 on OSX to create a .framework library. In this example, we’ll follow the code of one function in NitroLM called getMessages(). This function retrieves admin-generated messages from the NitroLM servers. For example, a developer might want to send his app users a message about the availability of a new version the next time they launch the software product.

On the AS3 side, you’ll need a class to handle the integration with the native libraries. For NitroLM, this class is called LicenseClient. You can view the full contents of this file on GitHub as the AS3 portion of the NitroLM library is open-source.

https://github.com/westbam/NitroLM/blob/master/nitrolm-air/src/com/nitrolm/LicenseClient.as

In the constructor of this class, we create the ExtensionContext and optionally call a function to turn on debugging in the native code.

public static var context:ExtensionContext;

/**
 * Create a new LicenseClient instance
 *
 * @param debug If enabled, the native library will write a file called nitrolm_debug.out
 */

public function LicenseClient(debug:Boolean = false)
{
  if(!context)
  {
    context = ExtensionContext.createExtensionContext("com.nitrolm.NitroLM", "");
  }
 
  if(context && debug)
  {
    context.call("enableDebug_air");
  }
}

Next, on the actionscript side, we call the getMessages() function to retrieve our list of messages. Since we don’t want to block our GUI thread waiting on an IO server call, we have to handle an event to retrieve the data. We listen for a LicenseClientEvent whose .data property will contain an Array of messages once it comes back.

public function getMessages(email:String, version:String, days:int):void
{
  context.addEventListener(StatusEvent.STATUS, handleStatusEvent);
  context.call("getMessages_air", email, version, days);
}

You’ll notice that we are handling another type of StatusEvent here. This is a message we’ll listen for internally that will let us know that the data is ready to be retrieved on the native side. When we handle this message in handleStatusEvent(), we can then make a second call into our native library to retrieve the message data. This 2-call approach prevents us from ever locking our AIR GUI thread waiting on the native side to complete.

private function handleStatusEvent(event:StatusEvent):void
{
  context.removeEventListener(StatusEvent.STATUS, handleStatusEvent);
  var req_type:int = new int(event.code);
  var response:int = new int(event.level);
  var lce:LicenseClientEvent = new LicenseClientEvent(LicenseClientEvent.LICENSE_RESPONSE, req_type, response);
 
  switch(req_type)
  {
    case NLMConstants.REQUEST_MESSAGES:
      if(response == NLMConstants.RESPONSE_OK)
      {
        lce.data = context.call("getMessages_data");
      }
    break;
  }
 
  dispatchEvent(lce);
}

The reason for the 2-call approach is that we’re using synchronous IO on the native side. If we didn’t spin up a new thread for the IO call on the native side, the AIR GUI would freeze every time we made a server call. To solve this, the first call is made to unpackage our AS3 parameters into native data types and spin up a new Thread to handle the IO. Our thread then tosses up a StatusEvent once it’s done receiving a response from the NitroLM server. A second call is then made to getMessages_data to convert the native response data into actionscript types we can use. Let’s take a look at the native code now.

We first need to define our native initializer, finalizer, and data functions in a header file.

/*
 * AIR_COMMANDS.H
 * Native entry point for Adobe AIR
 *
 * Copyright 2011 Simplified Logic, Inc.  All Rights Reserved.
 * Author: Andrew Westberg
 */


#ifndef __AIR_COMMANDS_H_
#define __AIR_COMMANDS_H_

#include "nlm_constants.h"
#include "nlm_commands.h"
#include "FlashRuntimeExtensions.h"

#ifdef __cplusplus
extern "C" {
#endif

...
FREObject getMessages_air(FREContext ctx, void* funcData, uint32_t argc, FREObject argv[]);
FREObject getMessages_data(FREContext ctx, void* funcData, uint32_t argc, FREObject argv[]);

void ContextInitializer(void* extData, const uint8_t* ctxType, FREContext ctx, uint32_t* numFunctions, const FRENamedFunction** functions);
void ContextFinalizer(FREContext ctx);
void ExtInitializer(void** extData, FREContextInitializer* ctxInitializer, FREContextFinalizer* ctxFinalizer);
void ExtFinalizer(void* extData);

#ifdef __cplusplus
} //extern "C"
#endif

#endif //__AIR_COMMANDS_H_

Our initializer function is called when we first create the extension context. We need to tell it which functions the context has defined.

/*
 * AIR_COMMANDS.C
 * Exported functions for Adobe AIR (and related non-exported functions)
 *
 * Copyright 2011 Simplified Logic, Inc.  All Rights Reserved.
 *
 * Written by Andrew Westberg
 */


#include <stdlib.h>
#include "air_commands.h"
#include "CAsyncTask.h"
#include "storage.h"
#include "Debug.h"

#ifdef __cplusplus
extern "C" {
#endif

static CThread asyncThread;
static CAsyncTask asyncTask;

void ExtInitializer(void** extData, FREContextInitializer* ctxInitializer, FREContextFinalizer* ctxFinalizer)
{
  *ctxInitializer = &ContextInitializer;
  *ctxFinalizer = &ContextFinalizer;
}

void ExtFinalizer(void* extData)
{
}

void ContextFinalizer(FREContext ctx)
{
  gracefulShutdown();
}

void ContextInitializer(void* extData, const uint8_t* ctxType, FREContext ctx, uint32_t* numFunctions, const FRENamedFunction** functions)
{
  asyncThread.m_gracefulShutdown = FALSE; //terminate thread hard at shutdown because we're inside a .DLL

  FRENamedFunction *func;

  *numFunctions = 50;
 
  func = (FRENamedFunction*) malloc(sizeof(FRENamedFunction) * (*numFunctions));

  ...
 
  func[24].name = (const uint8_t*) "getMessages_air";
  func[24].functionData = NULL;
  func[24].function = &getMessages_air;

  func[46].name = (const uint8_t*) "getMessages_data";
  func[46].functionData = NULL;
  func[46].function = &getMessages_data;

  ...
 
  *functions = func;
}

#ifdef __cplusplus
} //extern "C"
#endif

In the function getMessages_air(), we parse out our input parameters and store them in our thread task and then spin up the task on our thread for processing. The threading library I’m using was developed by Walter Capers and can be found under the CodeProject article Creating a C++ Thread Class.

FREObject getMessages_air(FREContext ctx, void* funcData, uint32_t argc, FREObject argv[])
{
    while(asyncTask.Status() == TaskStatusWaitingOnQueue || asyncTask.Status() == TaskStatusBeingProcessed)
    {
        //wait until our task is ready to process something new
        Sleep(10);
    }

    asyncTask.initialize(ctx, REQUEST_MESSAGES, argc, argv);

    if(asyncThread.Event(&asyncTask))
        Debug::logDebug("submitted getMessagesTask for background processing");
    else
        Debug::logDebug("ERROR submitting getMessagesTask for background processing");

    return NULL;
}

The asyncTask.initialize() method parses out our input parameters from actionscript and stores them in native object types so our thread can work with them. It’s important to run this code _before_ spinning up the thread because your thread WILL NOT have access to the FRE functions from Adobe. The only capabilities your thread has from Adobe is to toss up a StatusEvent using FREDispatchStatusEventAsync() when it’s done. You’ll also notice that you have to do a lot of error handling on just about every FRE function you call.

void CAsyncTask::initialize(FREContext ctx, const int requestType, int argc, FREObject *argv)
{
  //reset the task
  ThreadId_t id = 0;
  SetTaskStatus(TaskStatusNotSubmitted);
  Thread(&id);

  CAsyncTask::ctx = ctx;
  CAsyncTask::requestType = requestType;

  uint32_t len = -1;
  const uint8_t *email = 0;
  const uint8_t *version = 0;
  int32_t days = 0;
 
  switch(requestType)
  {
    case REQUEST_MESSAGES:
      if((res=FREGetObjectAsUTF8(argv[0], &len, &email)) != FRE_OK)
      {
        Debug::logDebug("ERROR: FREGetObjectAsUTF8(argv[0], &len, &email) = %d", res);
        dispatchEvent(REQUEST_MESSAGES, RESPONSE_INTERNAL_CLIENT_ERROR);
        return;
      }
      strcpy(CAsyncTask::email, (const char*)email);

      if(argv[1] != NULL)
      {
        if((res=FREGetObjectAsUTF8(argv[1], &len, &version)) != FRE_OK)
        {
          Debug::logDebug("ERROR: FREGetObjectAsUTF8(argv[1], &len, &version) = %d", res);
          dispatchEvent(REQUEST_MESSAGES, RESPONSE_INTERNAL_CLIENT_ERROR);
          return;
        }
        strcpy(CAsyncTask::version, (const char*)version);
      }
      else
        strcpy(CAsyncTask::version, "");

      if((res=FREGetObjectAsInt32(argv[2], &days)) != FRE_OK)
      {
        Debug::logDebug("ERROR: FREGetObjectAsUTF8(argv[2], &days) = %d", res);
        dispatchEvent(REQUEST_MESSAGES, RESPONSE_INTERNAL_CLIENT_ERROR);
        return;
      }
      CAsyncTask::days = days;
    break;
  }
}

Our CThread will call the Task() method of CAsyncTask once it starts processing. In this function, we make our synchronous IO call to the NitroLM server by calling the internal getMessages() method and dispatch an event once it’s complete.

void CAsyncTask::dispatchEvent(const int req_type, const int response)
{
  FREResult res;
  char code[5];
  char level[5];

  sprintf(code, "%d", req_type);
  sprintf(level, "%d", response);
  if((res=FREDispatchStatusEventAsync(ctx, (const uint8_t*)code, (const uint8_t*)level)) != FRE_OK)
  {
    Debug::logDebug("ERROR: FREDispatchStatusEventAsync(ctx, (const uint8_t*)code, (const uint8_t*)level) = %d", res);
    return;
  }
}

BOOL CAsyncTask::Task()
{
  ThreadId_t id = 0;
  Thread(&id);

  int ret;

  switch(CAsyncTask::requestType)
  {
    case REQUEST_MESSAGES:
      outMsgs = NULL;
      ret = getMessages(email, version, days, &outMsgs);
      dispatchEvent(REQUEST_MESSAGES, ret);
    break;
  }

  return TRUE;
}

Finally, we package the data as an Array of Objects when getMessages_data() is called from AIR.

FREObject getMessages_data(FREContext ctx, void* funcData, uint32_t argc, FREObject argv[])
{
  int i;
  FREResult res;
  FREObject result;
  FREObject subject;
  FREObject body;
  FREObject message;
  FREObject thrownException;
  FREObject stackTrace;
  const uint8_t *stack;
  uint32_t len;

  if(asyncTask.outMsgs != NULL)
  {
    if((res=FRENewObject((const uint8_t*)"Array", 0, NULL, &result, &thrownException)) != FRE_OK)
    {
      Debug::logDebug("ERROR: FRENewObject((const uint8_t*)\"Array\", 0, NULL, &result, &thrownException) = %d", res);
      freeMessages(asyncTask.outMsgs);
      FRECallObjectMethod(thrownException, (const uint8_t*)"getStackTrace", 0, NULL, &stackTrace, NULL);
      FREGetObjectAsUTF8(stackTrace, &len, &stack);
      Debug::logDebug((const char*)stack);
      return NULL;
    }

    FRESetArrayLength(result, asyncTask.outMsgs->size);

    for(i=0;i<asyncTask.outMsgs->size;i++)
    {
      subject = 0;
      if(asyncTask.outMsgs->list[i].subject != NULL)
      {
        if((res=FRENewObjectFromUTF8(strlen(asyncTask.outMsgs->list[i].subject), (const uint8_t*)asyncTask.outMsgs->list[i].subject, &subject)) != FRE_OK)
        {
          Debug::logDebug("ERROR: FRENewObjectFromUTF8(strlen(asyncTask.outMsgs->list[i].subject), (const uint8_t*)asyncTask.outMsgs->list[i].subject, &subject) = %d", res);
          freeMessages(asyncTask.outMsgs);
          return NULL;
        }
      }
      if((res=FRENewObjectFromUTF8(strlen(asyncTask.outMsgs->list[i].msg), (const uint8_t*)asyncTask.outMsgs->list[i].msg, &body)) != FRE_OK)
      {
        Debug::logDebug("ERROR: FRENewObjectFromUTF8(strlen(asyncTask.outMsgs->list[i].msg), (const uint8_t*)asyncTask.outMsgs->list[i].msg, &body) = %d", res);
        freeMessages(asyncTask.outMsgs);
        return NULL;
      }

      if((res=FRENewObject((const uint8_t*)"Object", 0, NULL, &message, &thrownException)) != FRE_OK)
      {
        Debug::logDebug("ERROR: FRENewObject((const uint8_t*)\"Object\", 0, NULL, &message, &thrownException) = %d", res);
        freeMessages(asyncTask.outMsgs);
        FRECallObjectMethod(thrownException, (const uint8_t*)"getStackTrace", 0, NULL, &stackTrace, NULL);
        FREGetObjectAsUTF8(stackTrace, &len, &stack);
        Debug::logDebug((const char*)stack);
        return NULL;
      }

      if((res=FRESetObjectProperty(message, (const uint8_t*)"subject", subject, &thrownException)) != FRE_OK)
      {
        Debug::logDebug("ERROR: FRESetObjectProperty(message, (const uint8_t*)\"subject\", subject, &thrownException) = %d", res);
        freeMessages(asyncTask.outMsgs);
        FRECallObjectMethod(thrownException, (const uint8_t*)"getStackTrace", 0, NULL, &stackTrace, NULL);
        FREGetObjectAsUTF8(stackTrace, &len, &stack);
        Debug::logDebug((const char*)stack);
        return NULL;
      }
      if((res=FRESetObjectProperty(message, (const uint8_t*)"body", body, &thrownException)) != FRE_OK)
      {
        Debug::logDebug("ERROR: FRESetObjectProperty(message, (const uint8_t*)\"body\", body, &thrownException) = %d", res);
        freeMessages(asyncTask.outMsgs);
        FRECallObjectMethod(thrownException, (const uint8_t*)"getStackTrace", 0, NULL, &stackTrace, NULL);
        FREGetObjectAsUTF8(stackTrace, &len, &stack);
        Debug::logDebug((const char*)stack);
        return NULL;
      }

      if((res=FRESetArrayElementAt(result, i, message)) != FRE_OK)
      {
        Debug::logDebug("ERROR: FRESetArrayElementAt(result, i, message) = %d", res);
        freeMessages(asyncTask.outMsgs);
        return NULL;
      }
    }

    freeMessages(asyncTask.outMsgs);
    return result;
  }

  return NULL;
}

Hopefully, this code helps you get a start on writing your own NativeExtensions for Adobe AIR. If you’d like more information about this particular NitroLM project, you can browse the GitHub projects and the NitroLM documentation.

Post to Twitter

Posted by Andrew, filed under AIR, as3, C++, security. Date: November 30, 2011, 1:48 pm | 10 Comments »