Command line processing in LLVM
2023-05更新
題外話:不知不覺,達成了llvm-project 1900 commits的成就。
LLVM中命令行選項的處理有兩個庫。
llvm/Support/ComandLine.h
文檔參見https://llvm.org/docs/CommandLine.html
簡單來說,用全局變量(llvm::cl::opt<type> var最常見,也有llvm::cl::list等)表示命令行選項。opt的構造函數會在一個全局的registry中註冊這個命令行選項。
在main中調用llvm::cl::ParseCommandLineOptions(argc, argv, ...)解析命令行。
opt支持很多類型,如各種integer types、bool、std::string等,還支持自定義enum類型。
static cl::OptionCategory cat("split-file Options");
static cl::opt<std::string> input(cl::Positional, cl::desc("filename"),
cl::cat(cat));
static cl::opt<std::string> output(cl::Positional, cl::desc("directory"),
cl::value_desc("directory"), cl::cat(cat));
static cl::opt<bool> noLeadingLines("no-leading-lines",
cl::desc("Don't preserve line numbers"),
cl::cat(cat));
int main(int argc, const char **argv) {
cl::ParseCommandLineOptions(argc, argv, ...);
}
LLVM中有很多開發者使用的命令行選項,除了功能選項外,還有:
- 對某一pass有較大改動,in-tree開發時爲了防止衰退,設置一個預設爲false的enable變量
- 一段時間功能穩定,把預設值改爲true。在某些場合下發現衰退的用戶可以使用false作爲workaround
- 給一個pass提供更多輸入,用於測試
這個庫使用便捷,添加一個新選項只需要在一個局部文件中加一個變量。還提供了一些錦上添花的小功能,如推薦拼寫接近的選項。 但命令行解析的定製性很弱。比如:
- 一個
cl::opt<bool>選項接受-v 0 -v=0 -v false -v=false -v=False等多種輸入方式 - 不便同時支持
--long和--no-long。偶爾有需求時的workaround是給--no-long也設置一個變量。假如要處理兩個選項互相override,就要判斷兩個選項在命令行中的相對位置
面向用戶的外部工具往往有這類定製需求。GNU getopt_long的風格是--long。
llvm/Support/ComandLine.h裏-long和--long可以混用,很長一段時間不支持強制--。
LLVM binary utilities (llvm-nm、llvm-objdump、llvm-readelf等)爲了替代GNU binutils,
需要提供POSIX shell utilities風格的grouped short options (-ab表示-a -b)。
很長一段時間這個功能不被支持,困擾了想要遷移到LLVM binary utilities的用戶。
另外cl::opt是singleton,也可以定義局部變量動態增加選項,但這種用法很少見(llvm-readobj和llvm-cov)。
還有個很奇特的用法,opt工具中legacy pass manager自動獲取pass name列表,並註冊大量全局選項。
爲了防止錯誤,cl::opt不支持多次定義同一個選項。如果同時鏈接了shared object和archive兩種LLVM庫,就會觸發經典錯誤:
: CommandLine Error: Option 'help-list' registered more than once!
LLVM ERROR: inconsistency in registered CommandLine options
在Clang裏如果要設置cl::opt變量的值,可以用-mllvm -option=value。使用ld.lld/LLVMgold.so Full/Thin LTO也可以設置這些選項值,用-plugin-opt=-option=value(ld.lld也可用-mllvm)。
llvm/Option/OptTable.h
原先給Clang開發,後來移入llvm,被llvm-objcopy、lld、llvm-symbolizer等採用。
用一個domain-specific language (TableGen)描述選項,生成一個parser。解析過的選項組織成一個object,每個選項用一個integer表示。
檢查一對預設值不定的boolean選項(--demangle --no-demangle)是否生效很容易:Args.hasFlag(OPT_demangle, OPT_no_demangle, !IsAddr2Line)。
multiclass B<string name, string help1, string help2> {
def NAME: Flag<["--", "-"], name>, HelpText<help1>;
def no_ # NAME: Flag<["--", "-"], "no-" # name>, HelpText<help2>;
}
multiclass Eq<string name, string help> {
def NAME #_EQ: Joined<["--", "-"], name #"=">,
HelpText<help>;
def: Separate<["--", "-"], name>, Alias<!cast<Joined>(NAME #_EQ)>;
}
defm debug_file_directory: Eq<"debug-file-directory", "Path to directory where to look for debug files">, MetaVarName<"<dir>">;
defm default_arch: Eq<"default-arch", "Default architecture (for multi-arch objects)">;
defm demangle: B<"demangle", "Demangle function names", "Don't demangle function names">;
def functions: F<"functions", "Print function name for a given address">;
在C++源文件中,可以這樣寫:
opt::InputArgList Args = parseOptions(argc, argv, IsAddr2Line, Saver, Tbl);
LLVMSymbolizer::Options Opts;
...
Opts.DebugFileDirectory = Args.getAllArgValues(OPT_debug_file_directory_EQ);
Opts.DefaultArch = Args.getLastArgValue(OPT_default_arch_EQ).str();
Opts.Demangle = Args.hasFlag(OPT_demangle, OPT_no_demangle, !IsAddr2Line);
Opts.DWPName = Args.getLastArgValue(OPT_dwp_EQ).str();Grouped short options
注意GCC的命令行選項不支持grouped short options,因此Clang也沒有需求。很長一段時間因爲缺少這個功能限制了它的使用場景。我在2020年7月加入了grouped short options (D83639)。
軼聞:LLD採用這個庫解析命令行選項。GNU ld實際上支持grouped short options,比如ld.bfd -vvv表示-v -v -v。我提出GNU ld實際上支持很多-long風格的選項,再支持grouped short options容易引起混亂。
% touch an ommand ':)'
% ld.bfd -you -can -ofcourse -use -this -Long -command -Line ':)'
:)
binutils 2.36有望deprecate grouped short options:)
Target-specific options
在Clang中,clang/include/clang/Driver/Options.td聲明了通用選項和目標特定選項。
優點是,如果一個有用的功能在GCC中是機器特定的,在Clang中很容易將其實現爲與目標無關的選項。
缺點是,如果它是一個固有的目標特定選項,很容易忘記爲其他目標報告錯誤。
通常會有一個-Wunused-command-line-argument警告,但警告可能不夠好。
例如,GCC的powerpc端口不支持-march=,但Clang錯誤地解析並忽略了它,導致-Wunused-command-line-argument警告。
https://reviews.llvm.org/D145141將警告改爲錯誤。
爲了防止上述問題,在處理clang/include/clang/Driver/Options.td中的目標特定選項時,我們應該添加標註兼容clang::driver::ToolChain的能力。
與getopt_long的比較
很多getopt_long用戶用一個switch加大量case處理命令行選項,很容易弄出各種各樣position dependent行爲。
對於大型build system,有時候搞不清楚compiler/linker options是在什麼地方添加的,有些position dependent行爲挺討厭的。
Clang中廣泛使用的Args.getLastArgValue(..., ...)模式有一個限制。
對於接受值的選項,我們通常只驗證最後一個選項,忽略之前的選項。
例如,clang -ftls-model=xxx -ftls-model=initial-exec和clang -mstack-protector-guard=xxx -mstack-protector-guard=global中非最後選項的無效選項值無法被檢測到。