cppRange 多维范围计算库

范围计算是电路综合中的一个常见问题。在计算一个多比特信号是否被完全赋值,或者是否存在多驱动的时候,都需要计算该信号的范围。比如说一个信号(sig)有8比特(Verilog HDL):

wire [7:0] sig;
assign sig[3:0] = 4'd5;
assign sig[6:3] = 4'd3;

对于sig的两次赋值就造成了对sig[3]的多驱(两个assign同时对其赋值)。同时,sig[7]没有驱动。

为了检查没有驱动的情况,可以判断[7:0] 是否等于 [3:0] 和 [6:3]的并集([3:0] | [6:3])。检查多驱,则可查看[3:0]和[6:3]的交集是否为空([3:0] & [6:3])。这似乎很简单。但是一旦信号的范围支持多维,就复杂很多。比如下面的例子:

wire [7:0][3:0] multi_sig;
assign multi_sig[0] = 4'd9;
assign multi_sig[7:2] = 24'd96;
assign multi_sig[1][2:0] = 3'd7;
assign multi_sig[4][0]=1'b1;

multi_sig[4][0]被驱动了两次,而multi_sig[1][3]没有驱动。为了判断驱动,需要计算 [0] | [7:2] | [1][2:0] | [4][0]。这种一维和两维的混合是非常难计算的。将信号的声明考虑在内,则可以规范化范围计算,从而计算 [0][3:0] | [7:2][3:0] | [1:1][2:0] | [4:4][0:0]。可是,这仍然是一个难题,比如说如何表示 [1:1][2:0] | [4:4][0:0]?

这种问题在硬件综合之外的其他环境中也可能出现,但是到现在好像还没有一个很好的解决方按,更别说开源库。

