Native extensions for Adobe AIR入门

Native extensions for Adobe AIR是Adobe自AIR3引入的Native扩展接口,类似JNI一样,用于VM代码和Native代码的交互调用。通过ANE,可以突破FlexSDK本身功能限制,获得操作系统平台所支持的大量功能,以及将现有的大量Native代码资源在AS中重用。

这里以Windows平台为例介绍一下ANE代码的开发流程。其他几个平台的流程有相当大的不同,具体可以参考Adobe的文档:http://help.adobe.com/en_US/air/extensions/index.html

一个完整的ANE模块实际上分为2个部分:

  • ActionScript接口模块
  • Native模块

这是引用自Adobe文档中的架构图:

这两个模块开发完毕后,通过adt工具打包为ane文件后,才能供AS项目使用。

其中,ActionScript模块是一个swc项目,负责Native接口的创建、销毁以及调用Native代码和进行事件监听。

Native模块需要处理ANE环境的初始化、反初始化以及提供被调用的代码。在IOS平台上,Native代码以so形式提供;Android平台上,如果基于SDK编写,那么提供jar包,如果用NDK编译,那么提供so库;在Windows平台上,以DLL形式提供。

接口定义

这个示例中,我们将实现一个ANE接口,从API中获取当前PC的计算机名。这个函数我们命名为getComputerName。

Native代码被调用时,将会派发一个事件,通知AS这边自己被调用了。

Native代码实现

这里我们采用VS2012来实现,实际项目里面可以使用任意开发工具,只要能够按照规范编译Windows dll,并且可以调用FlexSDK中的lib库即可。如果采用其他特殊的开发工具,也可以直接调用adobe air.dll中的函数。

Dll的框架代码我们不再赘述,如果不熟悉,请参考Windows编程的资料。

ANE使用的DLL有四个重要的函数。一对用于整个ANE接口模块的初始化和反初始化,一对用于单个ANE Context的初始化和反初始化。一个ANE dll被加载起来之后,可以创建任意个context。而最后实际被调用的ANE函数,是和之前创建的context绑定的。

这四个函数的名字可以自己任意确定,在打包时提供的xml文件中需要加入该ANE的id、dll名字、平台以及两个全局初始化和反初始化函数的导出名。实际上,最终需要被导出的函数,也只有这两个。

4个函数的原型声明如下,必须以C语言名称进行导出:

1、

typedef void (*FREInitializer)(

void** extDataToSet ,

FREContextInitializer* ctxInitializerToSet,

FREContextFinalizer* ctxFinalizerToSet

);

这个函数用于全局初始化。该函数里面需要处理的事情很少,只需要通过参数返回Context的初始化和反初始化函数指针,即函数3、4的指针。同时可以返回一个void指针,在AIR回调其他函数时传入的extData,即这个指针。则可以用来进行某些数据绑定操作。实际使用中,这个回传参数可以作为数据绑定的媒介,例如用来将ANE的C语言接口绑定到C++对象上。

2、

typedef void (*FREFinalizer)(

void* extData

);

这个函数用于全局反初始化,我测试的时候没有发现被调用到。估计是在GC回收完所有context对象并卸载这个ANE模块时会调用。用于作最终的反初始化工作。

3、

typedef void (*FREContextInitializer)(

void* extData ,

const uint8_t* ctxType ,

FREContext ctx ,

uint32_t* numFunctionsToSet,

const FRENamedFunction** functionsToSet

);

这个函数当AS代码调用ExtensionContext.createExtensionContext时会被调用,用于创建一个ANE context。

extData由之前FREInitializer的调用中创建,AIR仅仅是回传过来。

ctxType是来自ExtensionContext.createExtensionContext的第二个参数,实际使用中可以用来通过单个createExtensionContext创建特性各异的接口。ctx由AIR生成,单个Context会绑定到特定的ctx指针,所以之后在接口函数被调用时传递过来的ctx可用来区分来自哪个Context。

numFunctionsToSet用于返回Context中创建的函数个数,它会影响到functionsToSet里面返回的数据个数。每个函数都需要有一个FRENamedFunction结构来表示。

functionsToSet用于返回一个FRENamedFunction结构的数组,用来描述这个Context中提供的所有ANE接口。其中包含接口的名字、数据绑定,以及FREFunction函数指针。这个数组的成员个数中必须和numFunctionsToSet指定的个数匹配。关于这个函数的说明,不得不吐槽一下Adobe的文档。没有写明应该由谁来负责释放。我测试的时候调试了一下,AIR不会负责释放这片内存!!喜欢new和malloc流的同志们要特别注意,Context反初始化的时候一定记得释放掉。或者也可以类似LUA里面常用的方式那样直接在全局变量中进行声明。

4、

typedef void (*FREContextFinalizer)(

FREContext ctx

);

