C-Ruby源码分析

from:http://hi.baidu.com/yjpro/blog/item/4fdbdee74fd07b29b9382024.html


最近在读 Ruby 的源码,我分析的是Ruby-lang上 的C-Ruby 1.8.7-p72的版本。


大致地浏览了Ruby的源码目录结构,用 cloc 统计了一下,算上扩展库里面的东东

,C-Ruby 1.8.7-p72 的代码量已经达到了15万行左右的规模,语言核心相关的

代码量大概也有5万行左右的规模。

工欲善其事,必先利其器,Ruby的源码量已经不算少了,想要有效地完成分析工

作,借助一些工具还是很有必要的。目前我准备了如下一些工具:

    i.  gdb  

    通过动态调试C-Ruby来分析其内部运行机理是非常直接的手段,其重要性自

不待言。

    ii.  ctags
         
    iii.  cscope

    除了动态调试分析C-Ruby以外,对源码进行静态分析也是必不可少的,大量的

内部变量,内部函数,如果纯靠手工查找的话,可不是件轻松的事情,通过ctags和

cscope来协助快速定位到变量,函数的定义,以及确立变量,函数的引用点,可以

大大提高源码分析的效率。

    iv.  grep
    
    在一些特别的场景下,使用ctags和cscope不能实现快速定位某个名字的任务,这

种场景下通过grep人肉查找还是很必要的。
     
    v.  Doxygen
    
    虽然C-Ruby的代码实现中并没有遵循Doxygen的文档注释规范,但是通过Doxygen

还是能够从C-Ruby的源码中提取出一些有助于代码分析的东西,比如C-Ruby中大量的自

定义数据结构。

    vi.  SourceNavigator
    
    简单来说,SourceNavigator就是Linux下一个类似于SourceInsight的工具。以前在

实验室接手虚拟BIOS的开发工具的时候,出于快速熟悉代码的需求,曾经使用过一段时间,

感觉还不错。SourceNavigator的开发曾经停滞过五年左右的时间,不过从今年的9月份

开始又开始有了更新。

    C-Ruby的语法解析部分是基于 Bison 实现的,词法分析模块则没有使用 Flex ,而是手工

编写的。值得一提的是,Ruby的词法分析模块中,出于效率的考虑,引入了完美哈希生成

gperf 实现对Ruby语言的内部保留字的快速识别。
    
    对于一个普通的Ruby脚本,C-Ruby的执行流程大致如下:

     1) 。 parser模块与词法模块协同工作,读入Ruby源文件中的内容,边parse源文件内容

边生成相对应的语法节点,最终在parse模块结束的时候会生成一个语法树结构

     2) 。 基于步骤1)中生成的语法树,对每个树结点进行解释执行。

为了获得对C-Ruby基本执行流程的更直观理解,我编写了一个简单的Ruby脚本 test.rb ,里

面只有  print "hello\n" 这一行内容。

   调用C-Ruby对 test.rb 进行解析执行,基本的执行流程如下:

main()  // 主程序入口点
        ruby_init()  // Ruby解释器运行 相关的初始化
        ruby_options()  // 处理Ruby解释器的命令行选项,命令行指定执行的源文件也会被视
                                    //为一个
令行选项,在ruby_options()中
                                  //调用相应函数完成解析处理,生成语法树结构

          ruby_process_options()
             proc_options()
                load_file()
                   rb_compile_file()
                      yycompile()
                          ruby_yyparse()        // 此函数即为Bison生成的Parser入口函数,此函数会针对
                                                          //Ruby源文件,生成相应的语法树结构

        ruby_run()     // 对ruby_yyparse()生成的语法树进行解释执行
          ruby_exec()   
             Init_stack()    // 垃圾回收相关的初始化
             ruby_exec_internal()    // 从语法树的根结点开始解释执行语法树
                eval_node()    // 解释执行语法树结点
                   rb_eval()
                       rb_call()     // test.rb中只包括一行语句 print "hello\n",在ruby_yyparse()中会针对                                       //print生成一个类型为                  
                                     //NODE_FCALL的结点,在解释执行过程中,对NODE_FCALL的语法节点,
                                     //会调用rb_call()执行相应动作

                         rb_get_method_body()    // 获得print对应的动作函数实体

                            rb_call0()   // 执行 print对应的实际动作,向标准输出打印hello字符串



