Command line processing in LLVM

English version

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類型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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的風格是--longllvm/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庫,就會觸發經典錯誤:

1
2
: 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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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++源文件中,可以這樣寫:

1
2
3
4
5
6
7
8
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容易引起混亂。

1
2
3
% 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-execclang -mstack-protector-guard=xxx -mstack-protector-guard=global中非最後選項的無效選項值無法被檢測到。