實(shí)戰(zhàn):基于 plugins/example 從構(gòu)建、加載到自定義插件)
編程語言編譯器語言運(yùn)行時標(biāo)準(zhǔn)庫【免費(fèi)下載鏈接】haxeHaxe - The Cross-Platform Toolkit項目地址https://gitcode.com/gh_mirrors/ha/haxe點(diǎn)擊查看免費(fèi)下載本篇技術(shù)指南以 Haxe 倉庫中的示例插件目錄 plugins/example 為主體完整講解 Haxe 編譯器原生插件plugin機(jī)制的構(gòu)建、加載與擴(kuò)展流程。你將掌握如何用make plugin編譯 OCaml 動態(tài)庫插件、如何通過haxelib dev在宏中調(diào)用插件 API以及如何基于 example 模板快速創(chuàng)建一個自己的編譯器插件。插件機(jī)制是什么Haxe 編譯器的插件plugin是一種用 OCaml 編寫的動態(tài)加載庫在 Linux/macOS 上為.cmxs在 Windows 上為.dll。它運(yùn)行在編譯器進(jìn)程內(nèi)部可以直接訪問 Haxe 編譯器的內(nèi)部 API如haxe.macro.Context、類型化語法樹從而實(shí)現(xiàn)對編譯過程的深度定制。與普通的 Haxe 宏macro不同宏受限于 Haxe 暴露的haxe.macro.*API而原生插件可以接觸到底層的 OCaml 實(shí)現(xiàn)。example 插件的 ml/example.ml 就演示了這一點(diǎn)它直接修改編譯器的類型化語法樹AST把項目中所有名為test的靜態(tài)方法替換為拋出Hello from plugin異常。從源碼看插件的加載入口是宏上下文中的eval.vm.Context.loadPlugin。其底層實(shí)現(xiàn)位于 src/macro/eval/evalStdLib.ml#L630-L645它調(diào)用 OCaml 的Dynlink.loadfile加載動態(tài)庫并以文件路徑為鍵緩存已加載的插件避免重復(fù)加載let loadPlugin vfun1 (fun filePath - let filePath decode_string filePath in let filePath Dynlink.adapt_filename filePath in if PMap.mem filePath !plugins then PMap.find filePath !plugins else begin (try Dynlink.loadfile filePath with Dynlink.Error error - exc_string (Dynlink.error_message error)); match !plugin_data with | Some l - let vapi encode_obj_s l in plugins : PMap.add filePath vapi !plugins; vapi | None - vnull end )一個必須注意的前提見 std/eval/vm/Context.hx#L59-L63 的文檔說明插件必須使用與當(dāng)前 Haxe 編譯器相同的 OCaml 版本和 Haxe 版本編譯否則無法加載加載失敗時會拋出類型為String的異常。構(gòu)建插件make plugin PLUGINexampleplugins/example/README.md 給出的構(gòu)建命令只有一行$ make plugin PLUGINexample該命令只構(gòu)建當(dāng)前操作系統(tǒng)對應(yīng)的插件。要理解它的完整行為需要看 Makefile#L72-L75 中的plugin目標(biāo)plugin: haxe $(DUNE_COMMAND) build --profile release plugins/$(PLUGIN)/$(PLUGIN).cmxs mkdir -p plugins/$(PLUGIN)/cmxs/$(SYSTEM_NAME) cp -f _build/default/plugins/$(PLUGIN)/$(PLUGIN).cmxs plugins/$(PLUGIN)/cmxs/$(SYSTEM_NAME)/plugin.cmxs整個流程分為三步前置依賴plugin目標(biāo)依賴haxe目標(biāo)即先確保編譯器本身已通過dune build --profile release src/haxe.exe構(gòu)建完成編譯動態(tài)庫用 dune 以 release 配置構(gòu)建plugins/example/example.cmxs這是 OCaml 的可動態(tài)加載原生插件安裝產(chǎn)物創(chuàng)建plugins/example/cmxs/SYSTEM_NAME/目錄并把example.cmxs復(fù)制成該目錄下的plugin.cmxs固定文件名。SYSTEM_NAME由 Makefile#L32-L43 根據(jù)操作系統(tǒng)推導(dǎo)操作系統(tǒng)SYSTEM_NAME取值WindowsOSWindows_NTWindowsLinuxuname -s LinuxLinuxmacOSuname -s DarwinMac這個目錄布局并非隨意設(shè)計Haxe 側(cè)的加載邏輯會按同樣的規(guī)則計算插件路徑下文詳述。dune 構(gòu)建配置解析plugins/example/dune 定義了插件的構(gòu)建規(guī)則(data_only_dirs cmxs hx) (include_subdirs unqualified) (env (_ (flags -w -27 -w -50) ) ) (library (name example) (libraries haxe) )data_only_dirs cmxs hx把cmxs和hx目錄視為純數(shù)據(jù)目錄不參與 OCaml 編譯——hx里的.hx源碼與ml里的 OCaml 代碼是彼此獨(dú)立的(include_subdirs unqualified)允許子目錄中的 OCaml 文件ml/example.ml被當(dāng)前 library 直接包含因此庫名為example產(chǎn)出物為example.cmxs(libraries haxe)鏈接 haxe 編譯器自身的 OCaml 庫這正是插件能訪問編譯器內(nèi)部 API 的原因(flags -w -27 -w -50)屏蔽 OCaml 的兩類警告未使用變量、注釋內(nèi)未知標(biāo)簽等屬于示例插件慣用的寬松配置。在 Haxe 項目中使用插件第一步把插件注冊為 haxelib 庫plugins/example/README.md 給出的做法是使用haxelib dev$ haxelib dev example path/to/haxe/plugins/examplehaxelib dev會把本地目錄臨時注冊為一個開發(fā)版 haxelibpath/to/haxe/plugins/example是插件目錄的絕對路徑。為什么可以直接被 haxelib 識別因?yàn)樵撃夸洶瑯?biāo)準(zhǔn)的 haxelib.json{ name : example, url : http://haxe.org, license : MIT, description : Example Plugin, version : 0.1.0, releasenote : Initial release, classPath: hx, contributors : [example], tags: [plugin], dependencies : {} }其中關(guān)鍵字段是classPath: hx——它告訴 haxelib這個庫的 Haxe 源碼入口位于hx目錄因此項目里可以直接import或使用Example類。tags: [plugin]用于 haxelib 檢索version: 0.1.0可按需修改。第二步在宏中調(diào)用插件 APIREADME 給出了最小調(diào)用示例macro static public function testPlugin() { Example.plugin.hello(); return macro {} }這是一個宏函數(shù)當(dāng)編譯器在類型化階段遇到testPlugin()時它會加載 example 插件并執(zhí)行其hello方法然后返回空表達(dá)式macro {}即不產(chǎn)生任何替換。Haxe 側(cè)加載邏輯Example.macro.hx插件 API 的 Haxe 端封裝在 plugins/example/hx/Example.macro.hx 中。它先聲明了插件暴露的接口typedef ExamplePluginApi { function hello():Void; function stringifyPosition(p:haxe.macro.Expr.Position):String; function hijackStaticTest():Void; }然后通過懶加載的單例訪問插件static public var plugin(get,never):ExamplePluginApi; static function get_plugin():ExamplePluginApi { if(_plugin null) { try { _plugin eval.vm.Context.loadPlugin(getPluginPath()); } catch(e:Dynamic) { throw Failed to load plugin: $e; } } return _plugin; }getPluginPath()利用宏編譯期信息PosInfos提供當(dāng)前文件路徑計算插件動態(tài)庫的位置static function getPluginPath():String { var currentFile (function(?p:PosInfos) return p.fileName)(); var srcDir currentFile.directory().directory(); return Path.join([srcDir, cmxs, Sys.systemName(), plugin.cmxs]); }即從Example.macro.hx所在目錄向上兩級得到plugins/example再拼接cmxs/系統(tǒng)名/plugin.cmxs——這與make plugin的產(chǎn)物路徑plugins/example/cmxs/Linux/plugin.cmxs等完全對應(yīng)。由于該文件位于hx而非_std目錄它只在普通庫代碼編譯時生效不會污染標(biāo)準(zhǔn)庫。eval.vm.Context.loadPlugin的官方簽名見 std/eval/vm/Context.hx#L63static function loadPluginT(filePath:String):T;是一個泛型方法返回值類型由調(diào)用方?jīng)Q定。同一文件的文檔還給出了一個更底層的獨(dú)立用法示例——直接加載一個 OCaml 源碼編譯出的插件模塊testPlugin.cmo并調(diào)用其函數(shù)var module:TestPlugin eval.vm.Context.loadPlugin(testPlugin.cmo); trace(module.add_int(4, 3));深入OCaml 端如何實(shí)現(xiàn)與注冊插件 API插件真正的實(shí)現(xiàn)位于 plugins/example/ml/example.ml它定義了一個plugin對象class包含與 Haxe 端ExamplePluginApi一一對應(yīng)的三個方法。注意 Haxe 的駝峰命名stringifyPosition對應(yīng) OCaml 的下劃線命名stringify_positionHaxe 編譯器在編解碼 API 時會做自動轉(zhuǎn)換。hello最簡單的無參方法method hello () : value print_endline Hello from plugin; vnull直接向標(biāo)準(zhǔn)輸出打印Hello from plugin。注釋中特別指出即使 Haxe 側(cè)把方法類型聲明為Void插件架構(gòu)仍要求返回一個值這里返回vnull。stringifyPosition跨語言參數(shù)編解碼method stringify_position (pos:value) : value let pos EvalDecode.decode_pos pos in let str Lexer.get_error_pos (Printf.sprintf %s:%d:) pos in EvalEncode.encode_string str它接收 Haxe 的haxe.macro.Expr.Position值用EvalDecode.decode_pos解碼為編譯器的位置類型再用Lexer.get_error_pos格式化為與編譯器報錯完全一致的文本如文件路徑:行號:最后通過EvalEncode.encode_string編碼回 Haxe 的String。hijackStaticTest修改類型化語法樹這是最能體現(xiàn)插件能力的方法——它演示了如何像haxe.macro.Context.onAfterTyping一樣注冊類型化后回調(diào)把所有類型為TClassDecl的類中名為test的靜態(tài)方法替換為拋出一個字符串異常method hijack_static_test () : value let compiler (EvalContext.get_ctx()).curapi in compiler.after_typing (fun haxe_types - List.iter (fun hx_type - match hx_type with | TClassDecl cls - List.iter (fun field - match field.cf_name, field.cf_expr with | test, Some e - let hello { eexpr TConst (TString Hello from plugin); etype (compiler.get_com()).basic.tstring; epos Globals.null_pos; } in field.cf_expr - Some { e with eexpr TThrow hello } | _ - () ) cls.cl_ordered_statics | _ - () ) haxe_types ); vnull可以看到這里直接構(gòu)造了類型化 AST 節(jié)點(diǎn)TConst (TString ...)修改field.cf_expr為TThrow hello。這類操作在普通 Haxe 宏中也能做但插件方式可以直接接觸compiler.after_typing、TClassDecl、cl_ordered_statics等底層數(shù)據(jù)結(jié)構(gòu)適合需要精細(xì)控制編譯流程的高級場景。注冊 APIOCaml 文件末尾是插件的導(dǎo)出部分let api new plugin in EvalStdLib.StdContext.register [ (hello, EvalEncode.vfun0 api#hello); (stringifyPosition, EvalEncode.vfun1 api#stringify_position); (hijackStaticTest, EvalEncode.vfun0 api#hijack_static_test); ]這段代碼在eval.vm.Context.loadPlugin被調(diào)用時執(zhí)行動態(tài)庫被Dynlink加載后模塊級代碼立即運(yùn)行。register把方法名與方法值注冊進(jìn)插件上下文對應(yīng) src/macro/eval/evalStdLib.ml#L628-L629 的register即plugin_data : Some datavfun0/vfun1負(fù)責(zé)把 OCaml 函數(shù)包裝成 eval VM 可調(diào)用的值分別對應(yīng) 0 個、1 個參數(shù)后續(xù)loadPlugin返回的vapi就是 Haxe 側(cè)拿到的ExamplePluginApi對象。如何啟動一個新插件plugins/example/README.md 的建議非常直白Just make a copy of an example plugin directory and replace all occurrences of example word with your own plugin name.即復(fù)制plugins/example整個目錄并把所有出現(xiàn)example的地方替換為你自己的插件名。結(jié)合上文分析實(shí)際需要同步修改的至少包括構(gòu)建層面dune中的(name example)以及make plugin PLUGIN新名字的PLUGIN參數(shù)兩者必須一致產(chǎn)出物才是新名字.cmxsHaxe 端hx/Example.macro.hx中的類名Example、typedefExamplePluginApi以及加載路徑計算邏輯OCaml 端ml/example.ml中class plugin的實(shí)現(xiàn)與register的方法名列表分發(fā)層面haxelib.json中的name、description、version、contributors等元數(shù)據(jù)用于haxelib dev 新名字注冊構(gòu)建產(chǎn)物目錄重新執(zhí)行make plugin PLUGIN新名字后新目錄下會生成cmxs/SYSTEM_NAME/plugin.cmxs。替換后在目標(biāo)項目中執(zhí)行haxelib dev 新名字 path/to/haxe/plugins/新名字再于宏中通過新類名訪問plugin靜態(tài)屬性即可完成接入。限制與注意事項版本綁定嚴(yán)格插件必須與宿主 Haxe 編譯器使用相同的 OCaml 版本與 Haxe 版本編譯std/eval/vm/Context.hx#L59-L61編譯器升級后通常需要重新編譯插件平臺相關(guān)產(chǎn)物make plugin只構(gòu)建當(dāng)前 OS 的插件README 原文This command builds plugin for current OS only.cmxs/SYSTEM_NAME目錄本身就是為區(qū)分多平臺產(chǎn)物設(shè)計的跨平臺分發(fā)需在各平臺分別構(gòu)建錯誤處理Dynlink加載失敗時loadPlugin會拋出String類型異常如 plugins/example/hx/Example.macro.hx 中throw Failed to load plugin: $e宏調(diào)用端需要捕獲并給出可讀的錯誤信息調(diào)用時機(jī)插件加載發(fā)生在宏類型化階段Example.plugin屬性在宏函數(shù)內(nèi)被訪問時才觸發(fā)loadPlugin因此插件無法影響自身的加載過程只能在其后通過after_typing等回調(diào)干預(yù)編譯流程??偠灾瓾axe 的原生插件機(jī)制把編譯器能力開放到了 OCaml 層面example 插件就是一份完整的最小實(shí)現(xiàn)范本一條make命令構(gòu)建、兩處haxelib dev 宏調(diào)用即可接入、復(fù)制改名即可開啟新項目非常適合作為理解 Haxe 編譯器擴(kuò)展機(jī)制的起點(diǎn)。贊分享編程語言編譯器語言運(yùn)行時標(biāo)準(zhǔn)庫【免費(fèi)下載鏈接】haxeHaxe - The Cross-Platform Toolkit項目地址https://gitcode.com/gh_mirrors/ha/haxe點(diǎn)擊查看免費(fèi)下載相關(guān)推薦LightTable開發(fā)實(shí)戰(zhàn)從源碼編譯到自定義插件開發(fā)LightTable開發(fā)實(shí)戰(zhàn)從源碼編譯到自定義插件開發(fā) 本文全面介紹了LightTable編輯器的開發(fā)全流程從環(huán)境搭建、源碼編譯到自定義插件開發(fā)。詳細(xì)講解了開發(fā)工具IDETypeDoc 插件系統(tǒng)實(shí)戰(zhàn)指南從 --plugin 加載到自定義插件開發(fā)TypeDoc 插件系統(tǒng)實(shí)戰(zhàn)指南從 plugin 加載到自定義插件開發(fā) TypeDoc 的插件機(jī)制是其生態(tài)擴(kuò)展的核心通過 plugin 命令行參數(shù)或 plu開發(fā)工具文檔RetDec開發(fā)者指南構(gòu)建自定義反編譯插件實(shí)戰(zhàn)RetDec開發(fā)者指南構(gòu)建自定義反編譯插件實(shí)戰(zhàn) 引言解鎖RetDec的插件化能力 你是否曾遇到反編譯工具無法解析特定加殼文件的困境RetDec作為基于LL逆向工程靜態(tài)分析上一篇Ultimate Hacking Keyboard Agent打造專屬機(jī)械鍵盤的終極配置工具下一篇Spring庫文檔貢獻(xiàn)者獎勵貢獻(xiàn)者福利創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考