最近结合语法树的生成及解释执行的过程分析了一下C-Ruby中RNode结构体的设计。

RNode是用于记录C-Ruby解析源文件过程中所生成的语法树结点的重要数据结构。

在C-Ruby解析源文件的过程中,会在读入源文件内容的同时建立相应的语法结点,

即RNode变量,并以链表的形式建立起一个语法树,维护语法结点的依赖关系。

在C-Ruby要对源文件进行解释执行的时候,就会从这个语法树链表的首结点开始,逐个

遍历各个语法结点并解释执行。

RNode 的数据结构的定义如下:    // 位于 node.h

typedef struct RNode {
    unsigned long flags;    //标志位,用于标识结点类型,具体结点类型也定义
                            //在node.h中,从C-Ruby的源码中来看,语法结点所对应
                            //的源码行号也是存放在的flags中的

    char *nd_file; // 结点所对应的Ruby源文件名
    // 在RNode结构体中,定义了三个Union成员,比较有趣。
    // 之所以使用Union是希望减少语法树所占用的内存开销 
    //因为C-Ruby是一个解释器,其解释的对象就是RNode构成的
    //语法树,为了降低语法树的内存开销,松本在设计RNode的时候采用
    //了三个Union变量。针对不同类型的语法结点,u1,u2,u3会使用不同
    //的Union成员,而且,即便使用了同一个成员,也会有不同的含义,
    //在node.h中专门定义了一组宏来简化对RNode的操作:
    // 如nd_head, nd_next, nd_cond,还有我们下面会提到的
    //nd_mid, nd_args 等等
    // 在u1, u2, u3的定义中,VALUE成员提供了对u1, u2, u3执行写操作的
    //统一接口,而其他的Union成员定义则提供了可能被不同语法结点
    //使用的多种读接口,
    //比如函数调用结点会使用u2的id 成员,
    //而对于另外一种结点,则可能会使用u2的argc成员。
    //但是向u2中写入内容,则都是通过VALUE成员写入的.
    //我理解这样设计的目的是既希望通过Union来节约内存,还希望能够简化
    //语法结点的创建工作。

    union {
        struct RNode *node;
        ID id; 
        VALUE value; 
        VALUE (*cfunc)(ANYARGS);
        ID *tbl;
    } u1;
    union {
        struct RNode *node;
        ID id;
        long argc;
        VALUE value;
    } u2;
    union {
        struct RNode *node;
        ID id;
        long state;
        struct global_entry *entry;
        long cnt;
        VALUE value;
    } u3;
} NODE;


对RNode的基本结构有了一定了解之后,不妨以一个简单的例子来分析RNode,我采用的

例子文件test.rb仅包含一行内容:

    print "hello";

对于test.rb,C-Ruby会先建立起一个由RNode结点构成的语法树,然后再对其进行解释执

行。

语法树的生成流程大致可以描述如下:
    
   词法模块对源码流输入print "hello";进行分解,返回语法模块可以识别的终结符,
   
语法模块则根据parse.y中编写的语法规则执行相应的移入,归约动作,在执行归约动

作的时候调用相应语法规则的语义动作,从而完成语法树生成的工作。

从源码输入规约到顶层语法描述的完整过程序列有些繁杂,中间夹杂一些中转规则

和状态,为了简化描述,我对整个语法归约过程进行了一定的简化:

源码输入:   print "hello";
==>   operation "hello"; ( operation: print )
注:括号里表示为了得到本条语法序列,执行语法归约动作所使用的相应语法规则)
==>    operation strings; (strings: "hello" )
==>     operation primary;
 (primary: strings)
==>      operation arg; 
(arg: primary)
==>       operation arg_value;
( arg_value: arg)
==>        operation args;
 ( args: arg_value )
==>         operation call_args;
 ( call_args: args )
==>          operation open_args; 
( open_args: call_args )
==>           operation command_args;
 ( command_args: open_args )