所以我自己写了一个简单的多维范围计算库(C++)[https://github.com/wsong83/cppRange]

这个库可以用来计算一维或多维范围表达式。现在支持功能如下:

========
一个范围可以表示为:

1比特:            [3:3] or [3]
一维:             [3:0]
多维:             [5:0][2:-1]

* 范围默认为[上边界:下边界]。如果上边界<下边界,该范围非法。
* 该库使用STL支持任何可以比较和加减的数据类型。支持负数范围。

========
支持的计算:

包含 (>)
    [7:0] > [3:2]                               真
    [7:0][4:0][3:-2] > [1:0][3][1:0]            真
    [5:3][2:0] > [5:1][2:0]                     假
    [5:3][2:0] > [5:1]                          假 (非法计算)
* 相判断的两个多维范围必须有相同的维数,否则非法。

其他支持的比较 >=, <, <=, ==, !=

并集运算 (|)
    [7:3] | [4:0]                            -> [7:0]
    [5:4] | [1:0]                            -> []              // 空 (无法求值)
    [4:0][3:1] | [4:0][5:2]                  -> [4:0][5:1]
    [1:0][5:1] | [4:2][5:1]                  -> [4:0][5:1]
    [1:0][3:1] | [4:0][5:2]                  -> []              // 空 (非法运算)
    [1:0][3:1] | [4:0]                       -> []              // 空 (非法运算)
* 合并两个多维范围时,只能有一个纬度上的范围不等,同时该不等范围相邻才可计算。
交集运算 (&)
    [7:3] & [4:0]                            -> [4:3]
    [5:4] & [1:0]                            -> []
    [4:0][3:1] & [4:0][5:2]                  -> [4:0][3:2]
    [2:0][5:1] & [4:2][5:1]                  -> [2:2][5:1]
    [1:0][3:1] & [4:0][5:2]                  -> [1:0][3:2]
    [1:0][3:1] & [4:0]                       -> []
* 求取两个多维范围的交集时,两个范围必须有相同的维度。

去除 (-)
    [7:3] - [4:0]                            -> [7:5]
    [5:4] - [1:0]                            -> [5:4]
    [4:0][3:1] - [4:0][5:2]                  -> [4:0][1]
    [2:0][5:1] - [4:2][5:1]                  -> [1:0][5:1]
    [1:0][3:1] - [4:0][5:2]                  -> []
    [1:0][3:1] - [4:0]                       -> []
* 多维范围运算时,只能有一个维度上的范围不等。 

标准分割 (^)
    [7:3] ^ [4:0]                            -> [7:5],[4:3],[2:0]
    [5:4] ^ [1:0]                            -> []
    [4:0][3:1] ^ [4:0][5:2]                  -> [4:0][5:4],[4:0][3:2],[4:0][1:1]
    [2:0][5:1] ^ [4:2][5:1]                  -> [4:2][5:1],[2:2][5:1],[1:0][5:1]
    [1:0][3:1] ^ [4:0][5:2]                  -> []
    [1:0][3:1] ^ [4:0]                       -> []
* A ^ B = {High, A&B, Low}
* 多维范围运算时,只能有一个维度上的范围不等。同时范围相邻才可计算。

详细的使用则只能查看具体的代码了。

我计划在不久的将来扩展该库的运算,让其支持多维运算时多个维度上的不等范围。不过该计算的计算复杂度和时间则会大幅增长。

Advertisements

A C++ SAIF parser

SAIF is short for “switching activity interchange format”, which is a universal file format to record the signal switching activities in VLSI circuits. Both Synopsys and Cadence tools use this file format to save the signal switching results from simulations.

Recently due to the need of my own project, I need to extract switching information from SAIF files generated by commercial simulation tools. Due to the lack of an opensourced parser, I have written my own. It seems to work.

The parser can be accessed from https://github.com/wsong83/cppSaif

It reads a SAIF file and store the switching information into a C++ class called SaifDB, which is a C++ representation of the abstract syntax tree.

For the details of this parser, see the saif_db.hpp file.

简化类方法的形参多态

最近在看《重构与模式》一书的修订版,想借用思路简化一下我手头的代码,不觉间发现了个简化类方法形参多态重载的方法,也许是书里已经有我还没看到,或也许还是个新的。

问题是这样的,我建立了一个有向图的数据结构,其中节点、弧和图的简化数据结构如下:

class Node {       // 节点
public:
  vertex_descriptor id;     // 节点全局唯一的id
  std::string       name;   // 节点名称
  boost::shared_ptr<Graph> pg;   // 图指针
};
class Edge {       // 弧
public: 
  edge_descriptor id;       // 弧全局唯一的id
  std::string     name;     // 弧的名字(不唯一)
  vertex_descriptor src;    // 起点节点id
  vertex_descriptor tar;    // 终点节点id
  boost::shared_ptr<Graph> pg;   // 图指针
};
class Graph {      // 有向图
public:
  std::map<vertex_descriptor, boost::shared_ptr<Node> > nodes;     // 节点表
  std::map<std::string,vertex_descriptor>               node_map;  // 节点名称和id转换表
  std::map<edge_descriptor, boost::shared_ptr<Edge> >   edges;     // 弧表
};

一个节点,可以由三种方式唯一的标定:id、节点名称和其节点指针。在Graph里面,有很多关于节点的操作,比如是否存在(exist)一个节点或者边,获得一个节点的所有入边、入节点、出边和出节点,等等。只是用id表示的方法定义如下:

class Graph{
.....
  bool exist(vertex_descriptor);  // 是否存在一个节点
  bool exist(vertex_descriptor, vertex_descriptor); // 是否存在从一个节点到另一个节点的弧
  std::list<boost::shared_ptr<Node> > get_in_nodes(vertex_descriptor); // 获得所有入节点
  std::list<boost::shared_ptr<Edge> > get_in_edges(vertex_descriptor); // 获得所有入边
  std::list<boost::shared_ptr<Node> > get_out_nodes(vertex_descriptor); // 获得所有入节点
  std::list<boost::shared_ptr<Edge> > get_out_edges(vertex_descriptor); // 获得所有入边
};

这些方法的每一vertex_descriptor类型的参数,都可以换为std::string或者boost::shared_ptr<Node>来唯一标示一个节点。如果要完成每一种参数类型的多态,一个形参的函数有三种实现,两个参数的函数则有9种,显然会出现大量重复的代码。

解决这个问题需要用到STL和宏。用STL定义一个函数的泛型,然后将所有的泛型都指向同一个特例(使用vertex_descriptor的方法实现)。利用宏减少代码量。先讨论STL的用法。比如说bool exist(vertex_descriptor);方法,我们可以定义:

template
bool exist(T d) {
  return exist(to_id(d));
}

这里的T即可是id, string和指针中的任何一种。无论是哪一种,可以通过to_id()方法将其换成id,然后调用使用vertex_descriptor的方法处理。关于exist并包含to_id方法的完整代码如下:

vertex_descriptor to_id(std::string name) const {
  if(node_map.count(name))
    return node_map.find(name)->second;
  else
    return NULL;   // vertex_descriptor的类型实为void*
}
vertex_descriptor to_id(boost::shared_ptr<Node> pnode) const {
  if(pnode)  return pnode->id;
  else       return NULL;
}

template<typename T>
bool exist(T d) {
  return exist(to_id(d));
}
bool exist(vertex_descriptor);

这看起来比写3遍相同的代码省不到哪里,但是to_id方法可以被所有其他的方法共用,STL的写法也不会因为形参数量变多而变的更复杂。

更进一步,我们可以定义以下的两个宏来简化STL的定义:

#define FUNC_HELPER1(rvalue, fname)  \
template<typename T>   \
rvalue fname(T d) {                  \
  return fname(to_id(d));            \
}

#define FUNC_HELPER2(rvalue, fname)   \
template<typename T1, typename T2>    \
rvalue fname(T1 d1, T2 d2) {          \
  return fname(to_id(d1), to_id(d2)); \
}

为一个参数和两个参数的方法定义了两个宏辅助定义。利用该辅助定义,能够实现任意形参类型多态的方法定义如下:

class Graph{
.....
  FUNC_HELPER1(bool, exist);
  bool exist(vertex_descriptor);  // 是否存在一个节点
  FUNC_HELPER2(bool, exist);
  bool exist(vertex_descriptor, vertex_descriptor); // 是否存在从一个节点到另一个节点的弧
  FUNC_HELPER1(std::list<boost::shared_ptr<Node> >, get_in_nodes);
  std::list<boost::shared_ptr<Node> > get_in_nodes(vertex_descriptor); // 获得所有入节点
  FUNC_HELPER1(std::list<boost::shared_ptr<Edge> >, get_in_edges);
  std::list<boost::shared_ptr<Edge> > get_in_edges(vertex_descriptor); // 获得所有入边
  FUNC_HELPER1(std::list<boost::shared_ptr<Node> >, get_out_nodes);
  std::list<boost::shared_ptr<Node> > get_out_nodes(vertex_descriptor); // 获得所有入节点
  FUNC_HELPER1(std::list<boost::shared_ptr<Edge> >, get_out_edges);
  std::list<boost::shared_ptr<Edge> > get_out_edges(vertex_descriptor); // 获得所有入边
};

所有的方法为了实现形参多态实际只需要在头文件中添加一行代码就完成了。(当然to_id和宏是需要添加的,但只需要写一次)。

关于语法分析器

最近的工作导致我学习了好些编译器相关的知识,也接触了些语法分析相关的编程工具。基本上相关的编程已告一段落,于是是时写点小结了(生活平淡无趣,也好久没写东西了,最近也许会再写点)。

编译器说白了,其实就是个计算机语言转换器:将用一种语言编写的代码变成另一种语言的代码。比如GCC,其实就是将C代码变成机器汇编代码。一个完整的编译器一定会包含几个基本模块:语言解释器(Parser)、优化器(Optimizer)、目标代码生成器(Object Code Generator/Mapper)。其中语言解释器负责读入并理解输入语言;优化器可能对代码所描述的行为针对目标语言做一些优化;最后目标代码生成器将优化后的行为用目标语言描述一遍并输出。

我的工作是读入硬件设计语言Verilog HDL,做一些优化然后重新输出Verilog HDL。所以主要的工作在于前面两项,解释器和优化。目标代码生成基本就是简单打印输出。

语言解释器本身也有内部结构:预编译(Preprocessor)、语法分析(Lexical Analyser)和语意分析(Syntactic Analyser)。以C语言为例,预编译会处理所有的宏语句,比如条件编译#ifdef,头文件#include和宏定义#define等等。预编译的输出基本就是不包含宏的C语言文件(但仍有一个宏会出现:#line,用于标注预编译后文件与源文件的行号对应,否则GCC的报错信息就没有错误位置了)。语法分析则会检查输入语言的语法是否正确:比如括号是否对应、标识符是否合法、操作符是否都有操作数、类型定义是否合法、标识符是否有类型定义等等。如果语法正确,语法分析还负责将输入语言划分为语义分析所能理解的单词(token)。语意分析是最复杂的部分了,它负责理解所有的单词,并将它们转换为抽象语法树(即将所有单词存放入一个由语义关系构造的树形结构)。比如,语意分析会检查一个类型是否没有定义,操作符和操作数的类型是否不一至,一个表达式的左值是否不可修改或者一个函数是否返回了错误类型。

总的来说,语言有着不同的分析难度。语言的语法和语义特性决定了它们相应语言解释器的复杂度。比如,我们可以将计算器的输入理解成一种语言,该语言就非常简单。它不需要预编译(没有宏),甚至不需要语意分析(只有数字和运算符两种类型)。一个语法分析即可得出答案。如果计算器支持表达式输入就会复杂一些,语意分析就需要了。比如2+3*5,语意分析需要知道*的优先级比+高才能得到结果。同时()的出现也导致复杂度的提升。再往上,某些计算器还支持变量、函数,这便引入了存储和变量,一个表达式的求值不再简单取决于表达式本身。编程语言就更复杂了,简单的Basic语言支持引入库,则可能需要预编译。同时数据出现了类型,比如数和字符,语意分析则需要判别每一个变量的类型是否正确。C语言就更复杂了,结构体的出现导致用户能够自定义类型、语意分析需要学习所有用户定义的类型才能工作。C++则又把问题上升一个级别:typedef的出现导致一个类型可能有多种不同名称;namespace导致类型出现了作用域;子父类继承关系导致一个目标类型可以使用所有的父类类型,出现了类型的一对多关系。我现在所能想到最复杂的语言系统可能要数Haskell为代表的函数式语言了。它们支持类型自动推导,即表达式本身不许要一一标注类型,类型可根据调用关系动态指定和推断。

说到这里,你应该意识到类型是一个多么重要的概念。讲编译器的书往往会花大量章节讲不同的语法,比如语法是否有递归,是否需要大于1的预读取(lookahead)。但一旦涉及到类型,语法的那些复杂度就一下子变得小儿科了,因为大多数递归型语法可换成非递归的,大多数预读取问题可用更多的语法解决。但用户可自定义的语法则代表语言解释器需要自学习!在这个方面,我现在还是相当无知。好在Verilog HDL并不存在用户自定义类型,我现在还不需要处理这块大石头,但很可能将来需要(很可能我将读入VHDL,VHDL支持自有类型定义!)。不过我要说,这一块的水很深,慎入!!:-)