这个函数用于在context对象的dispose方法被调用时回调。用于清理任何和该Context相关的资源。

实际供AS代码调用的函数原型如下:

typedef FREObject (*FREFunction)(

FREContext ctx,

void* functionData,

uint32_t argc,

FREObject argv[]

);

ctx传入绑定的Context指针,可以用来区分不同的Context

functionData传入在设置FRENamedFunction结构时指定的指针,也是用于绑定一些和特定函数相关的数据。

argc是参数个数

argv是参数数组,有argc个FREObject 对象。这些对象都是AS数据类型,需要通过FREGetObjectAs*系列的函数来转换为C数据类型。

在本例中,没有接受从AS过来的参数,所以我们不管参数。仅仅通过GetComputerNameW拿到计算机名,并且转换为UTF8字符串。最后通过FRENewObjectFromUTF8变为AS对象并返回。这里顺便说一句,所有ANE中使用的字符串都是UTF8编码。如果和本例一样,由平台获得的字符串格式不是UTF8的话,必须经过转换才能和AS进行交互。

和C数据类型的相互访问,由AIR提供了一系列C函数来实现。如果有过Alchemy使用经验的话,应该会很熟悉。主要分为基本数据类型、字符串、对象访问、ByteArray和Array访问这几类。在ANE中,还特别提供了BitmapData类型的访问接口。Adobe预计到相当部分的ANE扩展应该是用来处理图形数据的,而实际上确实也是。具体的使用方法,可以参见Adobe的文档,里面有详细描述。

以上就涵盖了ANE中Native代码和AS之间函数调用的所有内容。基本结构和Alchemy有很多类似的地方。但是Alchemy只是C/C++的交叉编译环境实现,实际上还是运行于VM层。ANE却是直接提供了更底层的接口。

另外,有时候我们需要从ANE代码中向AS代码派发事件,可以调用FREDispatchStatusEventAsync函数。该函数不会直接指定包含在event对象中的参数,而是指定了两个字符串参数。实际应用中应该由AS模块和Native模块自己来约定这两个字符串的内部格式和意义。ANE事件是针对Context派发的,AS那边监听也是在Context对象上。

特别指出一点的是,Adobe文档中写到FREFunction函数的调用是非线程安全的,即AIR可能在不同的线程中调用同一个FREFunction函数,也可能在不同线程中调用其他任何FREFunction函数。那么在这些函数中如果会用到临界资源,需要自己处理线程安全和同步。在Native模块中,可以启动自己的线程,但是也需要自己负责处理同步问题。

另外,使用VC编译器时,项目设置上面需要注意。C++代码生成上应选择/MT或者/MTD,而不应该选择dll中的CRT库。

AS代码实现

AS部分的代码非常简单。通过ExtensionContext.createExtensionContext创建出来Context对象,即可用这个对象来调用Native代码和监听Native代码过来的事件了。具体实现可参考示例代码,这里不再赘述。

AS模块的编译有个问题有必要说一句,我在测试时使用FlexSDK 4.6的adt打包最终的ane文件时,提示swf版本必须低于等于13。实际测试发现使用FlashBuilder编译的swc没有办法最终打包到ane中,必须在编译选项中加上-swf-version=13以下的版本才能打包成功。而Adobe文档中写的”SWF 11 for AIR 2.7, SWF 13 for AIR 3, SWF 14 for AIR 3.1, and so on”。最后发现是受到打包时编写的xml配置文件中xmlns指定的Extension namespace所影响。Adobe文档中提供了如下对应表:

兼容的AIR版本

ANE SWF版本

Extension namespace
3.0+

10-13

ns.adobe.com/air/extension/2.5
3.1+

14

ns.adobe.com/air/extension/3.1
3.2+

15

ns.adobe.com/air/extension/3.2
3.3+

16

ns.adobe.com/air/extension/3.3
3.4+

17

ns.adobe.com/air/extension/3.4
3.5+

18

ns.adobe.com/air/extension/3.5

 

另外,swc的项目设置里面要包含AIR库才能编译成功。老鸟应该是不需要提醒这个问题的。

打包

打包的步骤比较恶心。Adobe居然没有提供直接的工具,而必须手动处理N个步骤,实在是偷懒至极,不得不吐槽一下。

  • 首先需要生成一个用于代码签名的p12证书。可以通过类似如下的命令:

