1. 程式人生 > >haxe的 條件編譯以及巨集

haxe的 條件編譯以及巨集

巨集 是 Haxe 最主要也是最強大的特性巨集可以在編譯時通過計算初使化一些值,比如 UI 的配置等等.巨集可以掃描資原始檔夾,用於自動嵌入檔案或者 IDE 智慧提示. 

條件編譯

編譯標誌(define), 通過使用 -D key=value 或只是 -D key 來設定, 未設定 value 預設值將為字串 1. 所有 -D 定義標記的值型別都為字串

#if flash
trace("flash");
#else
trace("other");
#end

// 還可以檢測 flash player 版本
#if flash11_4
trace("flash player version >= 11.4"
); // 如果目標為flash,且指定的編譯版本大於等於 FP11.4 #end // 使用運算子檢測 #if(haxe_ver > 3.1) trace("haxe version > 3.1"); #end // 支援邏輯運算子:&& 和 || ,需要有小括號 #if (neko && debug) // 只有在當平臺為neko並且為debug模式 #end #if (flash || php) // flash 或 php 平臺 #end #if js #error 目前不支援 js 平臺 #end

#if#elseif 之後的條件允許以下表達式:

  • 重要: 任意編譯標誌(haxe-defines)將替換成名字相同的 條件識別符號, 如果帶 減(-)號,將會 同時 生成另一個 帶 下劃線(_)的識別符號

    請注意: -D some-flag 將會產生 some-flag some_flag 二個條件識別符號. 但是 #if#elseif只能識別帶下劃線的那一個. 減號那個需要用引號括起來, 而用引號括起來的識別符號卻又不能使用一些運算子.巨集方法中 Compiler.getDefine 或 Context.definedValue 能正確識別這二個.

    是否Bug? haxe --help-defines 下的 haxe-ver 只能通過 haxe_ver 才能檢測得到. 一些其它標誌未測試.

    結論: 即使定義標誌時為 減(-)號, 但檢測編譯標誌時, 請使用 下劃線(_) 替換掉 減(-)號.

  • 重要: 使用 haxelib 時 會生成同庫名一樣的 條件識別符號, 減(-)號問題和上邊一樣

  • String, Int, Float 常量值可以直接使用, 0值 以及 空字串 用於表示 false

  • 邏輯運算子 &&(與), ||(或), !(非)

  • 條件運算子 ==, !=, >, >=, <=

  • 圓括號(), 用於組合多個表示式

  • 一些個人收集清單

    flash|neko|cpp|js|php|java  : 這種平臺相關無需多解釋,但是從上邊示例可以發現, 還可以指定版本
    

同樣可以把條件識別符號放置在 @:require 之後

@:require(Compiler Flag [,"custom error message" ])

如果沒有滿足 @:require 之後的條件識別符號, 類名可以訪問, 但是 類的所有欄位(包括static型別)都不可訪問.

@:require(haxe_ver>3.1)
@:require(nodejs, "require haxelib nodejs")
class Foo{
    public var value:String;
    public function new(val:String){
        value = val;
    }
}

其它:

  • 編譯標記除了用於條件編譯,還可以用於傳值:

    例如 巨集函式 只支援常量不支援使用變數傳值,這時使用 條件編譯標記 來賦值,然後在程式碼使用 haxe.macro.Compiler.getDefine(flag) 來獲得

    對於 Compiler.define,只能通過 Context 類下的方法獲得

  • 這裡假設需定義一個叫 hello 的標記, 並且將 hello 賦值為 world.

    命令列編譯或者 .hxml檔案 使用 -D hello ,如果需要賦值為world則是 -D hello=world

    flashdevelop 可以在 專案屬性 -> 編譯器選項 -> 常規 -> DirectivesString[] Array 中新增一行 hello ,如果需要賦值為world則是 hello=world

    openfl 專案可以在 .xml 配置檔案中新增 <haxedef name="hello" />,如果需要賦值為world則為 <haxedef name="hello" value="world" />,

    注意: openfl 專案中設定 flashdevelop 的專案屬性會被忽略,只能通過 .xml 檔案來配置

    // 測試條件編譯標記
    #if hello
    trace("hello");
    #end
    

