本人水平有限,如有错误,欢迎指正与补充。
本文会先介绍 require 的基本使用,然后介绍动态链接库的基本知识。
最后深入 ruby 源码,查找对 dlopen, dlsym 的调用
ruby 里加载第三方 gem 的方式,通常是用 require 加载其 lib/ 文件夹下的某 .rb 文件。
但除了.rb 文件,gem 内也可以包含 c 语言写的源码,源码入口函数名往往是例如 Init_xxx
的格式。当我们用 gem install 安装 gem 的过程中,c 源码被编译成,例如名叫 xxx.so
的动态链接库。
最后 require 时,我们可以像加载 .rb 文件一样。用 require 'xxx.so'
加载动态链接库内的Init_xxx
函数。
对于不了解链接的同学,推荐 《深入理解计算机系统》 的第七章链接部分。或者 《程序员的自我修养》 也行。简单来说,链接就是把不同的 c 语言库和在一起,方便相互之间调用各自的函数与变量。
这个和的过程,可以是在编译时完成,称作静态链接。代价是最终的可执行文件较大。
这个和的过程,也可以推迟到程序启动时才完成,称作动态链接。优点是编译时,可执行文件只需记下自己依赖的库(linux 下可用 ldd 查看程序依赖哪些库),不用实际链接。等到启动时再实际链接,除了节省可执行文件的磁盘空间,也因为多个可执行文件共享同一个库,节省了内存。
这个和的过程,甚至可以在程序运行时进行。方法是调用 dlopen 函数加载动态链接库,再用 dlsym 获取动态链接库内的函数引用。
综上不难猜到,ruby require 动态链接库的过程,就是 调用 dlopen
加载动态链接库,再用 dlsym
获取 Init_xxx
函数指针并调用的过程。
require 对应的 c 函数是在 load.c 中的 rb_f_require , 精简省略后,主要的调用栈如下
/* load.c */
void
Init_load(void)
{
rb_define_global_function("require", rb_f_require, 1);
}
VALUE
rb_f_require(VALUE obj, VALUE fname)
{
/* 调用 rb_require_string */
return rb_require_string(fname);
}
VALUE
rb_require_string(VALUE fname)
{
/* 省略 */
/* 调用 require_internal */
int result = require_internal(ec, fname, 1);
}
static int
require_internal(rb_execution_context_t *ec, VALUE fname, int exception)
{
/* 省略 */
/* 调用 rb_vm_call_cfuncs, 其中 load_ext 参数是回调函数,也是实际被调用的函数。path 是动态链接库的文件地址,其他的参数可以忽略 */
handle = (long)rb_vm_call_cfunc(rb_vm_top_self(), load_ext,
path, VM_BLOCK_HANDLER_NONE, path);
}
static VALUE
load_ext(VALUE path)
{
/* 调用 dln_load, RSTRING_PTR(path)参数是把path转成字符指针类型 */
return (VALUE)dln_load(RSTRING_PTR(path));
}
最终实际调用dlopen
的函数在 dln.c 的 dln_load 中,其中有针对不同操作系统的宏判断,这里只精简显示 linux 相关的 dlopen
/* dln.c */
void*
dln_load(const char *file)
{
/* 省略 */
/* buf存储动态链接库的入口函数名,即例子中的 Init_xxx */
char *buf;
init_funcname(&buf, file);
/* 调用dlopen , 获取动态链接库对应的 handle指针 */
handle = (void*)dlopen(file, RTLD_LAZY|RTLD_GLOBAL))
/* 调用dlsym, 获取 buf即例子Init_xxx 的函数指针*/
init_fct = (void(*)())(VALUE)dlsym(handle, buf);
/* 用函数指针调用 例子Init_xxx 函数 */
(*init_fct)();
}