==>            command;
 (command: operation command_args )
==>             command_call;
 ( command_call: command )
==>              expr; 
( expr : command_call )
==>               stmt; 
( stmt : expr )
==>                stmts;
 ( stmts: stmt )
==>                 stmts opt_terms
 ( opt_terms: ';' )
==>                  compstmt 
( compstmt: stmts opt_terms )
==>                   program
 (program: compstmt)

从源码print "hello"; 开始逐步归约,每执行一个相应动作,就会创建一个相应

的RNode结点(也有一些归约动作并不会创建RNode结点,而只是传递已经创

建好的RNode结点指针
),及至归约到program语法符号之时,整个语法树就已经建

立起来了。语法树的根结点存放在一个全局的RNode指针变量, ruby_eval_tree

定义在parse.y中。

test.rb中仅包含一个函数调用,所以在语法树生成过程中会调用new_fcall()(定义于

parse.y中)创建一个NODE_FCALL类型的结点。

static NODE*
new_fcall(m,a)
    ID m;       // 函数ID,具体到test.rb即是print函数的相应ID
    NODE *a;    // 函数参数列表对应的语法结点,具体到test.rb,对应于
                // "hello"
{
    return NEW_FCALL(m,a);
}

可以看到new_fcall()又调用了NEW_FCALL来完成实际的生成函数调用结点的动作,

NEW_FCALL则是一个定义在node.h的宏。

#define NEW_NODE(t,a0,a1,a2) rb_node_newnode((t),(VALUE)(a0),(VALUE)(a1),(VALUE)(a2))
#define NEW_FCALL(m,a) NEW_NODE(NODE_FCALL,0,m,a)
NODE*

rb_node_newnode(type, a0, a1, a2)   // 定义在parse.y
    enum node_type type;
    VALUE a0, a1, a2;
{
    NODE *n = (NODE*)rb_newobj();

    n->flags |= T_NODE;
    nd_set_type(n, type);
    nd_set_line(n, ruby_sourceline);
    n->nd_file = ruby_sourcefile;

    n->u1.value = a0;
    n->u2.value = a1;
    n->u3.value = a2;

    return n;
}


从NEW_FCALL中可以看出,对于函数调用结点,RNodeu1成员是不会使用的,u2成员则

会用来存放标识函数的ID,u3则用来存放函数调用的相关参数列表的信息。在解释

执行的过程中,对于NODE_FCALL类型的结点也会通过访问u2u3中相应的Union成员来获得

函数调用的相关信息。

最后再来看一下语法树的解释执行过程(后面简称为eval过程):

eval过程始于ruby_run()函数(定义于eval.c),基本的调用图如下所示:

    


eval_node() 方法以 ruby_eval_tree 作为起始结点调用 rb_eval() 函数, rb_eval() 内部

又会根据结点类型去执行相应的解释动作,并且通过RNode中定义的指针成员获得语法树

中后续需要处理的RNode对象的指针,如此往复,直至完成对整个语法树的解释执行动作。

具体到 test.rb 这个例子,对于 print "hello" 所生成的 NODE_FCALL 的语法结点,eval过程

会执行如下的动作:

     case NODE_FCALL:    // 定义于eval.c中的rb_eval()函数
    {
        // node 是一个定义在rb_eval()局部的RNode *
        int argc; VALUE *argv; /* used in SETUP_ARGS */
        TMP_PROTECT;

        BEGIN_CALLARGS;
        // nd_args是一个定义在node.h中的宏
        //#define nd_args u3.node
        SETUP_ARGS(node->nd_args);      // 配置参数, 会向argc, argv
                                        //中写入数据
        END_CALLARGS;
        
        ruby_current_node = node;
        SET_CURRENT_SOURCE();
        // nd_mid是一个定义在node.h中的宏
        //#define nd_mid   u2.id

        // 通过nd_mid获得语法结点中存放的函数ID,
        // 通过nd_args获得语法结点中存放的参数信息
        // 如此一来,就具备解释执行NODE_FCALL结点所需的足够信息了
        result = rb_call(CLASS_OF(self),self,node->nd_mid,argc,argv,1,self);
    }

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值