在工具方面,我现在用Flex+Bison处理Verilog,Boost.Spirit处理命令行解释和pugixml处理用标记语言(marking language)存储的数据文件。

针对复杂语言,基本需要解释器自动生成工具,比如Bison和ANTLR。它们都需要用户用语法描述语言定义目标语言的语法和语义。工具则通过这些语法描述自动生成一个语言解释器。这样做的好处是将代码编写的问题转嫁给了工具,但单独编译导致了语言解释器本身非常静态。在这方面,Boost.Spirit就允许用户动态定义语法,因为它本省就是一个C++库。不过它的编译速度可不敢恭维(大量使用范型STL的结果)。

对于标记语言,现在的工具基本已经傻瓜化了。我用的pugixml就是一个轻量级的XML解释器,能自动生成语法分析树,完全不用动脑。

不知我这半桶水的理解是否正确,不过用近一年的时间就从没学过编译器到独自写了个Verilog语言解释器我还是挺满意的。

Enforce the destruction order using shared_ptr (C++)

In recent development of C++/Tcl library, a user reported a crash when using the Tcl interpreter as a global object. From the error report and the function calling stack I had no idea what was wrong until this user provided a clue that it may be caused by the wrong destruction order of global objects. Finally it is verified true. I have just fixed this problem using boost::shared_ptr< >. As the nature of this issue is so weird, it is recorded accordingly.

