在 DLL (動態連接庫) 的開發中,管理和更新Exports(也就是那些被外部應用程式所調用的功能 )通常是一個令人頭痛的任務。尤其是當這些Exports的數量增加,或者需要頻繁的進行更改時。手動編輯和維護這些Exports可能非常耗時,且容易出錯。但有了Pre-build功能,這一切都可以自動化。
什麼是 Pre-build 事件?
Pre-build 事件是在主要的編譯和連接過程開始之前執行的命令或腳本。它為開發者提供了一個機會,在源代碼被編譯成二進位格式之前,對項目進行最後的調整或檢查。在軟體開發中,尤其是在大型項目中,編譯和建構過程可能涉及多個步驟和依賴項。有時候,在進行主要的編譯過程之前,我們可能需要執行某些預先的任務,如:更新資源、檢查依賴性或清理舊的檔案。這就是 Pre-build 事件發揮作用的地方。如何設定 Pre-build 事件?
我們可以在Delphi的IDE選單中找到「Project」 > 「Options」,並且在「Building」設定中找到「Build Events」後就可以看到今天的主角「Pre-build events」。任何的命令都可以設定在Commands欄位中,這些命令行接受多種變數,如:「$(OUTPUTDIR) 」、「 $(PROJECTNAME)」和「$(PROJECTDIR)」,它們在執行時會被替換為相應的值。這使你可以為不同的項目和設定建立通用的腳本和命令。使用 Delphi 的 TCustomAttribute 進行編譯前自動化
Delphi的提供了現代化的元編程特性,其中最有力的工具之一就是「TCustomAttribute」。這允許開發者在原始代碼中嵌入額外的元數據,而不會影響執行時的行為。利用這些特性,我們可以更簡單的實現編譯前的自動化。想像一下,當我們正在開發一個動態連接庫(DLL),我們希望某些類或函數能被自動導出。而不是手動管理這些列表,我們可以使用 [UseClass] 和 [UseFunction] 這樣的自定義屬性,直接標記需要導出的類別與方法。
例如:
[UseClass]
TAClass = class
public
[UseFunction]
function getAName(): string;
[UseFunction(InputData)]
function setAPosition(const position: string): string;
//getAType暫定不Exports出去
function getAType(): string;
end;
定義TCustomAttribute的方式:
APIMethod = (Empty, InputData);
UseClass = class(TCustomAttribute)
end;
UseFunction = class(TCustomAttribute)
private
FMethod: APIMethod;
public
constructor Create(const AMethod: APIMethod = Empty);
property Method: APIMethod read FMethod;
end;
設定自動化Script
在編譯前,我們可以運行一個腳本來掃描源代碼,尋找這些標記。基於找到的標記,腳本可以自動生成必要的 Exports 清單或其他所需的配置。以下是PowerShell的範例:
接著我們需要在Delphi專案中,使用「{$INCLUDE DLLMain.inc}」這樣的語句,就可以輕鬆地將這個.inc文件的內容包含到我們的主代碼中。這為我們提供了一個動態和靈活的方法來控制 DLL 的行為和特性,而不需要直接更改主代碼。
# 外部參數輸入
param (
[Parameter(Mandatory=$true)]
[string]$path
)
# 正則表達式
$unitPattern = '\bunit\s+([a-zA-Z0-9_]+);'
$classPattern = "(?s)\[UseClass\]\s+T(.*?)\s*(?::\s*\w+)?\s*=\s*class.*?\bend;"
$methodPatternFunction = '\[UseFunction\]\s*function\s+([^(:]+)'
$methodPatternFunctionInputData = '\[UseFunction\(InputData\)\]\s*function\s+([^(:]+)'
# 輸出檔案 DLLMain.inc
$outputFilePath = Join-Path $path "DLLMain.inc"
if (Test-Path $outputFilePath) {
Remove-Item $outputFilePath
}
# 初始化輸出參數
$usesUnits = [System.Collections.Generic.HashSet[string]]::new()
$functionsInterface = @()
$functionsImplementation = @()
$exportsList = @()
# 方法對照表
function GenerateFunctionDetails($attribute) {
switch ($attribute) {
# 可以繼續擴充其它規則
'InputData' {
@{
Parameters = 'const inputData: string';
InstanceCreate = "T$apiName.Create";
MethodCall = "instance.$method(inputData)";
}
}
default {
@{
Parameters = '';
InstanceCreate = "T$apiName.Create";
MethodCall = "instance.$method";
}
}
}
}
# 在指定目錄及其子目錄中遍歷所有的 .pas 檔案。
Get-ChildItem -Path $path -Recurse -Include *.pas | ForEach-Object {
$content = Get-Content $_.FullName -Raw
if ($content -match $classPattern) {
# 提取帶有[UseAPI]標籤的類別
$apiClassesMatches = [System.Text.RegularExpressions.Regex]::Matches($content, $classPattern)
# 取得該pas的Unit Name
if ($content -match $unitPattern) {
$unitname = $matches[1]
$usesUnits.Add("`n $unitname")
}
foreach ($match in $apiClassesMatches) {
$apiName = $match.Groups[1].Value.Trim()
$classContent = $match.Value
# 針對每種UseFunction屬性的模式和實現方法
@(
@{ Pattern = $methodPatternFunction; Attribute = 'Default' },
@{ Pattern = $methodPatternFunctionInputData; Attribute = 'InputData' }
) | ForEach-Object {
$patternInfo = $_
$methods = [System.Text.RegularExpressions.Regex]::Matches($classContent, $patternInfo.Pattern) | ForEach-Object {
$_.Groups[1].Value.Trim()
}
$methods | ForEach-Object {
$method = $_
$functionDetails = GenerateFunctionDetails $patternInfo.Attribute
$functionsInterface += "function $method($($functionDetails.Parameters)): string; stdcall;"
$funcImpl = @"
function $method($($functionDetails.Parameters)): string; stdcall;
var
instance: T$apiName;
begin
try
instance := $($functionDetails.InstanceCreate);
try
Result := $($functionDetails.MethodCall);
except
on E:Exception do begin
Result := E.ToString;
end;
end;
finally
if Assigned(instance) then FreeAndNil(instance);
end;
end;
"@
$functionsImplementation += $funcImpl
$exportsList += "`n $method"
}
}
}
}
}
# 格式化收集到的資訊
$usesFormatted = $($usesUnits -join ', ').TrimStart("`n, ")
$exportsFormatted = $($exportsList -join ', ').TrimStart("`n, ")
$output = @"
uses System.SysUtils,
//Class
$usesFormatted;
$($functionsInterface -join "`n")
exports
$exportsFormatted;
implementation
$($functionsImplementation -join "`n`n")
"@
# 將輸出寫入DLLMain.inc檔案。
Add-Content -Path $outputFilePath -Value $output -Encoding UTF8
unit DLLMain;
interface
{$INCLUDE DLLMain.inc}
end.
添加Pre-build Commands
最後將寫好的PowerShell加入到Pre-build的Commands內,並且帶入我們的專案參數,就完成Pre-build的功能囉!
powershell.exe -ExecutionPolicy Bypass -File ".\AutoBuildDLLMain.ps1" -Path "$(PROJECTDIR)"
在第一次編譯專案時,會跳出安全性警告詢問並且要你信任專案上設置的任何腳本。
💡 如果是不是自己規劃的專案,跳出這個訊息的話,請一定要確認它執行了什麼腳本哦!
編譯完成後,就可以在自己的專案下看到腳本自動產出的DLLMain.inc囉!
總結
以上就是全部的Pre-build的介紹,Delphi的Build Events功能為開發者提供了一個強大的工具,使他們能夠在建構過程的不同階段插入自定義的腳本或命令。這意味著開發者可以根據自己的需求進行專案的細節調整,從而實現更高級的自動化和效率哦!
0 Comments
張貼留言