LLVM命令行选项的处理

题外话:不知不觉,达成了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的风格是--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库,就会触发经典错误:

1
2
: CommandLine Error: Option 'help-list' registered more than once!
LLVM ERROR: inconsistency in registered CommandLine options

在Clang里如果要设置cl::opt变量的值,可以用-mllvm -option=value。使用LLD/LLVMgold.so Full/Thin LTO也可以设置这些选项值,用-plugin-opt=-option=value(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">;
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();

注意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:)

再拓展一下,很多getopt_long用户用一个switch加大量case处理命令行选项,很容易弄出各种各样position dependent行为。
对于大型build system,有时候搞不清楚compiler/linker options是在什么地方添加的,有些position dependent行为挺讨厌的。