Ruby Hack Challenge[0x00]: Ruby 的开发文化
本文介绍了 MRI 的源码结构,同时介绍了 hack MRI 的必要知识。
包括了以下几个话题:
要执行以下的命令,我们默认你使用的是 Unix-like 的环境,例如 Linux 和 macOS 等。如果你使用的是 Windows,你需要参考其他的材料。
备注:我们也提供了一个实验性的 docker 镜像:docker pull koichisasada/rhc
。通过 su rubydev
命令来切换到 rubydev
账号开始 hack。
我们默认使用以下的目录结构:
workdir/
ruby/
<- git clone 下来的 ruby 源码目录build/
<- 构建目录(*.o
文件和其他编译过程产生的文件存储在这里)install/
<- 安装目录(workdir/install/bin/ruby
是安装好的 Ruby 二进制)我们需要 git
、ruby
、autoconf
、bison
、gcc
(或 clang
等)和 make
命令。
如果依赖的库存在,Ruby 标准扩展(例如 zlib、openssl 等)也会被构建。
如果你用 apt-get
(或 apt
)来做包管理,可以使用下面的命令来安装所有依赖:
$ sudo apt-get install git ruby autoconf bison gcc make zlib1g-dev libffi-dev libreadline-dev libgdbm-dev libssl-dev
使用以下命令:
$ mkdir workdir
$ cd workdir
$ git clone https://github.com/ruby/ruby.git
# clone 下来的源码将会在 workdir/ruby
$ cd workdir/
# 进入 workdir
$ cd ruby
# 进入 workdir/ruby
$ autoconf
$ cd ..
$ mkdir build
$ cd build
$ ../ruby/configure --prefix=$PWD/../install --enable-shared
prefix
选项用来指定安装目录,你需要传入一个完整的绝对路径(这里是 workdir/install
)。Homebrew
用户需要添加以下选项 --with-openssl-dir="$(brew --prefix openssl)" --with-readline-dir="$(brew --prefix readline)" --disable-libedit
$ make -j
# 执行构建。 -j
指定了并行构建.$ make install
# 建议:为了加快安装,使用 make install-nodoc
安装不带 rdoc 的 ruby.$ ../install/bin/ruby -v
会显示你安装的 ruby 命令的版本信息备注:在执行 make
时添加 V=1
选项(例如,make V=1 -j
等)会输出构建过程中执行的全部命令。默认使用的是 V=0
即不输出详情。
你可以通过很多方式,使用构建好的 Ruby 来运行脚本。
最简单的是直接启动安装好的 Ruby,例如调用 workdir/install/bin/ruby
。这就和调用一个预先构建的 Ruby 二进制一样。但是这意味着每当你修改了 Ruby 的源码,都需要执行一次 make install
,非常费时。
这里介绍一些便捷的方法,在不安装的前提下启动我们修改过的 Ruby。
在构建 Ruby 过后,workdir/build
下会有一个可用的 miniruby
命令。miniruby
是一个精简版的 Ruby,用于构建 Ruby 本身。然而 miniruby
被精简的部分其实很少:它不能加载扩展库以及缺少完整的字符编码支持。你可以在 miniruby
中使用大部分的 Ruby 语法。
miniruby
是在 Ruby 构建过程中的第一阶段被构建的,因此使用 miniruby
对 MRI 的改动进行早期验证很有帮助。
下述开发流程是十分高效的:
make miniruby
来构建 miniruby
(这比 make
或 make all
快很多)。miniruby
执行一段 Ruby 脚本,测试你改动的正确性。为了支持这样的开发流程,我们在 Makefile 中提供了一个 make run
规则,用来:
miniruby
miniruby
运行 workdir/ruby/test.rb
(test.rb
是在 Ruby 源码目录里的)。借助 make run
你可以通过以下几步来测试你的改动:
ruby/test.rb
为你的改动编写测试。记住在 test.rb
里你不能 require gems 或者扩展库。$ make run
如果你想运行“正常的”Ruby,即可以加载扩展库的版本,你可以运行 make runruby
。这样不用执行 make install
就能运行 Ruby,节省时间。
ruby/test.rb
编写你的测试代码。$ make runruby
。备注:在 macOS 上运行 gdb
非常麻烦。我们默认你在 Linux 环境执行下文的指令。
当你修改 MRI 源码,很容易会引入一些严重问题,导致产生 SEGV。为了调试这种问题,我们提供了一条 Makefile 规则来支持使用 gdb 进行调试,当然你也可以使用断点进行调试。
在 ruby/test.rb
编写测试。记住在 test.rb
里你不能 require gems 或者扩展库。
调用 $ make gdb
,通过 gdb 启动 miniruby。如果没有问题,gdb 会结束执行而不报错。
make gdb
用的是 ./miniruby
。如果你想调试 ./ruby
, 使用 make gdb-ruby
。
如果你想使用断点,修改 make gdb
命令产生的 run.gdb
文件。
例如 b func_name
这条 gdb 命令会在 func_name
函数开始的地方插入一个断点。
$ make lldb
是为 lldb 准备的类似规则,你可以用 lldb 来替代 gdb(但是 Koichi 不清楚细节,因为他并不用 lldb)。如果你用 macOS 这也许有用。
$ make btest
# 运行 ruby/bootstraptest/
中的 bootstrap 测试$ make test-all
# 运行 ruby/test/
中的 test-unit 测试$ make test-spec
# 运行 ruby/spec
中的测试这三种测试有不同的目的和特点。
你大致可以看到下述的目录结构:
ruby/*.c
MRI 核心文件
vm*.[ch]
: Ruby 虚拟机实现vm_core.h
: Ruby 虚拟机数据结构定义insns.def
: Ruby 虚拟机指令定义compile.c, iseq.[ch]
: 指令序列 (字节码)gc.c
: GC 和内存管理thread*.[ch]
: 线程管理variable.c
: 变量管理dln*.c
: 扩展库使用的 dll 管理main.c
, ruby.c
: MRI 入口st.c
: Hash 算法实现 (参看 https://blog.heroku.com/ruby-2-4-features-hashes-integers-rounding)string.c
: String 类array.c
: Array 类ruby/*.h
: 内部定义,C 扩展库不能使用它们。ruby/include/ruby/*
: 外部定义,C 扩展库可以使用他们。ruby/enc/
: 字符编码信息。ruby/defs/
: 各种定义。ruby/tool/
: 构建 MRI 使用的工具。ruby/missing/
: 实现了在某些操作系统上缺失的特性。ruby/cygwin/
, ruby/nacl/
, ruby/win32
, ...: 特定操作系统和运行环境相关的代码。有两种库:
ruby/lib/
: Ruby 编写的标准库。ruby/ext/
: C 编写的扩展库,也会被一起打包。ruby/basictest/
: 旧的测试ruby/bootstraptest/
: bootstrap 测试ruby/test/
: 用 test-unit 表示的测试ruby/spec/
: 用 RSpec 表示的测试ruby/doc/
, ruby/man/
: 文档Ruby 的构建流程分为几个阶段,包括了代码生成等。其中有几个工具是用 Ruby 编写的,因此 Ruby 的构建需要依赖 Ruby 解释器。发布的 tarball 包括了生成好的代码,因此用发布的 tarball 安装 Ruby 不需要 Ruby 解释器(以及其他开发工具,比如 bison)。
如果你想用通过 Subversion 或 Git 仓库获取的源码来构建 MRI,你需要有一个 Ruby 解释器。
下面描述了构建和安装的流程:
BASERUBY
)将 Ruby 虚拟机指令编译成 C 代码。*.c
-> *.o
(在 Windows 上是 *.obj
): 将 C 代码编译成 object 文件。miniruby
将 enc/... 翻译成恰当的 C 代码。miniruby
,mkmf.rb
和 extconf.rb
来生成 Makefile
Makefile
来执行 make
ruby
命令rdoc
, ri
)configure --prefix
选项指定的路径)实际的构建过程中步骤会更多,然而要完整地列出所有步骤是非常难的(甚至我也没有记住全部),因此以上的只是精简过的流程。如果你好奇,可以查看包含了所有的规则的 common.mk
和相关文件。
让我们开始修改 MRI, 默认我们将所有源码放在 workdir/ruby
。
我们要修改版本描述信息,即 ruby -v
(或 ./miniruby -v
)会显示的内容,让它显示你定义的 Ruby 版本信息(例如包含了你名字的版本)。
version.c
。version.c
。ruby_show_version()
函数fflush()
用来输出缓冲区内容,因此我们推测在 fflush()
前添加用于打印的代码应该有用。printf("...\n");
(将 ...
替换成你想要的字符串)$ make miniruby
开始构建(别忘了要先切换到构建目录)$ ./miniruby -v
并检查结果$ make install
来安装 Ruby$ ../install/bin/ruby -v
,检查对于安装好的 Ruby 是否生效最后,除了插入 printf(...)
语句,可以试试替换掉整个 ruby...
描述(比如换成 perl...
之类的),这会很有趣 ;p