For the following code:

// code1.cpp
int a = 1;
int b = 2;

int main() {
  int c = a+b;
  return 0;
}

global variables a and b will be constructed in the order of how they are defined, and they will be destroyed in the reversed order (b then a) when the whole program is being terminating.

However, things will turn complicated when multiple global variables are defined in multiple objects. For example, we could have another global variable int d defined in "code2.cpp". Then suppose we need to link these two object files into one executive using

g++ code1.o code2.o -o test_run

In this case, the construction order of a, b and d will depends on the order by which the objects files are linked and the internal order defined by the compiler. While the destruction order is still the reserved order of how they are constructed.

This would cause no problem if all global variables are defined separately without any connections. However, errors will come out when they call each other in the definition. For the following codes, there must be a wrong value initialization for either b or d:

// code1.cpp ===================
int a = 1;
int b = d;

int main() {
  int c = a+b;
  return 0;
}

// code2.cpp ===================
int d = a;

The thing is: if code1.o is linked before code2.o, d is not defined in int b = d; else if code2.o is linked before code1.o, a is not defined when int d = a. You can see here, there is some limitations of using global variables.

Now is the time to introduce the problem I encountered in the C++/Tcl library.

This library provides an interpreter class to users who can add extra commands, variables even objects to an embedded tcl script interpreter. To make things easy and hide the complicated tcl C API interfaces, all extra objects added to tcl are recorded globally using a map (multiple interpreter can co-exist at the same time linked to the same or multiple embedded Tcl interpreter). However, this user (who reported the crash) declared interpreter as a global object, meaning both the interpreter and the global map (recording all extra tcl objects) are in the global namespace. Unfortunately, the compiler used by this user released the map before interpreters, which causeed crashes.