巨集

Tips: haxe 標準庫中有很多 工具類能更好地構建, 如: macro.TypeTools, macro.ExprTools, 以及其它以 Tools 結尾的類, 通常是使用 using 關鍵字新增這些工具類.

巨集方法

內容只適用於 haxe 3. 巨集相當於一個在編譯時執行的 neko 平臺.例如: 在編譯時輸出 --macro Sys.println('Hello macro!')

注意: 通常應該把巨集函式和其它函式分開放在不同檔案,否則程式碼中的很多地方要加上#if macro 這樣的條件編譯才能通過編譯.

注意: 給巨集函式傳引數時,引數應該是常量, 如果傳變數只能獲得 變數名 不能獲得 變數值(因為巨集編譯時賦值還沒發生).

  • 最簡單, macro 常量

    // 示例:    trace( tut_const() );    =>     相當於 trace("simple");
    macro public static function tut_const() {
        return macro "simple";
    }
    
  • 使用變數, macro $v{變數}, 注意: 變數型別不能為 Expr ,只能是簡單的資料型別,陣列,及 結構物件.

    // 示例:    trace( tut_variable() );    =>     trace("so easy!");
    macro public static function tut_variable() {
        var easy:String = "easy!";
        return macro "so " + $v{ easy };
    }
    
    // 示例:    trace( tut_array([1,2,3,4,5]) );    =>     trace([1,2,3,4,5,10]);
    macro public static function tut_array(arr:Array<Int>) {
        arr.push(10);
        return macro $v{arr};
    }
    
     //注意 ${ 變數 } ,不能是複雜資料型別,下邊語句 因為 編譯器不知道如何將 Map 型別轉換成 Expr
     macro public static function tut_map() {
        var map:Map<String,String> = new Map<String,String>();
        map.set('desc', 'some msg');
        try{
            return macro $v{map}; // 錯誤: Unsupported value {desc => "some msg"}
        }catch (err:Dynamic) {
            return macro $v{err};
        }
    }
    
  • Expr型別變數, macro $Expr變數, 加字首 $ 就行了,只能出現在 macro 的語句後邊

    你應該注意到了,即使巨集函式引數宣告為 Expr 型別,在呼叫這個函式時傳的值卻是 字串型別.

    如果引數為 Expr 時,編譯器會自動轉換這些直接常量為 Expr,然後在巨集函式內部 這個變數將會是 Expr 型別.

    // trace( tut_param('456') );   => trace('123456');
    macro public static function tut_param(param:Expr) {
    
        var str:String = "123"; 
    
        return macro $v{str} + $param;
    }
    
    // 這有個複雜的示例:
    // trace( repeat(10,5) )    =>      [10,10,10,10,10]
    macro public static function repeat(e : Expr, eN : Expr) {
        return macro [for( x in 0...$eN ) $e];
    }
    
    // Tips: 在巨集函式體中獲得 Expr 的值
    // getValue("hello");   then val = 'hello';
    macro public static function getValue(ep:Expr){
        var val = haxe.macro.ExprTools.getValue(ep)); //或者 using haxe.macro.ExprTools;
        return macro null;
    }
    
  • 處理檔案.

    這只是一個從檔案中解析資料的示例.

    或者你可以通過 template 以及 haxe.io.File 來動態將檔案資料寫成一個 class

    <?xml version="1.0" encoding="utf-8" ?>
    <root>
    <data lang="zh">
        <user name="小明" phone="123" addr="詳細地址描述" />
        <user name="小紅" phone="456" addr="不存在" />
    </data>
    <data lang="en">
        <user name="ming" phone="123" addr="expanded address details." />
        <user name="hong" phone="456" addr="..." />
    </data>
    </root>
    
    // example trace(tut_file("test.xml")) => [{ name : 小明, addr : 詳細地址描述, phone : 123 },{ name : 小紅, addr : 不存在, phone : 456 }]
    macro public static function tut_file(name:String) {
        var content = sys.io.File.getContent( Context.resolvePath(name) );
        var xml = Xml.parse(content);
        var fast = new haxe.xml.Fast(xml.firstElement());
    
        var ret = new Array<Dynamic>();
    
        for (data in fast.nodes.data) {
            if (data.att.lang == 'zh') {
                for (user in data.nodes.user) {
                    var obj:Dynamic<String> = { };    
                    for (k in user.x.attributes()) {                        
                        Reflect.setField(obj, k, user.att.resolve(k));
                    }
                ret.push(obj);
                }
            }    
        }
        return macro $v{ret};
    }   
    
  • 使用 Context.parseInlineString 用於解析字串程式碼, 多數ui庫,都使用這個方法來預處理 xml 配置檔案

    這個方法有一定的侷限性,通常使用類似的匿名函式,不可以包含 class 這些.

    //example: trace(tut_parse());  => {width => 800, lang => zh-CH, note => 測試, sprite => [object Sprite]}
    macro public static function tut_parse() {
        var str = "測試";    // Tip:在單引號字串中,可以使用 ${變數} 來引用一些變數, 
        var code:String = '
            function() {
                var map = new haxe.ds.StringMap<Dynamic>();
                map.set("note","${str}"); 
                map.set("width", Lib.current.stage.stageWidth);
                map.set("lang", flash.system.Capabilities.language);
                map.set("sprite", new flash.display.Sprite());
                return map;
            }()';
        return  haxe.macro.Context.parseInlineString(code,haxe.macro.Context.currentPos());
    }
    

