C-Ruby源码分析

来源:互联网 发布:网络问政 编辑:程序博客网 时间:2024/05/17 05:15

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);
    }

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 孕期吐的胃疼怎么办 买的巧克力化了怎么办 跑步后脸上出盐怎么办 头发被剪的很短怎么办 孩子做事情拖拉不专注怎么办 新热水壶有味道怎么办 新买电热壶有味怎么办 两个月狗耳朵臭怎么办 狗狗牙齿变黄怎么办 人用了狗沐浴露怎么办 狗狗吞食牙膏吐怎么办? 狗狗吞食了牙膏怎么办 大猪拉稀不吃食怎么办 猪不发烧不吃食怎么办 天天吃自热米饭怎么办 喝了加热包水怎么办啊 蛋挞没有盒子装怎么办 塑料饭盒盖子被吸住了怎么办 火腿淹的有臭味怎么办 微波炉热饭盖子打不开怎么办 夏天带饭容易馊怎么办? 保温饭盒里有气打不开怎么办 保温饭盒摔了一下打不开怎么办 饭盒跟盖子盖一起打不开怎么办 玻璃杯子盖被水吸住打不开怎么办 电饭煲热剩饭没加水怎么办 微波炉碗盖子吸住了怎么办 微波炉转饭盖子吸住了怎么办 玻璃碗放进微波炉打不开怎么办 乐扣微波炉加热后打不开怎么办 美的微波炉盖子打不开怎么办 美的微波炉门都打不开了怎么办 饭煮好了有异味怎么办 一正常吃饭就胖怎么办 高铁盒饭没15的怎么办 上火车前票丢了怎么办 减肥期吃了汉堡怎么办 寿司店鳗鱼有刺怎么办 吃泡面胃难受该怎么办 吃上火的东西脸上长痘痘怎么办 减肥期间吃撑了怎么办