To fix this problem, I must enforce a destruction order, making sure maps are valid when interpreters are released (as they are, in the destruction method, trying to clean the tcl objects defined by them).

the original code could like these:

// cpptcl.h ===================
class interpreter {
public:
  // some methods to define extra tcl objects
  void def_fun(...);
  void def_obj(...);
  void def_var(...);
  void ~interpreter();
};

// cpptcl.cpp ===================
...

// the global map definition
class global_map_type {
......
}

global_map_type gmap;  // the global map

void interpreter::def_fun(...) {
  gmap.def_fun(this, ...);   // use the global gmap
}

void interpreter::def_obj(...) {
  gmap.def_obj(this, ...);   // use the global gmap
}

void interpreter::def_var(...) {
  gmap.def_var(this, ...);   // use the global gmap
}

// destructor
interpreter::~interpreter() {
  // undef all objects defined by this interpreter
  for(...) gmap.undef_fun(this, ...);
  for(...) gmap.undef_obj(this, ...);
  for(...) gmap.undef_var(this, ...);
}

// main.cpp ===================

interpreter i0;   // global interpreters
interpreter i1;   // global interpreters

main() {
 ....
}

The two interpreters i0 and i1 may call gmap in the destructor when gmap is already released.

Well, I try to fix this using boost::shared_ptr as smart pointer can increase the ref count of a pointer and release the object only when no one is referencing it. The new code will be (only cpptcl.h and cpptcl.cpp are changed):

// cpptcl.h ===================

// a forward declaration
class global_map_type;

class interpreter {
public:
  // some methods to define extra tcl objects
  void def_fun(...);
  void def_obj(...);
  void def_var(...);
  void ~interpreter();
private:
  boost::shared_ptr<global_map_type> map_;  // to increase the pointer reference
};

// cpptcl.cpp ===================
...

// the global map definition
class global_map_type {
......
}

// global_map_type gmap;  // the global map
// now define it as a pointer
boost::shared_ptr<global_map_type> gmap(new global_map_type());

void interpreter::def_fun(...) {
  // gmap.def_fun(this, ...);   // use the global gmap
  if(map_.use_count() == 0) map_ = gmap;  // do the ref count incr if not done yet
  gmap->def_fun(this, ...);   // use the global gmap
}

void interpreter::def_obj(...) {
  // gmap.def_obj(this, ...);   // use the global gmap
  if(map_.use_count() == 0) map_ = gmap;  // do the ref count incr if not done yet
  gmap->def_obj(this, ...);   // use the global gmap
}

void interpreter::def_var(...) {
  // gmap.def_var(this, ...);   // use the global gmap
  if(map_.use_count() == 0) map_ = gmap;  // do the ref count incr if not done yet
  gmap->def_var(this, ...);   // use the global gmap
}

// destructor
interpreter::~interpreter() {
  // undef all objects defined by this interpreter
  // for(...) gmap.undef_fun(this, ...);
  for(...) gmap->undef_fun(this, ...);
  // for(...) gmap.undef_obj(this, ...);
  for(...) gmap->undef_obj(this, ...);
  // for(...) gmap.undef_var(this, ...);
  for(...) gmap->undef_var(this, ...);
}