小抄

class Main{
    static public function main(){
        trace( Um.tut_const() );
    }
}

class Um{
    macro public static function tut_const() {
        return macro "simple";
    }
}

如上示例: 當呼叫 巨集方法 Um.tut_const 時, Um.tut_const 的返回值將替換掉 Um.tut_const(), 也就是說 trace( Um.tut_const() ) 將替換成 trace("simple");

class Main {

    static function main() {
        var i = 1;
        var j = 2;
        var a = [1, 2];

        Um.callMethed(a);
        Um.assign(i);
        Um.noChange(j);

        trace(a); // 輸出: [1, 2, 77]
        trace(i); // 輸出: 88
        trace(j); // 輸出: 2 
    }

}

class Um{

    macro static public function callMethed(a) {
        return macro $i { haxe.macro.ExprTools.toString(a) }.push(77);
    }

    macro static public function assign(i) {
        return macro $i { haxe.macro.ExprTools.toString(i) } = 88;
    }

    macro static public function noChange(i) {
        macro $i { haxe.macro.ExprTools.toString(i) } = 99;
        return macro null;
    }
}

上邊是一個將變數傳遞給 巨集方法的示例(haxe 3.2 推薦使用 Context.getLocalTVars 來獲得本地變數,一,而不是通過巨集傳遞)

注意: 巨集方法 assign 和 noChange 只有返回值不一樣, 充分說明了 巨集返回值 將替換所 巨集呼叫. 這一概念很重要.

  • haxe.macro.ExprTools 類中的 toString 和 getValue 都是常用方法

  • 如何從巨集方法返回一個 bytes

    // 原始檔 h3d/hxd/res/Embed.hx
    // 先字串序列化 bytes ,然後 反序列化就行了
    public static macro function getResource( file : String ) {
        var path = Context.resolvePath(file);
        var m = Context.getLocalClass().get().module;
        Context.registerModuleDependency(m, path);  // 參考 Haxe 命令列, haxe -wait
        var str = haxe.Serializer.run(sys.io.File.getBytes(path));
        return macro hxd.res.Any.fromBytes($v{file},haxe.Unserializer.run($v{str}));
    }
    
  • macro.MacroType

    // 在 CastleDB 的 一個示例中, Data.hx 如下:
    package dat;
    
    private typedef Init = haxe.macro.MacroType < [cdb.Module.build("test.cdb")] > ;
    
  • macro 關鍵字後可以接任意 haxe 程式碼. AST

    // 輕鬆獲得一個型別. 對於巨集構建(@:build) 非常有幫助.
    var loaderType = macro : hxd.res.Loader;
    
    var method = macro {
        return flash.Lib.current.stage;
    }
    

