實(shí)戰(zhàn):InsertAtEntry、InsertAtExit 與 ReplaceFuncBody 的兩次編譯插樁)
標(biāo)準(zhǔn)庫網(wǎng)絡(luò)密碼學(xué)并發(fā)編程【免費(fèi)下載鏈接】cangjie_stdx倉頡編程語言提供了 stdx 模塊該模塊提供了網(wǎng)絡(luò)、安全等領(lǐng)域的通用能力。項目地址https://gitcode.com/Cangjie/cangjie_stdx點(diǎn)擊查看免費(fèi)下載本篇指南基于 cangjie_stdx 倉庫的 AOP 開發(fā)示例文檔完整講解stdx.aspect_cj包提供的三個切面注解InsertAtEntry、InsertAtExit、ReplaceFuncBody的使用方法、兩次編譯收集 織入的cjc命令流程與預(yù)期運(yùn)行結(jié)果并結(jié)合 收集插件 與 織入插件 的源碼剖析切面信息文件.annoinfo的生成與織入時機(jī)。讀完本文你可以獨(dú)立完成“在指定函數(shù)入口/出口插樁、整體替換函數(shù)體”的 AOP 場景并理解其底層 CHIR 級別的實(shí)現(xiàn)機(jī)制。stdx.aspect_cj 概述注解 兩個編譯插件stdx.aspect_cj包提供了倉頡語言中面向切面編程Aspect Oriented Programming, AOP的注解能力配合libstdx.collect_aspects和libstdx.weave_aspects兩個編譯插件可以對目標(biāo)函數(shù)進(jìn)行入口插樁、退出前插樁、替換函數(shù)體。三個注解類的定義位于 aspect_cj.cj注解類切面能力關(guān)鍵參數(shù)InsertAtEntry在注解所指定方法的入口織入對被注解標(biāo)注函數(shù)的調(diào)用packageName、className、methodName、isStatic、recursive、funcTypeStrInsertAtExit在注解所指定方法的退出點(diǎn)織入對被注解標(biāo)注函數(shù)的調(diào)用同上ReplaceFuncBody將注解所指定方法的方法體整體替換為對被注解標(biāo)注函數(shù)的調(diào)用packageName、className、methodName、isStatic、recursive無funcTypeStr需要注意的參數(shù)語義注解標(biāo)注在“切面函數(shù)”你要織入進(jìn)去的代碼所在函數(shù)上而注解參數(shù)描述的是“被織入的目標(biāo)函數(shù)”packageName目標(biāo)函數(shù)所屬包名如default、std.coreclassName目標(biāo)函數(shù)是成員函數(shù)時填所屬類名是全局函數(shù)時填空字符串methodName目標(biāo)函數(shù)名isStatic目標(biāo)函數(shù)是否為靜態(tài)成員函數(shù)recursive目標(biāo)函數(shù)是成員函數(shù)時是否對子類 override 版本也織入全局函數(shù)場景應(yīng)填falsefuncTypeStr僅 Insert 系列目標(biāo)函數(shù)的類型字符串不含空格自定義類型需寫全包名如(Int64,std.core.Object)-Unit不含隱式this形參。這兩個編譯插件以動態(tài)庫形式隨 stdx 提供不同平臺對應(yīng).so/.dll/.dylib使用流程固定為兩步第一次編譯使用libstdx.collect_aspects編譯時收集所有切面、連接點(diǎn)信息落盤為.annoinfo文件第二次編譯使用libstdx.weave_aspects讀取已收集的信息把切面織入到連接點(diǎn)。包概述文檔 中同時注明stdx.aspect_cj當(dāng)前處于開發(fā)階段尚不具備完整功能。下文所有用法均以當(dāng)前倉庫的文檔與源碼為準(zhǔn)。InsertAtEntry函數(shù)入口插樁示例下面是使用InsertAtEntry在指定函數(shù)入口插樁的完整示例文件aop_demo1.cjpackage AOP_demo1 import stdx.aspect_cj.* import std.time.DateTime InsertAtEntry[packageName: AOP_demo1, className: , methodName: printCurrentTime, isStatic: false, recursive: false, funcTypeStr: ()-Unit] public func printCurrentTimeImpl() { println(----- ${DateTime.now()} -----) } public func printCurrentTime() { println(hi) println(bye) } main() { printCurrentTime() 0 }示例中printCurrentTimeImpl是切面函數(shù)InsertAtEntry參數(shù)指明目標(biāo)為同包下的全局函數(shù)printCurrentTimeclassName為空、isStatic: falsefuncTypeStr: ()-Unit與目標(biāo)函數(shù)簽名一致??椚牒竺看握{(diào)用printCurrentTime時函數(shù)體第一條語句就變?yōu)閷rintCurrentTimeImpl的調(diào)用。Linux 平臺編譯命令需設(shè)置CANGJIE_STDX_PATH指向 stdx 安裝路徑cjc aop_demo1.cj -L $CANGJIE_STDX_PATH --import-path $CANGJIE_STDX_PATH -lstdx.aspect_cj --plugin$CANGJIE_STDX_PATH/libstdx.collect_aspects.so -o main # 第一次編譯收集切面 cjc aop_demo1.cj -L $CANGJIE_STDX_PATH --import-path $CANGJIE_STDX_PATH -lstdx.aspect_cj --plugin$CANGJIE_STDX_PATH/libstdx.weave_aspects.so -o main # 第二次編譯織入切面運(yùn)行結(jié)果可能如下----- 2025-06-03T00:00:18.994507-07:00 ----- hi bye時間戳先于hi/bye打印正說明插樁代碼被織入到了函數(shù)入口。從源碼看織入插件對InsertAtEntry的處理是在目標(biāo)函數(shù)體的entryBlock起始位置創(chuàng)建一次createApply調(diào)用見 weave_aspects.cj因此該調(diào)用會先于原函數(shù)體任何語句執(zhí)行。InsertAtExit函數(shù)退出前插樁示例下面是使用InsertAtExit在指定函數(shù)退出前插樁的完整示例文件aop_demo2.cjpackage AOP_demo2 import stdx.aspect_cj.* import std.time.DateTime InsertAtExit[packageName: AOP_demo2, className: , methodName: printCurrentTime, isStatic: false, recursive: false, funcTypeStr: ()-std.core.String] public func printCurrentTimeImpl() { println(----- ${DateTime.now()} -----) } public func printCurrentTime() { println(hi) println(bye) done } main() { println(printCurrentTime()) 0 }與入口插樁的差異點(diǎn)目標(biāo)函數(shù)printCurrentTime返回std.core.String因此funcTypeStr寫為()-std.core.String切面函數(shù)printCurrentTimeImpl本身返回UnitInsert 系列要求切面函數(shù)返回類型為Unit。編譯命令與上文相同僅文件名改為aop_demo2.cjcjc aop_demo2.cj -L $CANGJIE_STDX_PATH --import-path $CANGJIE_STDX_PATH -lstdx.aspect_cj --plugin$CANGJIE_STDX_PATH/libstdx.collect_aspects.so -o main # 第一次編譯收集切面 cjc aop_demo2.cj -L $CANGJIE_STDX_PATH --import-path $CANGJIE_STDX_PATH -lstdx.aspect_cj --plugin$CANGJIE_STDX_PATH/libstdx.weave_aspects.so -o main # 第二次編譯織入切面運(yùn)行結(jié)果可能如下hi bye ----- 2025-06-03T00:04:59.996469-07:00 ----- done時間戳出現(xiàn)在hi/bye之后、返回值被打印之前即插樁發(fā)生在函數(shù)退出前。從源碼看織入插件通過InsertAtExitVisitor遍歷目標(biāo)函數(shù)的 CHIR 中間表示對每一個exit表達(dá)式在其之前插入切面函數(shù)調(diào)用見 InsertAtExitVisitor因此無論函數(shù)有多少個出口多個 return、異常路徑等每個出口前都會觸發(fā)插樁。ReplaceFuncBody替換函數(shù)體示例下面是使用ReplaceFuncBody將指定函數(shù)體替換為切面函數(shù)調(diào)用的完整示例文件aop_demo3.cjpackage AOP_demo3 import stdx.aspect_cj.* import std.time.DateTime ReplaceFuncBody[packageName: AOP_demo3, className: , methodName: printCurrentTime, isStatic: false, recursive: false] public func printCurrentTimeImpl(original: (String) - String) { println(----- ${DateTime.now()} -----) println(original(abc)) println(----- end -----) def } public func printCurrentTime(x: String): String { println(x) 456 } main() { println(printCurrentTime(123)) 0 }ReplaceFuncBody與 Insert 系列有兩處本質(zhì)區(qū)別注解參數(shù)中沒有funcTypeStr——目標(biāo)函數(shù)類型由切面函數(shù)最后一個形參閉包類型隱式確定。規(guī)格要求切面函數(shù)必須額外添加一個形參其類型與被織入函數(shù)相同代表被織入函數(shù)原始版本的閉包本例即original: (String) - String切面函數(shù)的返回類型必須與被織入函數(shù)相同本例均為String。編譯命令同上文件名為aop_demo3.cjcjc aop_demo3.cj -L $CANGJIE_STDX_PATH --import-path $CANGJIE_STDX_PATH -lstdx.aspect_cj --plugin$CANGJIE_STDX_PATH/libstdx.collect_aspects.so -o main # 第一次編譯收集切面 cjc aop_demo3.cj -L $CANGJIE_STDX_PATH --import-path $CANGJIE_STDX_PATH -lstdx.aspect_cj --plugin$CANGJIE_STDX_PATH/libstdx.weave_aspects.so -o main # 第二次編譯織入切面運(yùn)行結(jié)果可能如下----- 2025-06-03T00:04:59.996469-07:00 ----- abc 456 ----- end ----- def逐行解讀main調(diào)用printCurrentTime(123)但函數(shù)體已被替換為對printCurrentTimeImpl的包裝調(diào)用——先打印時間戳original(abc)執(zhí)行原始函數(shù)體打印abc返回456再打印456隨后打印----- end -----切面函數(shù)返回def作為替換后printCurrentTime的返回值最終被main打印。從源碼看織入插件對ReplaceFuncBody的處理是為原函數(shù)新建一個同簽名的 wrapper 函數(shù)替換它wrapper 體中分配返回槽位、調(diào)用切面函數(shù)把原函數(shù)自身作為最后一個實(shí)參傳入、再把結(jié)果寫入返回槽位同時原函數(shù)的符號名會被改寫為附加.original后綴見 weave_aspects.cj從而保證原始實(shí)現(xiàn)仍可通過閉包被調(diào)用。收集與織入兩階段管線的底層機(jī)制第一階段collect_aspects 收集切面信息第一次編譯時libstdx.collect_aspects.so中的CollectAspects插件collect_aspects.cj會遍歷當(dāng)前包的所有函數(shù)對帶 AOP 注解的函數(shù)執(zhí)行一系列合法性檢查任一檢查失敗即編譯報錯例如同一個函數(shù)不能同時標(biāo)注InsertAtEntry/InsertAtExit和ReplaceFuncBodycheckAndCollect帶注解的函數(shù)必須是public、具有外部鏈接且不能是foreign、抽象函數(shù)或泛型函數(shù)/泛型類型的成員checkAspectFunction定義了切面的包中只允許定義基本類型整型、浮點(diǎn)、Rune、Bool字面值的全局變量否則報錯globalVarsArePrimitiveLiterals實(shí)例成員函數(shù)不能織入全局函數(shù)或靜態(tài)成員函數(shù)recursive為true時className不可為空checkAnnotationArgsInsert 系列切面函數(shù)返回類型必須是Unit且形參列表須與目標(biāo)函數(shù)一致checkInsertAnnoFuncTypeReplaceFuncBody切面函數(shù)至少 1 個形參最后一個形參必須是函數(shù)類型且其返回類型與目標(biāo)函數(shù)返回類型一致checkReplaceBodyAnnoFuncType。檢查通過后插件把每條切面記錄寫入.annoinfo文件文件名為當(dāng)前包名-目標(biāo)包名.annoinfowriteAnnoInfoToFile每條記錄按行依次存儲 13 個字段當(dāng)前包名、包初始化函數(shù) mangled 名、注解類名隨后是 5 個“連接點(diǎn)”字段目標(biāo)包名、方法 mangled 名、形參數(shù)量、是否成員函數(shù)、是否靜態(tài)和 6 個“目標(biāo)”字段目標(biāo)包名、類名、方法名、函數(shù)類型字符串、是否靜態(tài)、是否遞歸。文件名中編碼“目標(biāo)包名”是關(guān)鍵設(shè)計它決定了這條切面信息在哪個包的編譯過程中被消費(fèi)。第二階段weave_aspects 織入切面第二次編譯時libstdx.weave_aspects.so中的WeaveAspects插件weave_aspects.cj的工作流程見 run讀取切面信息遍歷當(dāng)前目錄僅讀取文件名匹配*當(dāng)前包名.annoinfo的記錄readAnnoInfo。這解釋了為什么示例中切面函數(shù)與目標(biāo)函數(shù)位于同一個包——被織入的包編譯時才會消費(fèi)對應(yīng)記錄調(diào)用上游包初始化在當(dāng)前包的 package init 入口開頭織入對所有相關(guān)包初始化函數(shù)的調(diào)用callUpstreamPkgInitFunc確保切面所在包的靜態(tài)初始化先于運(yùn)行期執(zhí)行匹配連接點(diǎn)對當(dāng)前包每個函數(shù)用包名、類名含recursive時沿繼承鏈向上匹配、方法名、是否靜態(tài)、完整函數(shù)類型字符串逐項比對meets執(zhí)行織入按注解類型分別走“入口插入 apply”、“exit 前插入 apply”或“替換為 wrapper 調(diào)用”三條路徑weaveFuncBody。若目標(biāo)函數(shù)不在當(dāng)前包插件會自動補(bǔ)一個imported函數(shù)聲明再發(fā)起調(diào)用getCallee。兩個插件本身通過stdx.plugin的CHIRPlugin宏注冊宏會為其生成繼承CHIRPluginBase的初始化邏輯并在靜態(tài)初始化時調(diào)用PluginManager.registerCHIRPluginregister.cj。編譯器加載--plugin指定的動態(tài)庫后最終經(jīng)由executeCHIRPlugins反序列化包級 CHIR、依次運(yùn)行所有已注冊插件再重新序列化輸出execute.cj。這也解釋了兩次編譯命令完全相同、僅--plugin參數(shù)不同即可切換“收集/織入”階段的原因。規(guī)格與約束速查以下約束匯總自 stdx.aspect_cj 包概述使用時務(wù)必遵守標(biāo)注范圍注解暫不支持標(biāo)注在泛型函數(shù)上也不支持織入泛型函數(shù)注解只允許標(biāo)注在public函數(shù)上注解可以標(biāo)注在全局函數(shù)上支持織入另一個全局函數(shù)、實(shí)例成員函數(shù)、靜態(tài)成員函數(shù)注解可以標(biāo)注在靜態(tài)成員函數(shù)上支持織入全局函數(shù)、實(shí)例成員函數(shù)、靜態(tài)成員函數(shù)注解可以標(biāo)注在實(shí)例成員函數(shù)上支持織入同類型的其它實(shí)例成員函數(shù)。全局變量定義約束定義了切面的包中只允許定義基本類型整型、浮點(diǎn)、Rune、Bool字面值的全局變量否則編譯報錯若需要使用超規(guī)格的全局變量需要將其定義在另外的包再導(dǎo)入使用以避免編譯后可能產(chǎn)生的循環(huán)依賴。形參約束被標(biāo)注InsertAtEntry/InsertAtExit的函數(shù)其返回類型只能是Unit被標(biāo)注ReplaceFuncBody的函數(shù)其返回類型應(yīng)和被織入的函數(shù)的返回類型相同被標(biāo)注的函數(shù)如果沒有形參織入后總是被無參調(diào)用被標(biāo)注的函數(shù)如果有形參其形參列表應(yīng)和被織入函數(shù)的源碼形參列表相同。特別地如果被織入的函數(shù)是實(shí)例成員函數(shù)需要將this參數(shù)顯式地寫在形參中對于被標(biāo)注ReplaceFuncBody的函數(shù)需要額外添加一個形參形參的類型和被織入的函數(shù)相同該形參表示被織入函數(shù)原始版本的閉包。參考路徑示例文檔doc/libs_stdx/aspect_cj/aspect_cj_samples/aspect_cj_sample.md包概述與規(guī)格doc/libs_stdx/aspect_cj/aspect_cj_package_overview.md注解類 APIdoc/libs_stdx/aspect_cj/aspect_cj_package_api/aspect_cj_package_classes.md注解定義源碼src/stdx/aspect_cj/aspect_cj.cj收集插件src/stdx/aspect_cj/plugins/collect_aspects/collect_aspects.cj織入插件src/stdx/aspect_cj/plugins/weave_aspects/weave_aspects.cj插件注冊宏src/stdx/plugin/register.cj插件執(zhí)行入口src/stdx/plugin/manager/execute.cj贊分享標(biāo)準(zhǔn)庫網(wǎng)絡(luò)密碼學(xué)并發(fā)編程【免費(fèi)下載鏈接】cangjie_stdx倉頡編程語言提供了 stdx 模塊該模塊提供了網(wǎng)絡(luò)、安全等領(lǐng)域的通用能力。項目地址https://gitcode.com/Cangjie/cangjie_stdx點(diǎn)擊查看免費(fèi)下載相關(guān)推薦stdx.aspect_cj 注解類 API倉頡語言 AOP 函數(shù)插樁與替換實(shí)戰(zhàn)指南stdx.aspect_cj 注解類 API倉頡語言 AOP 函數(shù)插樁與替換實(shí)戰(zhàn)指南 stdx.aspect_cj 包是 cangjie_stdx 中提供面向標(biāo)準(zhǔn)庫網(wǎng)絡(luò)密碼學(xué)并發(fā)編程倉頡 stdx.aspect_cj 面向切面編程實(shí)戰(zhàn)注解定義切面編譯期兩遍插樁與函數(shù)體替換倉頡 stdx.aspect_cj 面向切面編程實(shí)戰(zhàn)注解定義切面編譯期兩遍插樁與函數(shù)體替換 本文圍繞 stdx.aspect_cj 包展開講解如何用 I標(biāo)準(zhǔn)庫網(wǎng)絡(luò)密碼學(xué)并發(fā)編程AFL LLVM 編譯器級插樁llvm_mode實(shí)戰(zhàn)指南構(gòu)建、插樁選項與高級特性AFL LLVM 編譯器級插樁llvm_mode實(shí)戰(zhàn)指南構(gòu)建、插樁選項與高級特性 本文系統(tǒng)講解 AFL 基于 LLVM 的編譯器級插樁方案llv應(yīng)用安全測試漏洞掃描創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考