Haxe宏机制的中文简介:http://blog.csdn.net/rocks_lee/article/details/8989706
Haxe宏官网文档(英文):http://haxe.org/manual/macros
Haxe宏进阶使用(英文):http://haxe.org/manual/macros/advanced
基于宏的编译器设置:http://haxe.org/manual/macros_compiler
使用宏动态创建新类型(英文):http://haxe.org/manual/macros/build
宏上下文类haxe.macro.Context API:http://haxe.org/api/haxe/macro/context
可供参考的基于宏的Haxe库:StablexUI,haxe-continuation,HaxeAngularSupport
AngularSupport库(国人作品哦)的中文说明:http://freewind.me/blog/20130122/2016.html
上次看着官方文档,简单的脑补了一下Macro的运作机制,不过呢,纸上得来终觉浅,于是我决定实际编写一个基于宏的haxelib来边开发边学习边总结。
这个库也是从实际需求出发,是一个国际化/本地化支持库,目标是在编译时实现字符串的外部化(externalize),生成xml供翻译使用,多语言字串的替换等等。
因为我自己的开发环境限制,目前还限制在Haxe2.10条件下,没有用到Haxe3.0的宏具化特性。
下面说说我在此库开发中对宏运用的心得体会:
一切皆为表达式:
首先需要理解,在Haxe编译器生成的语法树中,不存在所谓控制结构,一切皆为表达式。比如4, "a"这些是常量表达式;a+b, b=c是二元表达式;{a;b;c;}是块表达式,其参数为表达式数组;if(a) b else c则是if表达式,a,b,c是表达式的三个参数表达式,等等。
具体可以看haxe.macro.Expr的源码。
下面说的表达式皆指的是Haxe的语法树上的表达式节点。
宏方法的概念:
可以参考C/C++中的宏来帮助理解Haxe的宏。在C/C++中,用#define定义的宏,会在预编译阶段作为字符串直接替换到代码的使用位置。但是预编译器并不理解这字符串的语义,而是交由下阶段的编译器处理。
从替换机制来说,Haxe宏的意义和C/C++的宏非常类似,但不是简单的字符串替换,而是表达式替换,即宏方法接受输入的若干参数表达式,替换成输出表达式。
和C/C++的预编译过程类似,Haxe宏也运行在一个专门的宏编译过程中,这期间,宏方法可以通过宏上下文类haxe.macro.Context获取编译器当前状态相关的信息。
在Haxe中用@:macro元数据标注的方法即为宏方法(Haxe3使用macro关键字),宏方法运行在编译期,通常宏方法是一个公共静态方法,它的参数是0至多个表达式,返回一个新的表达式。
因为宏运行在编译期,而Haxe编译器不存在跨平台问题(即使是在Windows, Linux和Mac上,就haxe源码的处理来说,编译器的行为是完全一致的),因此宏代码是不必考虑平台问题的,但宏也只能够使用跨平台的包,包括haxe.macro, sys.io等等。
宏方法实际应用:
你可以获得常量表达式的值,然后进行运算,比如我的库中的宏方法I18n.str("hello"),它要求参数必须是字符串常量表达式,它会获取其值即"hello",然后提取出来,插入到输出的xml文件中,然后查找是否存在给定语言的"hello"的对应翻译,比如找到了是"你好",然后把"你好"这个字符串重新包装成字符串常量表达式返回。相当于如下的替换:
textfield.text = I18n.str("hello"); ==> textfield.text = "你好";
你也可以在宏方法中进行文件操作,比如对资源文件进行预处理,或把外部文件内容转换为Haxe代码等。举个实际例子,我库中的另一个宏方法I18n.res("logo.png"),会首先检测工作目录中给定语言的对应资源文件是否存在,如果存在,就把它复制到最终的资源文件目录中,否则把默认的资源文件复制过去。最后,返回资源文件的实际路径。相当于以下替换:
Assets.getBitmapData(I18n.res("logo.png")); ==> Assets.getBitmapData("res/i18n/logo.png");
后处理和预处理:
因为宏方法都是在编译过程中,在源文件中被调用的位置执行的,如果想在全部编译完成之后进行一次后处理工作,则可以用Context.onGenerate()来传入一个供后处理调用的回调方法。比如,在我的库中,要在所有编译完成后,把提取出来的所有字符串生成一个聚合的xml文件供翻译使用。
对应的,如果需要预处理工作的话,只要添加一个用来初始化库的宏方法即可,并要求使用者必须在程序入口首先调用这个初始化宏方法。
基于宏的编译器设置:
宏还可以被用于进行编译器设置,上面的英文文档里有一些例子,这里从我的实际需求出发说明一下。
因为我希望我的库的两个参数即目标地区和资源输出目录能够在编译时从外部传入,而不是需要硬编码在代码中,但是,Haxe中编译器的参数都是布尔类型的,比如传入-Ddebug,则在源码中可以通过#if debug来判断debug是否定义。
但是我的需求则是传入这样的参数:-locale=zh,但这个用布尔参数机制无法实现,因此只能求助于基于宏的编译器设置。
具体方法是给haxe编译器传递如下形式的参数:--macro com.roxstudio.i18n.I18n.locale('zh') ,locale(s: String)就是我的I18n类中的一个普通静态方法。
这样在宏编译期刚开始的时候,这个方法就能够被调用,从而可以以命令行方式把参数传入。
值得注意的是,这个方法和宏方法不同,不必用@:macro (或macro关键字)标注,而是写在#if macro ... #end 代码块中即可。
另外非常值得指出的是,编译器设置宏虽然和宏方法使用方式不同,但它们运行在同一个上下文,因此,用编译器设置宏设置的静态变量值是能够被随后的宏方法读取的。
在nme项目里,因为我们不直接调用haxe编译器,因此需要如下编写nmml来传入编译器设置:
<compilerflag name="--macro com.roxstudio.i18n.I18n.locale('zh')" />
<compilerflag name="--macro com.roxstudio.i18n.I18n.assets('res/i18n')" />
包含宏方法的Haxe源文件:
官方不建议把宏方法和普通方法混合放在同一个源文件中,主要是容易弄混,不过我的库为了简洁全写在了一个类中,这样的话,就需要用条件编译来区隔不同的代码段。
用#if macro ... #end来包围仅被宏方法使用的成员,用#if !macro ... #end 来包围供运行时使用的成员。
注意宏方法本身不要用#if macro ... #end 包围。
另外被 #if neko ... #end 包围的代码也能被宏方法正常引用。
编译时序问题:
最近再把我的i18n库移植到Haxe 3.0时,遇到了不少问题,看来Haxe3.0在编译机制上,和Haxe2.10比起来确实有不少不为人知的变化。
对使用宏的Haxe库最大的影响就是编译时序问题,我感到在Haxe2中,编译时序和运行时序比较接近,但Haxe3中则差别较大。
首先,Haxe3中,Haxe源文件中定义的工具类(就是类名和文件名不一致的那些类)似乎要先于同源文件中的主类(即和文件名相同的那个类)编译,不管它们的定义位置先后如何。因此,入口类的源文件最好尽量简洁,以确保宏类中的初始化方法首先执行。
另外,宏方法和普通方法混合写在同一个源文件中的做法,在Haxe3中似乎很容易引起时序问题,尤其是该源文件中的普通方法要调用同源的宏方法时。我最后的解决方案是把宏方法和普通方法分开在两个不同的类中。
Haxe3.0新特性:宏具化(Macro Reification)的概念:
Haxe2.x的宏已经很强大了,但是有一点不方便,就是生成结果表达式很麻烦,如果是简单的常量表达式或其它简单表达式还好,如果是比较复杂的,比如创建一个对象,或函数调用等,那么就相当的繁琐。
Haxe2.x时代的一个简单解决方案是使用Context.parse(s: String, pos: Position)来通过生成源码字符串然后重新解析的方式来生成表达式,比如StablexUI就采用类似的机制来生成创建Widget实例的代码。但是很多时候生成这样的源码也不是件容易的事情,比如涉及到字符串转义,或引用参数表达式等等。
Haxe3.0的宏具化就是用来解决这个问题的,你只要像写普通Haxe代码一样编写需要返回的表达式,然后前面加一个macro关键字即可,这样编译器就会把macro后面的内容具体化成一个表达式,而不是直接运行它。
关于宏的那些坑儿:
宏确实很强大,不过想用好也不容易,建议对Haxe熟练的人可以使用,初学者么,还是小心的踩着前人血肉铺就的道路前进吧。
1. 永远的文档缺乏问题,Haxe本身文档就骨感的可怜,何况宏这种很新还比较小众的语言特性?不过,宏的可靠性应该是可以保证的,因为Haxe本身就大量使用宏机制。
2. 宏代码完全无法调试,因为neko本身就不支持调试,因此只能用Context.warning()输出日志的方法来进行查错。
3. 编辑器支持问题,至少目前的支持Haxe的IDE对宏都没有特别好的支持,编码期语法检查,自动完成什么的通常是浮云。
4. 编写宏代码很容易陷入到时空错乱的思维紊乱状态中,因为宏代码运行在编译期,但要生成运行期的代码,访问的API有些是宏专用的,有些则是通用的,等等。