Reification Escaping

The Haxe Compiler allows reification of expressions, types and classes to simplify working with macros.

The syntax for reification is macro expr, where expr is any valid Haxe expression.

巨集方法小節使用的就是這類語法, 不詳寫了,參考 相關小節

  • 所有下列 $引用 需要必須在 macro 語句後邊

    ${}$e{}: Expr -> Expr

    $a{} : Expr -> Array<Expr> 用於把數量未確定的引數傳遞給 作引數的函式, 示例見 Promhx 庫的 Promise.when

    $b{} : Array<Expr> -> Expr

    $i{} : String -> Expr 注: 這裡的 String 變數名字串.

    function main(){
        var abc = 100;
        trace( getIdent() ); // 巨集替後將為 trace(abc);
    }
    macro static function getIdent(i:Expr){
        return macro $i{"abc"};
    }
    

    $p{} : Array<String> -> Expr 同上. String 指的是變數名.

    $v{} : Dynamic -> Expr 這個應該是使用頻率最多的標記.

  • haxe 3.1

    欄位名 {$name : 1}

    函式名 function $name(){}

    try/catch try{e()}catch($name:Dynamic){}

    class Main {
        macro static function generateClass(funcName:String) {
            var c = macro class MyClass {
                public function new() { }
                public function $funcName() { //函式名
                    trace({ $funcName : "was called" }); //欄位名
                }
            }
            haxe.macro.Context.defineType(c); // 動態定義的類需要通過定義,外邊才可以引用.
            return macro new MyClass();
        }
    
        public static function main() {
            var c = generateClass("myFunc");
            c.myFunc();
        }
    }
    

Compiler

haxe.macro.Compiler 的一些方法, 這個類的方法執行在編譯時, 當編譯引數 --macro 後呼叫方法時,預設為這個包下的方法.

例如: 'haxe --macro define('SomeFlag','Hello')', 記得字串用 單引號 而不是 雙引號.

// 就像 haxe 編譯的引數 -cp, 將指定的路徑新增類路徑
addClassPath(path:String):Void;

//
addGlobalMetadata(pathFilter:String, meta:String, recursive:Bool = true, toTypes:Bool = true, toFields:Bool = false):Void

//
addMetadata(meta:String, className:String, field:String = null, isStatic:Bool = null):Void


// Adds a native library depending on the platform (eg : -swf-lib for Flash)
addNativeLib(name:String):Void


// 允許訪問指定的包, 例如: 預設情況下當目標平臺為 neko 時, 不允許訪問 flash 包, 參見 openfl 編譯選項
allowPackage(v:String):Void

// 像 haxe 編譯引數 -D  
define(flag:String, value:String = null):Void

// Exclude a given class or a complete package from being generated
// 手動排除給定的包或類名在編譯時, 由於編譯引數 -dce 的關係, 事實上你不用呼叫這個方法
exclude(pack:String, rec:Bool = true):Void


// Exclude classes listed in an extern file (one per line) from being generated.
// 讀取指定檔案, 排除 外部類清單 每行一個
excludeFile(fileName:String):Void

// 得到 -D 定義的值.
getDefine(key:Dynamic):Dynamic

// 未知. 估計是用於 程式碼 編輯器
getDisplayPos():Null<{pos:Int, file:String}>

// 得到編譯時輸出的絕對全檔名, 例如: haxe -main Main -swf main.swf, 那麼這個方法返回 D:\path\main.swf
getOutput():String

/**
    這個方法形為像在程式碼中的 import pack.*;
 @param rec     歸遞包含所有子項
 @param ignore  要忽略的類名陣列, 包含路徑在內的全名.
 @param classPaths pack 的所在路徑, TODO: 即使指定了這個路徑同樣需要以 -cp 的方式新增路徑,所以這個引數好像有 Bug, 因為如果指定了 -cp 也就不需要這個引數了.
*/
include(pack:String, rec:Bool = true, ignore:Array<String> = null, classPaths:Array<String> = null)

