llvm CommandLine 库的实现(其二)

前一节 已经介绍了llvm CommandLine库的使用,本节将探讨该库的实现细节。

  1. 首先我们先介绍下在构造cl::optcl::list等类时用到的modifier,看看这些修饰是如何作用到option上的。
  2. 然后我们再深入cl::optcl::listcl::bits以及cl::alias四个顶层类的接口设计与实现。对于C++代码的阅读,我个人的看法是一种接口设计的艺术,了解每个类提供的接口,再探索下接口的实现基本就能掌握代码的逻辑了。
  3. 最后我们将介绍这几个顶层类在解析选项值时用到的option parser

modifier修饰符(选项属性+选项修饰)的实现及应用

cl::optcl::list等类的构造函数中出现的参数统称为修饰符(modifier),在源代码层面都认为是modifier,但是在一些官方手册中会将其细分为Option AttributeOption Modifer,这一点需要注意。Option Attribute修饰符是一些类,需要构造这些类来携带特定信息;而Option Modifier则是一类标记,所以从实现角度这样细分是合理的(虽然使用上并不需要关心。modifier的作用就是记录选项的一些特征,比如你在设计一个选项时肯定要说明它的名字,是否有值,是否有默认值等。

选项属性

除了名字属性可以是一个普通字符串外,其余选项属性都是一个class。这些class都实现了一个apply接口:

void apply(Option &O) const { O.xxx; } //将本属性的值apply到选项O上

顶层类在构造时会调用apply接口将属性或者标记添加到option类中,CommandLineParser在解析命令行字符串时就可以根据option类记录的信息来合理解析选项了。

cl::desc属性类

描述选项信息的字符串,内部就是一个简单字符串。apply接口简单地将字符串添加到Option中:

struct desc {
    StringRef Desc;
    desc(StringRef Str) : Desc(Str) {}
    void apply(Option &O) const { O.setDescription(Desc); }
}

cl::value_desc属性类

描述选项值的字符串,实现同cl::desc

struct value_desc {
    StringRef Desc;
    value_desc(StringRef Str) : Desc(Str) {}
    void apply(Option &O) const { O.setValueStr(Desc); }
}

cl::initcl::list_init属性类

这些属性设置选项的默认值,并且要能初始化不同数据类型的选项。一个直观的想法是将cl::init实现为一个类模板,但是类模板不具有模板参数推导能力,每次使用都得显示声明默认值的类型,像下面这样:

cl::init<int>(10);
cl::init<double>(10.0);
cl::init<std::string>("-");

这样实现略显麻烦,由于模板函数可以进行参数推导,所以llvm的做法是用模板函数封装模板类。实现方式如下:

  1. 定义描述初始值的模板类,该类实现apply接口:

    template <class Ty> struct initializer {
        const Ty &Init;
        initializer(const Ty &Val) : Init(Val) {}
        template <class Opt> void apply(Opt &O) const { O.setInitialValue(Init); }
    }
    
  2. 使用模板函数封装该类,函数接收初始值并自动推导类型,构造并返回上面的模板类:

    template <class Ty> initializer<Ty> init(const Ty &Val) {
        return initializer<Ty>(Val);
    }
    

cl::list_initcl::init的实现类似,不同的是内部初始值是一个数组:

template <class Ty> struct list_initializer {
    ArrayRef<Ty> Inits;
    list_initializer(ArrayRef<Ty> Vals) : Inits(Vals) {}
    template <class Opt> void apply(Opt &O) const { O.setInitialValues(Inits); }
}

template <class Ty> list_initializer<Ty> list_init(ArrayRef<Ty> Vals) {
    return list_initializer<Ty>(Vals);
}

cl::location属性类

location属性允许用户定义外部变量存储选项值的解析结果,而不必保存在选项内部。该属性可根据外部变量自动推导类型,返回对应的属性类。因此也是通过模板类+模板函数实现:

  1. 模板类为LocationClass,内部一个外部变量的引用,实现了apply接口:

    template <class Ty> struct LocationClass {
        Ty &Loc;
        LocationClass(Ty &L) : Loc(L) {}
        template <class Opt> void apply(Opt &O) const { O.setLocation(O, Loc);}
    }
    
  2. 模板函数如下:

    template <class Ty> LocationClass<Ty> location(Ty &L) {
        return LocationClass<Ty>(L);
    }
    

cl::cat属性类

cat属性指定选项属于哪一个类别,这样可在-help的输出中将选项组织起来。该类的数据成员就是选项类别:

struct cat {
    OptionCategory &Category;
    cat(OptionCategory &c) : Category(c) {}
    template<class Opt> void apply(Opt &O) const { O.addCategory(Category); }
}

cl::sub属性类

该属性指定选项所属的子命令,有关子命令的介绍,看第一节:

struct sub {
    SubCommand &Sub;
    sub(SubCommand &S) : Sub(S) {}
    template <class Opt> void apply(Opt &O) const { O.addSubCommand(Sub); }
}

cl::cb属性类

该属性指定一个选项相关的回调函数,每当解析其遇到一个该属性时就会调用一次注册的回调。为了自动推导输入的回调函数类型,该属性的实现也是利用模板类+模板函数的机制:

  1. 实现一个保存回调函数的模板类:

    template <typename R, typename Ty> struct cb {
        std::function<R(Ty)> CB;
        cb(std::function<R(Ty)> CB) : CB(CB) {}
        template <typename Opt> void apply(Opt &O) const { O.setCallBack(CB); }
    }
    
  2. 实现模板函数做输入函数的类型推导,并生成对应的属性类:

    template <typename F>
    cb<typename detail::callback_traits<F>::result_type,
       typename detail::callback_traits<F>::arg_type> // 模板参数依赖的名字
    callback(F CB) {
           using result_type = typename detail::callback_traits<F>::result_type;
           using arg_type = typename detail::callback_traits<F>::arg_type;
           return cb<result_type, arg_type>(CB);
       }
    
  3. 上面用到的callback_traits实现如下:

    • 首先需要定义callback_traits的主模板:

      template <typename F>
      struct callback_traits : public callback_traits<decltype(&F::operator()) {}
      

      主模板是暴露给用户使用的,提供一个函数类型就能提取返回类型与参数类型。要获取具体类型,常见的做法是用偏特化来匹配。

    • 偏特化实现如下:

      template <typename R, typename C, typename... Args>
      struct callback_traits<R (C::*)(Args...) const> {
          using result_type = R;
          using arg_type = std::tuple_element_t<0, std::tuple<Args...>>;
      }
      

cl::values属性类

有一类特殊的选项,选项本身表示一个特定的枚举值。这类选项称为字面量选项(手册有时也叫作alternative)。如llvm opt工具中可以手动指定要运行的优化:

bash$ opt -mem2reg -dce -instcombine hello.ll

上面的mem2reg,dec,instcombine就是字面量选项。要支持这类选项,需要在声明选项时加入cl::values属性表明字面量的取值范围。其实现及使用如下:

  1. 需要一个类来实现属性类的apply接口并储存字面量值的信息,这个类由ValueClass实现:

    class ValuesClass {
        SmallVector<OptionEnumValue, 4> Values;
    public:
        ValuesClass(std::initializer_list<OptionEnumValue> Options)
            : Values(Options) {}
        template <class Opt> void apply(Opt &O) const { // apply 接口
            for (const auto &Value : Values)
                O.getParser().addLiteralOption(Value.Name, Value.Value, Value.Description); // 保存在Opt内部的parser中,这些OptionEnumValue会被用于解析。
        }
    }
       
    struct OptionEnumValue {
        StringRef Name;
        int Value;
        StringRef Description;
    }
    
  2. ValuesClass可接收任意数量的OptionEnumValue,但是需要用初始化列表构造,可以通过使用函数参数包来实现变长参数:

    template <typename... OptsTy> ValuesClass values(OptsTy... Options) {
        return ValueClass({Options...});
    }
    

选项修饰标记

选项修饰标记只是一个枚举值,对选项标记的解析需要不同的处理(选项属性有对应的类,并提供了apply接口进行处理)。为了让修饰标记与属性类用相同的接口处理,可以用模板特化来分别实现:

  1. 所有modifier都通过一个applicator::opt函数来解析。

  2. 选项属性类可以作为主模板来实现,调用属性类的apply接口完成选项解析:

    template <class Mod> struct applicator {
        template <class Opt> static opt(const Mod &M, Opt &O) { M.apply(O); }
    }
    
  3. 每个选项修饰标志都通过特化该主模板来分别实现:

    // 处理选项名modifier
    template <unsigned n> struct applicator<char[n]> {
        template <class Opt> static opt(StringRef Str, Opt &O) {
            O.setArgStr(Str);
        }
    };
    // 处理NumOccurrences标记
    template <> struct applicator<NumOccurrencesFlag> {
        template <class OPt> static opt(NumOccurencesFlag N, Opt &O) {
            O.setNumOccurrencesFlag(N);
        }
    };
    ...
    

现在,所有modifier都可以通过实例化applicator并调用其中的opt方法来解析。要解析cl::opt等类中的可变构造函数参数,可以使用函数参数包进行实现:

template <class Opt, class Mod, class... Mods>
void apply(Opt *O, const Mod &M, const Mods&... Ms) {
    applicator<Mod>::opt(M, O);
    apply(O, Ms...);
}

template <class Opt, class Mod> // 递归基础
void apply(Opt *O, const Mod &M) {
    applicator<MOd>::opt(M, O);
}

顶层类opt的实现

cl::opt的作用是根据modifier的描述与约束,提供本选项的选项值解析函数。可以依据cl::opt提供的功能将其实现进行分解:

  1. 首先需要对modifier进行解析记录,这一点前面已经提到。
  2. 其次需要将本选项进行注册,CommandLineParser在解析命令行参数时是按照单词流匹配选项的,所以需要在opt构造时就将其注册到选项库中供匹配。
  3. 接着opt需要提供一个选项值的解析接口,供CommandLineParser调用来处理选项值字符串。
  4. 最后需要提供一个存储变量来保存选项值的解析结果。

第一点和第二点在构造函数中进行了实现:

template <class... Mods>
explicit opt(const Mods &... Ms)
    : Option(llvm::cl::Optional, NotHidden), Parser(*this) {
  apply(this, Ms...); // 解析记录modifier
  done(); // 注册选项到OptionMap以及SubCommand中
}

第三点中解携的接口是handleOccurrence

bool handleOccurrence(unsigned pos, StringRef ArgName,
                        StringRef Arg) override {
  typename ParserClass::parser_data_type Val =
      typename ParserClass::parser_data_type();
  if (Parser.parse(*this, ArgName, Arg, Val))
    return true; // Parse error!
  this->setValue(Val);
  this->setPosition(pos);
  Callback(Val);
  return false;
}

第四点保存的值是由opt_storage基类提供的,下面会进行介绍。

基类opt_storage模板的介绍

OptionValue模板类的介绍

OptionValue是存储选项默认值的类,其继承关系如下图所示:

GenericOptionValue 抽象基类
OptionValueCopy类
OptionValueBase类
OptionValue类

基类Option的介绍

该类存储与选项相关的所有属性与修饰内容,提供选型属性与选项修饰的设置与访问函数:

private:
  uint16_t NumOccurrences; // The number of times specified
  // Occurrences, HiddenFlag, and Formatting are all enum types but to avoid
  // problems with signed enums in bitfields.
  uint16_t Occurrences : 3; // enum NumOccurrencesFlag
  // not using the enum type for 'Value' because zero is an implementation
  // detail representing the non-value
  uint16_t Value : 2;
  uint16_t HiddenFlag : 2; // enum OptionHidden
  uint16_t Formatting : 2; // enum FormattingFlags
  uint16_t Misc : 5;
  uint16_t FullyInitialized : 1; // Has addArgument been called?
  uint16_t Position;             // Position of last occurrence of the option
  uint16_t AdditionalVals;       // Greater than 0 for multi-valued option.

public:
  StringRef ArgStr;   // The argument string itself (ex: "help", "o")
  StringRef HelpStr;  // The descriptive text message for -help
  StringRef ValueStr; // String describing what the value of this option is
  SmallVector<OptionCategory *, 1>
      Categories;                    // The Categories this option belongs to
  SmallPtrSet<SubCommand *, 1> Subs; // The subcommands this option belongs to.

功能类OptionCategory

该类用于描述一个选项类别。一个选项类别的标识是一个名字可选的描述,另外由于-help能按照选项的OptionCategory来进行打印,因此解析器需要能遍历所有的OptionCategory,这就需要提供一个全局的数据结构供注册了。其核心成员如下:

class OptionCategory {
StringRef const Name; // 类别名字
StringRef const Description; // 可选的描述

void registerCategory(); // 注册函数
...
}

注册的数据结构为GlobalParser->RegisteredOptionCategories

功能类SubCommand

cl::SubCommand用于声明一个逻辑独立的子命令(比如git commit, git pull中commit和pull就是子命令)。如果命令行参数可以从逻辑上分为若干个子功能,那么就可以声明多个cl::SubCommand,并将选项通过cl::sub属性进行归类。归类后就可以利用cl::SubCommand提供的功能接口判断是否有该子命令的选项、有哪些选项等。如下是一个例子:

#include "llvm/Support/CommandLine.h"

using namespace llvm;

int main(int argc, char **argv) {
  cl::subcommand Add("add", "加法运算");
  cl::subcommand Sub("sub", "减法运算");

  cl::opt<int> Addend("addend", cl::desc("加数"), cl::sub(Add));
  cl::opt<int> Minuend("minuend", cl::desc("被减数"), cl::sub(Sub));
  cl::opt<int> Subtrahend("subtrahend", cl::desc("减数"), cl::sub(Sub));

  cl::ParseCommandLineOptions(argc, argv);

  if (Add) {
    // 执行加法运算
    int sum = Addend + cl::getSubcommand(Add)->getAdditionalArgsCount();
    // ...
  } else if (Sub) {
    // 执行减法运算
    int difference = Minuend - Subtrahend;
    // ...
  }

  return 0;
}

实现上,cl::SubCommand的成员可以划分为3类:

  1. 子命令的标识,主要有名字与可选的描述两个成员:

    StringRef Name;
    StringRef Decription;
    StringRef getName() const { return Name; }
    StringRef getDescription() const { return Description; }
    
  2. 注册相关成员函数。解析器应该能知道所有的cl::SubCommand,因而在构造时需要将其注册到GlobalParser中。

    void registerSubCommand();
    void unregisterSubCommand();
    
  3. 子命令包含的选项成员:

    SmallVector<Option *, 4> PositionalOpts;
    SmallVector<Option *, 4> SinkOpts;
    StringMap<Option *> OptionsMap;
    

class list的实现

cl::list的作用是收集解析的同类型选项值,因此需要做如下几件事:

  1. 设计数据结构保存解析结果。这是通过继承基类cl::list_storage来实现的。

    template <class DataType, class StorageClass = bool, class ParserClass = parser<DataType>>
    class list : public Option, public list_storage<DataType, StorageClass> { ... }
    
  2. 解析选项字符串。这可以在内部设计一个option parser来解析字符串。有关option parser的介绍,见下文

  3. 提供接口查询每个选项所在命令行参数的位置。因此需要一个数组保存每次解析字符串的位置。

故而其内部数据成员如下:

std::vector<unsigned> Positions; //记录每个解析字符串的位置
ParserClass Parser; //option parser

cl::list的功能接口主要分为两个:

  1. 首先,list应该能将自己注册到对应的SubCommand以及全局的OptionMap中。这样CommandLineParser在解析命令行参数时就能查询某个字符串是否是一个注册的选项。
  2. 提供选项值的解析功能。CommandLineParser在匹配到一个选项后需要调用cl::list提供的接口解析并保存选项值。

1中的注册通过构造函数实现是直接的:

template <class... Mods>
explicit list(const Mods &... Ms): Option(ZeroOrMore, NotHidden), Parser(*this) {
    apply(this, Ms...); // 将modifier应用到option上,比如`cl::sub`会记录该option所属的subcommands.
    done(); // 注册
}

void done() {
    addArgument(); // 1. 将该选项加入全局的OptionMap 2. 注册该选项到SubCommand中
    Parser.initialize(); // option parser初始化,不过目前的实现该函数为空
}

2中的解析功能是通过handleOccurence实现的。该解析函数接收选项名ArgName,选项值字符串Arg。将解析结果保存到内部存储中:

bool handleOccurrence(unsigned pos, StringRef ArgName,
                      StringRef Arg) override {
    typename ParserClass::parser_data_type Val =
        typename ParserClass::parser_data_type(); // 保存解析结果的临时变量
    if (list_storage<DataType, StorageClass>::isDefaultAssigned()) { // 有默认值
        clear(); // 清除掉默认值
        list_storage<DataType, StorageClass>::overwriteDefault(); // 清除默认标记
    }
    if (Parser.parse(*this, ArgName, Arg, Val)) // 解析!
        return true; // Parse Error!
    list_storage<DataType, StorageClass>::addValue(Val); // 将解析结果加入内部存储结构
    setPosition(pos);
    Positions.push_back(pos); // 记录选项位置
    Callback(Val); // 调用注册的回调
    return false;
}

基类list_storage的实现

list_storage是存储cl::list中同类型选项的容器。其目的与opt_storage一样保存选项值。且同样支持内部存储与外部存储的不同特化实现。

list_storage外部存储主模板

list_storage内部存储特化模板

class bits的实现

cl::bits的实现和cl::list别无二致,区别在于bits内部是用一个unsigned来作为bitmap存储选项值。实现的接口也主要是构造函数中的选项注册以及供CommandLineParser使用的解析函数handleOccurrence

option parser的实现

命令行解析器(CommandLineParser)会将命令行参数划分为空格分割的单词流(Tokens),然后将单词匹配注册的选项名,如果匹配成功,则将选项选项值字符串交给option parser解析。所以option parser的大致作用是解析选项值字符串,并将值保存到parse函数的输出参数中。

// O - 解析器所在的选项
// ArgName - 选项名字符串
// Arg - 选项值字符串
// Val - 解析结果输出参数
bool parse(Option &O, StringRef ArgName, StringRef Arg, DataType &Val);

option parser也是一个模板类,先介绍主模板的实现,然后介绍几个普通数据类型的特化实现。

Option Parser主模板

Option Parser特化模板

阅读疑问记录