adt.bat -certificate -cn SelfSign -ou QE -o “Set your org name” -c US 2048-RSA test.p12 test

  • 然后,要使用解压工具,将前面生成的SWC解压出来,提取里面的library.swf文件。
  • 编写一个xml格式的扩展描述文件。详细说明可以参见http://help.adobe.com/en_US/air/extensions/WSf268776665d7970d-2e74ffb4130044f3619-8000.html,这里只提供我的示例文件内容:

    <extension xmlns=”http://ns.adobe.com/air/extension/3.1″>

    <id>ANETest</id>

    <versionNumber>1.0.0</versionNumber>

    <platforms>

    <platform name=”Windows-x86″>

    <applicationDeployment>

    <nativeLibrary>ANEDll.dll</nativeLibrary>

    <initializer>initializer</initializer>

    <finalizer>finalizer</finalizer>

    </applicationDeployment>

    </platform>

    </platforms>

    </extension>

    id的内容影响到ExtensionContext.createExtensionContext时需要传入的第一个参数,nativeLibrary是Native dll文件名,initializer是初始化函数导出名,finalizer是反初始化函数导出名。

    如果还提供了其他平台的Native模块,可以添加多个platform小节。

  • 最后使用adt命令打包,类似下面的格式:

    adt.bat -package -storetype pkcs12 -keystore test.p12 -storepass test -target ane ANEtest.ane extension.xml -swc ANETestLib.swc -platform Windows-x86 library.swf ANEDll.dll

    具体参数请参见adt的帮助或者Adobe的代码。打包时要注意文件都拷贝到当前目录下。

    如果成功,则会生成可以在AIR项目中引用的ANEtest.ane文件。

ANE模块的调用

ANE模块的使用也很简单,和导入swc库类似。首先在构建路径中设置ANE文件搜索路径:

然后在构建打包设置中将需要的ANE勾上

使用的时候直接Import之前SWC模块的包即可使用。详见示例代码。

后记

ANE总体来说是一个很实用的功能。FlexSDK的功能说实在除了渲染之外,其他方面乏善可陈。通过ANE可以利用大量现有资源来扩展AS平台的应用范围,特别是在移动平台上,ANE扩展使得应用可以采用Flash方便的进行渲染,同时使用Native代码来和系统对接。

但是,ANE为Native模块几乎没有提供调试支持,在开发上对开发者有比较高的要求。就我目前测试的Windows平台上来看,发现Native模块中的异常无法由Native调试器捕捉,而加载时机又不确定,因此Native调试的切入点比较麻烦,没有办法利用DebugBreak之类的API启动调试。猜测应该可以使用Attach的方式附加上去调,但是总的来说是很不方便的,对调试字符串的分析就显得特别重要。实际开发上面,建议将AIR接口部分和功能代码进行剥离,将功能代码进行完善的单元测试之后再合并到代码中使用。另外,由于接口是采用C语言方式提供,项目规模大的时候代码管理会是一个问题,尽量将这些接口封装为C++方式,会比较利于大项目的管理。

代码下载:ANEDll

VS2012 Update1 安装BUG修复

VS2012的HelpViewer2.0安装成本地文件之后,如果移动过本地存储路径,那么Update 1安装之后会出现报错“Help查看器所需的内容文件缺失或已损坏”,其原因是这个Patch将存储路径重新修改到了原始路径C:\ProgramData\Microsoft\HelpLibrary2\Catalogs\VisualStudio11下,HelpViewer启动时在该路径下找不到配置文件catalogType.xml造成的。

将注册表HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Help\v2.0\Catalogs\VisualStudio11\LocationPath手动修改为以前设置过的存储路径即可解决。

记录一下

吐槽

最近Linux下我的DB server遇到一诡异问题。日志文件中毫无来由多了N多query string的内容,经查看似乎每条查询都被记录到了日志中,压力测试下几分钟就能生成N个G的内容。搜索了无数遍代码,确认没有写过这部分日志。Windows版本下却没有发现这个问题。

bp fwrite,无果;bp write,无果……仔细查看代码,往文件中写入完整查询语句的地方只可能有一处。字符串处理的时候为了计算sprintf需要的缓冲区长度,在MString类中本来想通过fopen打开/dev/null文件,并调用vfprintf来计算了格式化输出的长度,但是手贱写成了vprintf,结果本不应被保存下来的中间数据被写到stdout。而Windows下面由于CRT直接提供了_vscprintf函数,没有自己做处理,所以没有发现问题。

改掉守护进程的初始化代码,照抄了UNIX环境高级编程中的那部分处理代码,关闭当前所有FD,并open /dev/null再dup两次占掉0,1,2三个fd,将标准输入输入重定向过去。问题解决。

之前的daemon代码抄袭了某垃圾代码,close掉了三个标准fd,造成后面我自己打开日志文件fd被分配到1。而vfprintf输出到stdout,结果本来应该输出到/dev/null中的内容被输出到这个日志文件中。

这个问题有点巧合,我自己的BUG加上UNIX环境下的一些遗留的东西造成了诡异问题。稍微记录一下,写daemon的时候一定要记得处理三个标准fd,顺便吐槽一下UNIX的这些古董设计……