// JS 平臺. 嵌入 JS 檔案到 輸出端, 目前 haxe 只提供 jQuery 和 swfObject 二個 JS 檔案
includeFile(fileName:Dynamic):Dynamic;

/**
    防止 包 或類 或欄位 被 -dce 編譯引數刪除, 參見  命令列編譯引數 的 @:keep 元標記

    這個標記經常被以 @:keep 的形式新增到 類或靜態欄位上

@param path         包,模組或 子型別
@param paths        包,模組或 子型別 陣列, 就是 path 引數的陣列形式,
@param recursive    如果為 true, 將歸遞包含 sub-packages 針對 paths   
*/
keep(path:String = null, paths:Array<String> = null, recursive:Bool = true):Void

// 載入 flash 補丁檔案, 因該只能用於 flash 端的 http://r32.github.io//haxe/2014/05/10/tips-haxe-flash.html#patch-files
patchTypes(file:String):Void

// 移除指定欄位
removeField(className:String, field:String, isStatic:Bool = null):Void

// 自定義 js 輸出, bing 上自行搜尋教程.
setCustomJSGenerator(callb:JSGenApi -> Void):Void

// 設定欄位型別
setFieldType(className:String, field:String, type:String, isStatic:Bool = null):Void

// 設定輸出檔名, 絕對路徑全名包括磁碟. 例如編譯到 hello.swf, 那麼 輸出檔名為 X:\path\to\hello.swf
setOutput(fileOrDir:String):Void

Context

haxe.macro.Context 上下文,就是呼叫這個類方法的環境, 也就是說如果在 Test.hx 程式碼的 第 6 行呼叫了一個自定義的巨集方法例如: Um.some(), 那麼這第 6 行就是 巨集的上下文環境. Um.some 方法體內的 currentPos() 也只是返回第 6 行, getLocalModule 也只是返回 第 6 行處所在的 類模組.

package;

class Test{
    public static function(){
        trace("Context");
        Um.some();
    }   
}

class Um{
    macro static function some(){
        trace(haxe.Context.currentPos());       //輸出在編譯端: #pos(Test.hx:6: characters 2-7)
        trace(haxe.Context.getLocalModule());   //輸出在編譯端: Test
        return macro null;
    }
}

這個類的方法經常用於 巨集方法, 巨集構建中, 下列方法未註釋的請引數 API 文件描述.

// 新增資原始檔, haxe.Resource 類能獲得這個方法定義的資原始檔, 通過 -resource [email protected] 在編譯命令列定義
addResource(name:String, data:Bytes):Void

// 返回上下文的位置,
function currentPos():Position

// 定義新模組, 也就是像在一個 hx 檔案中寫幾個類或定義. 通常來說 defineType 就夠用了.
defineModule(modulePath:String, types:Array<TypeDefinition>, imports:Array<ImportExpr> = null, usings:Array<TypePath> = null):Void

// 定義一個型別 ,示例見: heaps 遊戲引擎 hxd.res.FileTree.hx 
defineType(t:TypeDefinition):Void

// 檢測是否存在一個 -D 定義
function defined(s:String):Bool

// 檢測 -D 定義的值, 如果僅定義標記,那麼預設為 字串 1, 
definedValue(key:String):String

// 丟擲編譯錯誤, 並中斷當前巨集呼叫
error(msg:String, pos:Position):Dynamic

// 丟擲編譯錯誤, 並中斷當前編譯
fatalError(msg:String, pos:Position):Dynamic

// Follows a type. See haxe.macro.TypeTools.follow for details.
follow(t:Type, once:Bool = null):Type

// 用於 巨集構建 @:build 或 @:autoBuild 所在上下文
getBuildFields():Array<Field>