In this way, every time a new interpreter tries to define an extra object in tcl, it will increase the global map ref count. Before all interpreters being released, the ref count for the global map will be larger than one, prohibiting it being released. In other words, the destruction order of global objects is enforced. The only pain here is I cannot do the ref count incr (or copy the smart pointer) inside the construction method of interpreter, as at that time, it is uncertain whether the global map is constructed. If not, the copied smart pointer is an empty one which will not increase the ref count.

C++/Tcl :更容易地在C++中嵌入Tcl

才发现原来在C++程序中嵌入一个完整的script语言环境不是那么的困难。鉴于项目需要,我决定将Tcl语言解释器嵌到我的代码里,加上一个非常简单的输入流处理,就实现了一个动态Tcl命令行用户接口。

稍微说一下语言解释器。一般来说script语言,也就是脚本语言是解释执行的。但并不是所有的脚本语言都能用于嵌入。有些太大了不好嵌入,比如python,有些则基本不适合用于嵌入,比如Javascript。现在用的最多的一个是Tcl,另一个是较新的Lau。据说Lau在游戏里面用的很多,一般用作后台命令行解释。我自己受限于以往的类似软件多使用Tcl,因此还是继续使用Tcl的向后兼容新会好一些。

Tcl的C/C++接口使用C标准,提供了约50来个API函数。C/C++程序可以使用这些API运行Tcl解释器,任意扩展Tcl命令集,查看和修改Tcl变量,以及设定事件触发回调函数(这仅仅是我关心的功能,还有其他的一堆)。

但这些函数并不怎么好用,基本都是指针操作,而且文档看起来也不是那么好懂。一番搜索之后我发现了个好库,C++/Tcl,这个库将一部分的Tcl API用C++封装,并使用Tcl和Boost库实现了资源自动管理和释放,以及自动函数类型匹配,支持最多达9个参数的函数和对象扩展。

不过它还缺了两个我必须使用的功能,即在Tcl环境中修改我程序的环境变量和设定Tcl变量事件的回调。如果没有这些,Tcl就像是一个与世隔绝的封闭嵌入式解释器,没什么实际功能。

于是,我在Github上开了个新分支并在原有基础上增加了这两个功能。这是我第一次如此大规模的使用STL来构建自己的库,居然还动用了boost/preprocessor预处理来减少代码量。我已经更新了文档,并给出了最新功能的使用范例。具体链接如下:

文档:wsong83.github.com/cpptcl
代码:github.com/wsong83/cpptcl

如有bug或功能请求,欢迎发信,提供patch就更好了。

Preprocessor for Verilog HDL

现在做的博后需要设计一个异步电路综合软件,我选择Verilog作为设计输入,于是乎最近就生生扎入了编译器的无底深渊。

Verilog的Parser写的差不多了,大概需要的语法特性都能读入并转化为数据结构,但是宏处理和文件嵌入还没解决。传统上这是预处理的事,也就是将包含include的多文件合并成一个文件并将所有的宏都替换掉。

自己也能写,不过估计又得花很长时间,于是搜了搜。有人建议LLVM的CLang,不过看看之后还是觉得太大。后来找到了Verilog-Perl。这是Wilson Snyder用C++和Perl写的一个Perl软件包。其中包含一个支持Verilog和SystemVerilog的预处理,不过有一部分是Perl实现的。这位老哥写的程序还是很牛的,于是就准备用这个。

开始觉得很简单,可是看着看着就郁闷了。这位老哥成心不让人好过是咋地,程序里面类套类,用了无数C的遗留代码,更抓狂的是其中宏变量存储居然是用Perl实现的。就是说顶层是Perl,核心是C++和一部分C,但有调用了很多Perl。无奈的发现我需要把所有的Perl都替换掉。

我就不说整个过程是多么崩溃了,反正代码是彻底删改了两次。最后终于决定只做最小改动先把软件包用上再说(这位老哥应该是个实用主义者,代码没啥优雅的逻辑,完全是补丁满天飞,想看懂还真不太容易)。

改得差不多了。现在还有些小问题没解决,比如说他用的文件处理函数不能正确地读相对路径,不知道是我还没理解对还是本来就有问题。估计快搞定了。

代码放在https://github.com/wsong83/vpreproc了,有兴趣的可以瞧瞧。

我用了boost_filesystem和boost_program_options,所以编译需要boost library。