// 得到編譯時類全部路徑,包括呼叫的 haxelib 所在路徑, -cp 包含的路徑, 甚至包含呼叫了 haxe 標準庫下的 std及各目標平臺的 _std 目錄路徑 
getClassPath():Array<String>

// Returns the constructor arguments that are used to construct the current @:genericBuild class, if available.
// Returns null if the current macro is not a build-macro which was called from constructing a @:genericBuild instance.
getConstructorArguments():Null<Array<Expr>>

// haxe 3.2+ 得到所有 -D 的定義
getDefines():Map<String, String>

// 
getExpectedType():Null<Type>

// 得到上下文所在 類名
getLocalClass():Null<Ref<ClassType>>

// 得到上下文所在 方法名
getLocalMethod():Null<String>

// 得到上下文所在 模組名(hx原始碼檔名,但不包含副檔名)
getLocalModule():String

//
getLocalTVars():Map<String, TVar>

//
getLocalType():Null<Type>

//
getLocalUsing():Array<Ref<ClassType>>

//
getLocalVars():Map<String, Type>

// 得到指定模組名(hx原始碼檔名,但不包含副檔名)所定義的型別陣列
getModule(name:String):Array<Type>

// 得到上下文所在位置資訊, 
// Tips: 通過這個方法可以得到一個 上下文所在的檔案路徑, 在自定義庫中載入資源很方便.
getPosInfos(p:Position):{min:Int, max:Int, file:String}

// 取得所有 -resource 定義的資源
getResources():Map<String, Bytes>

// 得到已經存在的指定型別, 如: getType("Int")
getType(name:String):Type

// Returns a syntax-level expression corresponding to typed expression t.
// This process may lose some information
getTypedExpr(t:TypedExpr):Expr

/**
Builds an expression from v, 自動解析 v 的值 構建相應的 Expr

This method generates AST nodes depending on the macro-runtime value of v. As such, only basic types and enums are supported and the behavior for other types is undefined

The provided Position pos is used for all generated inner AST nodes.
*/
makeExpr(v:Dynamic, pos:Position):Expr

// Builds a Position from inf.
makePosition(inf:{min:Int, max:Int, file:String}):Position

//
onAfterGenerate(callback:Void -> Void):Void

//
onGenerate(callback:Array<Type> -> Void):Void

//
onMacroContextReused(callb:Void -> Bool):Void

// 
onTypeNotFound(callback:String -> TypeDefinition):Void

// 解析字串程式碼, 實際上經常用 parseInlineString 代替這個方法
parse(expr:String, pos:Position):Expr

// 解析字串程式碼
parseInlineString(expr:String, pos:Position):Expr

// 註冊模組依賴, 以快取編譯, 如果你沒有使用 --wait --connect 快取編譯, 將沒有任何效果.
// 在 heaps 的 hxd.res 庫中經常能見到這個方法示例.
registerModuleDependency(modulePath:String, externFile:String):Void

// Add a macro call to perform in case the module is reused by the compilation cache.
registerModuleReuseCall(modulePath:String, macroCall:String):Void

// 從 classPaths 是否存在指定檔名, 並返回 絕對路徑全名. 參考 getClassPath 將會返回哪些類路徑
// 對於 windows 平臺會將 /path/to/file 這種 unix 斜線路徑轉成 windows 系統能識別的反斜線.
resolvePath(file:String):String

// 返回 MD5 值, 但我總覺得這個方法返回的 MD5 值不對.
signature(v:Dynamic):String

// 根據指定型別建立複雜型別 , 參看 makeExpr
// toComplexType( getType("Int") ) 等於 `macro :Int` 等於 `TPath({name:"StdTypes", pack:[], params:[], sub:"Int" })`
// 那麼簡單型別就是 `macro Int` 返回的值 `src/Main.hx:34: { expr => EConst(CIdent(Int)), pos =>...}`
toComplexType(t:Type):Null<ComplexType>

// Types expression e and returns the corresponding TypedExpr.
typeExpr(e:Expr):TypedExpr

// Types expression e and returns its type.
typeof(e:Expr):Type