为 Ruby 创建扩展库

本文档说明如何为 Ruby 制作扩展库。

基础知识

在 C 中,变量有类型,而数据没有类型。相反,Ruby 变量没有静态类型,而数据本身有类型,因此需要在语言之间转换数据。

Ruby 中的对象由 C 类型“VALUE”表示。每个 VALUE 数据都有其数据类型。

要从 VALUE 检索 C 数据,您需要

  1. 识别 VALUE 的数据类型

  2. 将 VALUE 转换为 C 数据

转换为错误的数据类型可能会导致严重问题。

Ruby 数据类型

Ruby 解释器具有以下数据类型

T_NIL

nil

T_OBJECT

普通对象

T_CLASS

T_MODULE

模块

T_FLOAT

浮点数

T_STRING

字符串

T_REGEXP

正则表达式

T_ARRAY

数组

T_HASH

关联数组

T_STRUCT

(Ruby) 结构

T_BIGNUM

多精度整数

T_FIXNUM

Fixnum(31 位或 63 位整数)

T_COMPLEX

复数

T_RATIONAL

有理数

T_FILE

IO

T_TRUE

true

T_FALSE

false

T_DATA

数据

T_SYMBOL

符号

此外,还有几个其他类型在内部使用

T_ICLASS

包含的模块

T_MATCH

MatchData 对象

T_UNDEF

未定义

T_NODE

语法树节点

T_ZOMBIE

等待完成的对象

大多数类型由 C 结构表示。

检查 VALUE 数据的类型

ruby.h 中定义的宏 TYPE() 显示 VALUE 的数据类型。TYPE() 返回上面描述的常量数字 T_XXXX。要处理数据类型,您的代码将类似于以下内容

switch (TYPE(obj)) {
  case T_FIXNUM:
    /* process Fixnum */
    break;
  case T_STRING:
    /* process String */
    break;
  case T_ARRAY:
    /* process Array */
    break;
  default:
    /* raise exception */
    rb_raise(rb_eTypeError, "not valid value");
    break;
}

有数据类型检查函数

void Check_Type(VALUE value, int type)

如果 VALUE 没有指定类型,它会引发异常。

针对 fixnum 和 nil 还有更快的检查宏。

FIXNUM_P(obj)
NIL_P(obj)

将 VALUE 转换为 C 数据

类型 T_NIL、T_FALSE、T_TRUE 的数据分别为 nil、false、true。它们是数据类型的单例。等效的 C 常量为:Qnil、Qfalse、Qtrue。如果 VALUE 既不是 Qfalse 也不是 Qnil,RTEST() 将返回 true。如果您需要区分 Qfalse 和 Qnil,请专门针对 Qfalse 进行测试。

T_FIXNUM 数据是一个 31 位或 63 位长度的定长整数。此大小取决于 long 的大小:如果 long 为 32 位,则 T_FIXNUM 为 31 位;如果 long 为 64 位,则 T_FIXNUM 为 63 位。可以通过使用 FIX2INT() 宏或 FIX2LONG() 将 T_FIXNUM 转换为 C 整数。尽管在使用它们之前必须检查数据是否确实为 FIXNUM,但它们的速度更快。FIX2LONG() 绝不会引发异常,但如果结果大于或小于 int 的大小,则 FIX2INT() 会引发 RangeError。还有 NUM2INT() 和 NUM2LONG(),它们会将任何 Ruby 数字转换为 C 整数。这些宏包含类型检查,因此如果转换失败,将引发异常。NUM2DBL() 可用于以相同的方式检索双精度浮点值。

可以使用宏 StringValue() 和 StringValuePtr() 从 VALUE 获取一个 char*。StringValue(var) 会用 “var.to_str()” 的结果替换 var 的值。StringValuePtr(var) 会执行相同的替换并返回 var 的 char* 表示形式。如果 var 是一个 String,这些宏将跳过替换。请注意,这些宏只将其参数作为左值,以就地更改 var 的值。

还可以使用名为 StringValueCStr() 的宏。这与 StringValuePtr() 类似,但始终在结果的末尾添加一个空字符。如果结果包含一个空字符,此宏会引发 ArgumentError 异常。StringValuePtr() 不保证结果末尾存在空字符,并且结果可能包含空字符。

其他数据类型具有对应的 C 结构,例如 T_ARRAY 等的 struct RArray。可以将具有对应结构的类型的值强制转换为以检索指向该结构的指针。强制转换宏对于每种数据类型将采用 RXXXX 形式;例如,RARRAY(obj)。请参阅 “ruby.h”。但是,我们不建议直接访问 RXXXX 数据,因为这些数据结构很复杂。使用相应的 rb_xxx() 函数来访问内部结构。例如,要访问数组的一个条目,请使用 rb_ary_entry(ary, offset) 和 rb_ary_store(ary, offset, obj)。

有一些用于访问结构成员的宏,例如 ‘RSTRING_LEN(str)’,用于获取 Ruby String 对象的大小。可以通过 ‘RSTRING_PTR(str)’ 访问已分配的区域。

注意:不要直接更改结构的值,除非您对结果负责。这最终会导致出现有趣的错误。

将 C 数据转换为 VALUE

将 C 数据转换为 Ruby 值

FIXNUM

左移 1 位,并打开其最低有效位 (LSB)。

其他指针值

转换为 VALUE。

您可以通过检查其 LSB 来确定 VALUE 是否为指针。

注意:Ruby 不允许任意指针值成为 VALUE。它们应该是 Ruby 已知的结构的指针。已知结构在 <ruby.h> 中定义。

要将 C 数字转换为 Ruby 值,请使用以下宏

INT2FIX()

用于 31 位以内的整数。

INT2NUM()

用于任意大小的整数。

如果 INT2NUM() 将整数转换为 Bignum 超出 FIXNUM 范围,但速度稍慢。

操作 Ruby 对象

正如我之前提到的,不建议修改对象的内部结构。要操作对象,请使用 Ruby 解释器提供的函数。下面列出了一些(并非全部)有用的函数

String 函数

rb_str_new(const char *ptr, long len)

创建一个新的 Ruby 字符串。

rb_str_new2(const char *ptr)
rb_str_new_cstr(const char *ptr)

从 C 字符串创建一个新的 Ruby 字符串。这等效于 rb_str_new(ptr, strlen(ptr))。

rb_str_new_literal(const char *ptr)

从 C 字符串文字创建一个新的 Ruby 字符串。

rb_sprintf(const char *format, …)
rb_vsprintf(const char *format, va_list ap)

使用 printf(3) 格式创建一个新的 Ruby 字符串。

注意:在格式字符串中,“%”PRIsVALUE 可用于 Object#to_s(或如果设置了“+”标志,则为 Object#inspect)输出(并且相关参数必须为 VALUE)。由于它与“%i”冲突,因此对于格式字符串中的整数,请使用“%d”。

rb_str_append(VALUE str1, VALUE str2)

将 Ruby 字符串 str2 追加到 Ruby 字符串 str1。

rb_str_cat(VALUE str, const char *ptr, long len)

将 ptr 中的 len 字节数据追加到 Ruby 字符串。

rb_str_cat2(VALUE str, const char* ptr)
rb_str_cat_cstr(VALUE str, const char* ptr)

将 C 字符串 ptr 追加到 Ruby 字符串 str。此函数等效于 rb_str_cat(str, ptr, strlen(ptr))。

rb_str_catf(VALUE str, const char* format, …)
rb_str_vcatf(VALUE str, const char* format, va_list ap)

根据类似 printf 的格式将 C 字符串格式和连续参数追加到 Ruby 字符串 str。这些函数分别等效于 rb_str_append(str, rb_sprintf(format, …)) 和 rb_str_append(str, rb_vsprintf(format, ap))。

rb_enc_str_new(const char *ptr, long len, rb_encoding *enc)
rb_enc_str_new_cstr(const char *ptr, rb_encoding *enc)

使用指定的编码创建新的 Ruby 字符串。

rb_enc_str_new_literal(const char *ptr, rb_encoding *enc)

使用指定的编码从 C 字符串文字创建新的 Ruby 字符串。

rb_usascii_str_new(const char *ptr, long len)
rb_usascii_str_new_cstr(const char *ptr)

使用编码 US-ASCII 创建新的 Ruby 字符串。

rb_usascii_str_new_literal(const char *ptr)

使用编码 US-ASCII 从 C 字符串文字创建新的 Ruby 字符串。

rb_utf8_str_new(const char *ptr, long len)
rb_utf8_str_new_cstr(const char *ptr)

使用编码 UTF-8 创建新的 Ruby 字符串。

rb_utf8_str_new_literal(const char *ptr)

使用编码 UTF-8 从 C 字符串文字创建新的 Ruby 字符串。

rb_str_resize(VALUE str, long len)

将 Ruby 字符串调整为 len 字节。如果 str 不可修改,此函数将引发异常。str 的长度必须预先设置。如果 len 小于旧长度,则 len 字节之外的内容将被丢弃,否则如果 len 大于旧长度,则旧长度字节之外的内容将不会被保留,而是垃圾。请注意,调用此函数可能会更改 RSTRING_PTR(str)。

rb_str_set_len(VALUE str, long len)

设置 Ruby 字符串的长度。如果 str 不可修改,此函数将引发异常。此函数保留最多 len 字节的内容,无论 RSTRING_LEN(str) 如何。len 不能超过 str 的容量。

rb_str_modify(VALUE str)

准备修改 Ruby 字符串。如果 str 不可修改,此函数将引发异常,或者如果 str 的缓冲区是共享的,此函数将分配新的缓冲区以使其不共享。在使用 RSTRING_PTR 和/或 rb_str_set_len 修改内容之前,必须始终调用此函数。

Array 函数

rb_ary_new()

创建不包含任何元素的数组。

rb_ary_new2(long len)
rb_ary_new_capa(long len)

创建不包含任何元素的数组,为 len 个元素分配内部缓冲区。

rb_ary_new3(long n, …)
rb_ary_new_from_args(long n, …)

使用参数创建 n 个元素的数组。

rb_ary_new4(long n, VALUE *elts)
rb_ary_new_from_values(long n, VALUE *elts)

使用 C 数组创建 n 个元素的数组。

rb_ary_to_ary(VALUE obj)

将对象转换为数组。等效于 Object#to_ary。

有许多函数可用于操作数组。如果给出了其他类型,它们可能会转储核心。

rb_ary_aref(int argc, const VALUE *argv, VALUE ary)

等同于 Array#[]

rb_ary_entry(VALUE ary, long offset)

ary[offset]

rb_ary_store(VALUE ary, long offset, VALUE obj)

ary[offset] = obj

rb_ary_subseq(VALUE ary, long beg, long len)

ary[beg, len]

rb_ary_push(VALUE ary, VALUE val)
rb_ary_pop(VALUE ary)
rb_ary_shift(VALUE ary)
rb_ary_unshift(VALUE ary, VALUE val)

ary.push、ary.pop、ary.shift、ary.unshift

rb_ary_cat(VALUE ary, const VALUE *ptr, long len)

将 ptr 中 len 个对象的元素追加到数组中。

用 C 扩展 Ruby

向 Ruby 添加新功能

可以向 Ruby 解释器添加新功能(类、方法等)。Ruby 提供了用于定义以下内容的 API

ClassModule 定义

要定义类或模块,请使用以下函数

VALUE rb_define_class(const char *name, VALUE super)
VALUE rb_define_module(const char *name)

这些函数返回新创建的类或模块。你可能希望将此引用保存到变量中以供以后使用。

要定义嵌套类或模块,请使用以下函数

VALUE rb_define_class_under(VALUE outer, const char *name, VALUE super)
VALUE rb_define_module_under(VALUE outer, const char *name)

Method 和单例方法定义

要定义方法或单例方法,请使用这些函数

void rb_define_method(VALUE klass, const char *name,
                      VALUE (*func)(ANYARGS), int argc)

void rb_define_singleton_method(VALUE object, const char *name,
                                VALUE (*func)(ANYARGS), int argc)

‘argc’ 表示 C 函数的参数数量,必须小于 17。但我怀疑你不需要那么多。

如果 ‘argc’ 为负数,则它指定调用顺序,而不是参数数量。

如果 argc 为 -1,则函数将被调用为

VALUE func(int argc, VALUE *argv, VALUE obj)

其中 argc 是实际参数数量,argv 是参数的 C 数组,obj 是接收器。

如果 argc 为 -2,则参数将以 Ruby 数组的形式传递。函数将被调用为

VALUE func(VALUE obj, VALUE args)

其中 obj 是接收器,args 是包含实际参数的 Ruby 数组。

还有一些函数可以定义方法。一个函数将 ID 作为要定义的方法的名称。另请参阅下面的 ID 或 Symbol

void rb_define_method_id(VALUE klass, ID name,
                         VALUE (*func)(ANYARGS), int argc)

有两个函数用于定义私有/受保护的方法

void rb_define_private_method(VALUE klass, const char *name,
                              VALUE (*func)(ANYARGS), int argc)
void rb_define_protected_method(VALUE klass, const char *name,
                                VALUE (*func)(ANYARGS), int argc)

最后,rb_define_module_function 定义了一个模块函数,它是模块的私有且单例的方法。例如,sqrt 是在 Math 模块中定义的模块函数。可以通过以下方式调用它

Math.sqrt(4)

include Math
sqrt(4)

要定义模块函数,请使用

void rb_define_module_function(VALUE module, const char *name,
                               VALUE (*func)(ANYARGS), int argc)

此外,可以使用以下方法定义函数式方法,即在 Kernel 模块中定义的私有方法

void rb_define_global_function(const char *name, VALUE (*func)(ANYARGS), int argc)

要定义方法的别名,

void rb_define_alias(VALUE module, const char* new, const char* old);

要定义属性的读写器,

void rb_define_attr(VALUE klass, const char *name, int read, int write)

要定义和取消定义“分配”类方法,

void rb_define_alloc_func(VALUE klass, VALUE (*func)(VALUE klass));
void rb_undef_alloc_func(VALUE klass);

func 必须将 klass 作为参数,并返回一个新分配的实例。此实例应尽可能为空,不包含任何昂贵的(包括外部)资源。

如果您要覆盖类的任何祖先的现有方法,则可以依赖

VALUE rb_call_super(int argc, const VALUE *argv)

要指定在调用 super 时是否传递关键字参数

VALUE rb_call_super_kw(int argc, const VALUE *argv, int kw_splat)

kw_splat 可以具有以下可能值(由所有接受 kw_splat 参数的方法使用)

RB_NO_KEYWORDS

不传递关键字

RB_PASS_KEYWORDS

传递关键字,最后一个参数应为关键字哈希

RB_PASS_CALLED_KEYWORDS

如果当前方法是用关键字调用的,则传递关键字,这对于参数委派很有用

要获取当前范围的接收器(如果没有其他可用方式),可以使用

VALUE rb_current_receiver(void)

常量定义

我们有 2 个函数来定义常量

void rb_define_const(VALUE klass, const char *name, VALUE val)
void rb_define_global_const(const char *name, VALUE val)

前者是在指定的类/模块下定义常量。后者是定义全局常量。

从 C 中使用 Ruby 功能

有几种方法可以从 C 代码调用 Ruby 的功能。

在字符串中计算 Ruby 程序

从 C 程序使用 Ruby 功能的最简单方法是将字符串计算为 Ruby 程序。此函数将完成这项工作

VALUE rb_eval_string(const char *str)

计算是在当前上下文中完成的,因此可以访问由 Ruby 定义的最内层方法的当前局部变量。

请注意,计算可能会引发异常。有一个更安全的函数

VALUE rb_eval_string_protect(const char *str, int *state)

如果发生错误,它将返回 nil。此外,如果成功计算了 str,则 *state 为零,否则为非零。

ID 或 Symbol

你可以直接调用方法,无需解析字符串。首先,我需要解释一下 ID。ID 是表示 Ruby 标识符(例如变量名)的整数。ID 对应的 Ruby 数据类型是 Symbol。可以通过以下形式从 Ruby 访问它

:Identifier

:"any kind of string"

你可以使用以下方法从 C 代码中获取字符串中的 ID 值

rb_intern(const char *name)
rb_intern_str(VALUE name)

你可以使用以下方法从作为参数提供的 Ruby 对象(SymbolString)中检索 ID

rb_to_id(VALUE symbol)
rb_check_id(volatile VALUE *name)
rb_check_id_cstr(const char *name, long len, rb_encoding *enc)

如果参数不是 SymbolString,这些函数会尝试将其转换为 String。第二个函数将转换后的结果存储在 *name 中,如果字符串不是已知的符号,则返回 0。此函数返回非零值后,*name 始终是 SymbolString,否则如果结果为 0,则为 String。第三个函数采用以 NUL 结尾的 C 字符串,而不是 Ruby VALUE。

你可以使用以下方法从作为参数提供的 Ruby 对象(SymbolString)中检索 Symbol

rb_to_symbol(VALUE name)
rb_check_symbol(volatile VALUE *namep)
rb_check_symbol_cstr(const char *ptr, long len, rb_encoding *enc)

这些函数与上述函数类似,只是它们返回 Symbol,而不是 ID。

你可以使用以下方法将 C ID 转换为 Ruby Symbol

VALUE ID2SYM(ID id)

要将 Ruby Symbol 对象转换为 ID,请使用

ID SYM2ID(VALUE symbol)

从 C 调用 Ruby 方法

要直接调用方法,可以使用以下函数

VALUE rb_funcall(VALUE recv, ID mid, int argc, ...)

此函数使用符号 mid 指定的方法名,对 recv 调用方法。

访问变量和常量

你可以使用访问函数访问类变量和实例变量。此外,可以在两个环境之间共享全局变量。无法访问 Ruby 的局部变量。

用于访问/修改实例变量的函数如下

VALUE rb_ivar_get(VALUE obj, ID id)
VALUE rb_ivar_set(VALUE obj, ID id, VALUE val)

id 必须是符号,可以通过 rb_intern() 检索。

要访问类/模块的常量

VALUE rb_const_get(VALUE obj, ID id)

另请参阅上面的常量定义。

Ruby 和 C 之间的信息共享

可以从 C 访问的 Ruby 常量

如第 1.3 节所述,可以从 C 引用以下 Ruby 常量。

Qtrue
Qfalse

布尔值。Qfalse 在 C 中也是 false(即 0)。

Qnil

C 范围内的 Ruby nil。

C 和 Ruby 之间共享的全局变量

可以使用共享全局变量在两个环境之间共享信息。要定义它们,可以使用下面列出的函数

void rb_define_variable(const char *name, VALUE *var)

此函数定义了由两个环境共享的变量。可以通过名为“name”的 Ruby 全局变量访问“var”指向的全局变量的值。

可以使用以下函数定义只读(当然是从 Ruby 中)变量。

void rb_define_readonly_variable(const char *name, VALUE *var)

可以定义挂钩变量。在访问挂钩变量时调用访问器函数(getter 和 setter)。

void rb_define_hooked_variable(const char *name, VALUE *var,
                               VALUE (*getter)(), void (*setter)())

如果需要提供 setter 或 getter,只需为不需要的挂钩提供 0。如果两个挂钩都是 0,则 rb_define_hooked_variable() 的工作方式与 rb_define_variable() 相同。

getter 和 setter 函数的原型如下

VALUE (*getter)(ID id, VALUE *var);
void (*setter)(VALUE val, ID id, VALUE *var);

还可以定义没有相应 C 变量的 Ruby 全局变量。变量的值将仅由挂钩设置/获取。

void rb_define_virtual_variable(const char *name,
                                VALUE (*getter)(), void (*setter)())

getter 和 setter 函数的原型如下

VALUE (*getter)(ID id);
void (*setter)(VALUE val, ID id);

将 C 数据封装到 Ruby 对象中

有时需要将 C 世界中的结构公开为 Ruby 对象。在这种情况下,利用 TypedData_XXX 宏系列,可以相互转换指向结构和 Ruby 对象的指针。

C 结构到 Ruby 对象

可以使用下一个宏将指向结构的 sval 转换为 Ruby 对象。

TypedData_Wrap_Struct(klass, data_type, sval)

TypedData_Wrap_Struct() 返回一个创建的 Ruby 对象作为 VALUE。

klass 参数是对象的类。klass 应该派生自 rb_cObject,并且必须通过调用 rb_define_alloc_func 或 rb_undef_alloc_func 来设置分配器。

data_type 是指向 const rb_data_type_t 的指针,它描述了 Ruby 应如何管理结构。

rb_data_type_t 这样定义。让我们看看结构的每个成员。

typedef struct rb_data_type_struct rb_data_type_t;

struct rb_data_type_struct {
    const char *wrap_struct_name;
    struct {
        void (*dmark)(void*);
        void (*dfree)(void*);
        size_t (*dsize)(const void *);
        void (*dcompact)(void*);
        void *reserved[1];
    } function;
    const rb_data_type_t *parent;
    void *data;
    VALUE flags;
};

wrap_struct_name 是此结构实例的标识符。它基本上用于收集和发出统计信息。因此,标识符在进程中必须是唯一的,但不需要作为 C 或 Ruby 标识符有效。

这些 dmark / dfree 函数在GC执行期间调用。期间不允许分配对象,因此不要在其中分配 ruby 对象。

dmark 是一个函数,用于标记从你的结构引用的 Ruby 对象。如果你的结构保留了此类引用,则它必须使用 rb_gc_mark 或其系列标记来自你的结构的所有引用。

dfree 是一个函数,用于释放指针分配。如果这是 RUBY_DEFAULT_FREE,则指针将被释放。

dsize 按字节计算结构的内存消耗。其参数是指向你的结构的指针。如果难以实现这样的函数,你可以将 0 作为 dsize 传递。但仍然建议避免使用 0。

当发生内存压缩时调用 dcompact。由 rb_gc_mark_movable() 标记的引用的 Ruby 对象可以在此处根据 rb_gc_location() 进行更新。

你必须用 0 填充 reserved。

parent 可以指向 Ruby 对象继承自的另一个 C 类型定义。然后 TypedData_Get_Struct() 也接受派生对象。

你可以用任意值填充“data”以供你使用。Ruby 不会对成员做任何操作。

flags 是以下标志值的按位 OR。由于它们需要深入了解 Ruby 中的垃圾收集器,因此如果你不确定,你可以将 0 设置为 flags。

RUBY_TYPED_FREE_IMMEDIATELY

此标志使垃圾收集器在需要释放你的结构时立即在 GC 期间调用 dfree()。如果 dfree 从不解锁 Ruby 的内部锁 (GVL),你可以指定此标志。

如果未设置此标志,Ruby 会延迟调用 dfree(),并在与终结器相同的时间调用 dfree()。

RUBY_TYPED_WB_PROTECTED

它表明对象的实现支持写入屏障。如果设置此标志,Ruby 能够更好地对对象进行垃圾收集。

但是,当它被设置时,你负责在该对象的各个方法实现中适当地放置写入屏障。否则,Ruby 可能会在运行时崩溃。

有关写入屏障的更多信息,请参见附录 D 中的“代际 GC”。

RUBY_TYPED_FROZEN_SHAREABLE

此标志表示如果对象被冻结,则该对象是可共享对象。有关更多详细信息,请参见附录 F。

如果未设置此标志,则对象不能通过 Ractor.make_shareable() 方法成为可共享对象。

你可以在一步中分配和包装结构。

TypedData_Make_Struct(klass, type, data_type, sval)

此宏返回一个已分配的 T_DATA 对象,包装指向结构的指针,该指针也已分配。此宏的工作原理如下

(sval = ZALLOC(type), TypedData_Wrap_Struct(klass, data_type, sval))

参数 klass 和 data_type 的工作原理与其在 TypedData_Wrap_Struct() 中的对应参数类似。指向已分配结构的指针将被分配给 sval,它应该是指定类型的指针。

声明式标记/压缩结构引用

在结构引用 Ruby 对象(这些对象是简单值,未包装在条件逻辑或复杂数据结构中)的情况下,通过声明指向结构中值的偏移引用,提供了标记和引用更新的替代方法。

这样做允许 Ruby GC 支持标记这些引用和 GC 压缩,而无需定义 dmarkdcompact 回调。

您必须定义一个指向结构中引用所在偏移量的值指针的静态列表,并将“data”成员设置为指向此引用列表。引用列表必须以 RUBY_END_REFS 结尾。

已提供一些宏以简化边缘引用

以下示例来自 Dir(在 dir.c 中定义)

// The struct being wrapped. Notice this contains 3 members of which the second
// is a VALUE reference to another ruby object.
struct dir_data {
    DIR *dir;
    const VALUE path;
    rb_encoding *enc;
}

// Define a reference list `dir_refs` containing a single entry to `path`.
// Needs terminating with RUBY_REF_END
RUBY_REFERENCES(dir_refs) = {
    RUBY_REF_EDGE(dir_data, path),
    RUBY_REF_END
};

// Override the "dmark" field with the defined reference list now that we
// no longer need a marking callback and add RUBY_TYPED_DECL_MARKING to the
// flags field
static const rb_data_type_t dir_data_type = {
    "dir",
    {RUBY_REFS_LIST_PTR(dir_refs), dir_free, dir_memsize,},
    0, NULL, RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_DECL_MARKING
};

以这种方式声明简单引用允许 GC 标记和移动基础对象,并在压缩期间自动更新对它的引用。

Ruby 对象到 C 结构

要从 T_DATA 对象中检索 C 指针,请使用宏 TypedData_Get_Struct()。

TypedData_Get_Struct(obj, type, &data_type, sval)

指向结构的指针将被分配给变量 sval。

有关详细信息,请参见以下示例。

示例 - 创建 dbm 扩展

好的,下面是制作扩展库的示例。这是访问 DBM 的扩展。完整源代码包含在 Ruby 源代码树的 ext/ 目录中。

创建目录

% mkdir ext/dbm

在 ext 目录下为扩展库创建一个目录。

设计库

在创建库之前,你需要设计库的功能。

编写 C 代码

你需要为扩展库编写 C 代码。如果你的库只有一个源文件,建议将文件名选择为“LIBRARY.c”。另一方面,如果你的库有多个源文件,请避免将文件名选择为“LIBRARY.c”。在某些平台上,它可能会与中间文件“LIBRARY.o”冲突。请注意,下面描述的 mkmf 库中的一些函数会生成一个文件“conftest.c”以进行编译检查。你不应将“conftest.c”选择为源文件的文件名。

Ruby 将执行库中名为“Init_LIBRARY”的初始化函数。例如,在加载库时将执行“Init_dbm()”。

以下是初始化函数的示例。

#include <ruby.h>
void
Init_dbm(void)
{
    /* define DBM class */
    VALUE cDBM = rb_define_class("DBM", rb_cObject);
    /* Redefine DBM.allocate
    rb_define_alloc_func(cDBM, fdbm_alloc);
    /* DBM includes Enumerable module */
    rb_include_module(cDBM, rb_mEnumerable);

    /* DBM has class method open(): arguments are received as C array */
    rb_define_singleton_method(cDBM, "open", fdbm_s_open, -1);

    /* DBM instance method close(): no args */
    rb_define_method(cDBM, "close", fdbm_close, 0);
    /* DBM instance method []: 1 argument */
    rb_define_method(cDBM, "[]", fdbm_aref, 1);

    /* ... */

    /* ID for a instance variable to store DBM data */
    id_dbm = rb_intern("dbm");
}

dbm 扩展使用 TypedData_Make_Struct 将 dbm 结构包装到 C 环境中。

struct dbmdata {
    int  di_size;
    DBM *di_dbm;
};

static const rb_data_type_t dbm_type = {
    "dbm",
    {0, free_dbm, memsize_dbm,},
    0, 0,
    RUBY_TYPED_FREE_IMMEDIATELY,
};

static VALUE
fdbm_alloc(VALUE klass)
{
    struct dbmdata *dbmp;
    /* Allocate T_DATA object and C struct and fill struct with zero bytes */
    return TypedData_Make_Struct(klass, struct dbmdata, &dbm_type, dbmp);
}

此代码将 dbmdata 结构包装到 Ruby 对象中。我们避免直接包装 DBM*,因为我们希望缓存大小信息。由于 Object.allocate 分配了一个普通的 T_OBJECT 类型(而不是 T_DATA),因此必须使用 rb_define_alloc_func() 覆盖它或使用 rb_undef_alloc_func() 删除它。

要从 Ruby 对象中检索 dbmdata 结构,我们定义以下宏

#define GetDBM(obj, dbmp) do {\
    TypedData_Get_Struct((obj), struct dbmdata, &dbm_type, (dbmp));\
    if ((dbmp) == 0) closed_dbm();\
    if ((dbmp)->di_dbm == 0) closed_dbm();\
} while (0)

这种复杂的宏对 DBM 执行检索和关闭检查。

有三种方法可以接收方法参数。首先,具有固定数量参数的方法接收如下参数

static VALUE
fdbm_aref(VALUE obj, VALUE keystr)
{
    struct dbmdata *dbmp;
    GetDBM(obj, dbmp);
    /* Use dbmp to access the key */
    dbm_fetch(dbmp->di_dbm, StringValueCStr(keystr));
    /* ... */
}

C 函数的第一个参数是 self,其余参数是方法的参数。

其次,具有任意数量参数的方法接收如下参数

static VALUE
fdbm_s_open(int argc, VALUE *argv, VALUE klass)
{
    /* ... */
    if (rb_scan_args(argc, argv, "11", &file, &vmode) == 1) {
        mode = 0666;          /* default value */
    }
    /* ... */
}

第一个参数是方法参数的数量,第二个参数是方法参数的 C 数组,第三个参数是方法的接收者。

你可以使用函数 rb_scan_args() 检查和检索参数。第三个参数是一个字符串,指定如何捕获方法参数并将它们分配给以下 VALUE 引用。

你只需使用 rb_check_arity() 检查参数数量,在将参数视为列表时,这很方便。

以下是通过 Ruby 数组获取参数的方法示例

static VALUE
thread_initialize(VALUE thread, VALUE args)
{
    /* ... */
}

第一个参数是接收器,第二个参数是包含方法参数的 Ruby 数组。

注意GC 应了解引用 Ruby 对象但未导出到 Ruby 世界的全局变量。您需要通过以下方式保护它们

void rb_global_variable(VALUE *var)

或通过以下方式保护对象本身

void rb_gc_register_mark_object(VALUE object)

准备 extconf.rb

如果存在名为 extconf.rb 的文件,它将被执行以生成 Makefile。

extconf.rb 是用于检查编译条件等的文件。您需要将

require 'mkmf'

放在文件顶部。您可以使用以下函数来检查各种条件。

append_cppflags(array-of-flags[, opt]): append each flag to $CPPFLAGS if usable
append_cflags(array-of-flags[, opt]): append each flag to $CFLAGS if usable
append_ldflags(array-of-flags[, opt]): append each flag to $LDFLAGS if usable
have_macro(macro[, headers[, opt]]): check whether macro is defined
have_library(lib[, func[, headers[, opt]]]): check whether library containing function exists
find_library(lib[, func, *paths]): find library from paths
have_func(func[, headers[, opt]): check whether function exists
have_var(var[, headers[, opt]]): check whether variable exists
have_header(header[, preheaders[, opt]]): check whether header file exists
find_header(header, *paths): find header from paths
have_framework(fw): check whether framework exists (for MacOS X)
have_struct_member(type, member[, headers[, opt]]): check whether struct has member
have_type(type[, headers[, opt]]): check whether type exists
find_type(type, opt, *headers): check whether type exists in headers
have_const(const[, headers[, opt]]): check whether constant is defined
check_sizeof(type[, headers[, opts]]): check size of type
check_signedness(type[, headers[, opts]]): check signedness of type
convertible_int(type[, headers[, opts]]): find convertible integer type
find_executable(bin[, path]): find executable file path
create_header(header): generate configured header
create_makefile(target[, target_prefix]): generate Makefile

有关这些函数的完整文档,请参见 MakeMakefile

以下变量的值将影响 Makefile。

$CFLAGS: included in CFLAGS make variable (such as -O)
$CPPFLAGS: included in CPPFLAGS make variable (such as -I, -D)
$LDFLAGS: included in LDFLAGS make variable (such as -L)
$objs: list of object file names

编译器/链接器标志通常不可移植,您应该分别使用 append_cppflagsappend_cpflagsappend_ldflags,而不是直接追加上述变量。

通常,对象文件列表是通过搜索源文件自动生成的,但如果在构建过程中将生成任何源文件,则必须明确定义它们。

如果未满足编译条件,则不应调用“create_makefile”。Makefile 将不会生成,编译将不会完成。

准备依赖项(可选)

如果存在名为 depend 的文件,Makefile 将包含该文件以检查依赖项。您可以通过调用来创建此文件

% gcc -MM *.c > depend

这是无害的。准备好它。

生成 Makefile

尝试通过以下方式生成 Makefile

ruby extconf.rb

如果应将库安装在 vendor_ruby 目录下而不是 site_ruby 目录下,请使用 –vendor 选项,如下所示。

ruby extconf.rb --vendor

如果您将扩展库放在 Ruby 源代码树的 ext 目录下,则不需要此步骤。在这种情况下,解释器的编译将为您执行此步骤。

运行 make

键入

make

编译您的扩展。如果您已将扩展库放在 Ruby 源代码树的 ext 目录下,则也不需要此步骤。

调试

您可能需要对扩展进行 rb_debug。可以通过在 ext/Setup 文件中添加目录名称来静态链接扩展,以便您可以使用调试器检查扩展。

完成!现在您有了扩展库

您可以对库执行任何操作。Ruby 作者不会对取决于 Ruby API 的代码提出任何限制。您可以随意使用、修改、分发或出售您的程序。

附录 A. Ruby 头文件和源文件概述

Ruby 头文件

$repo_root/include/ruby 下的所有内容都通过 make install 安装。它应该从 C 扩展中包含 #include <ruby.h>。除了以 rbimpl_RBIMPL_ 为前缀的符号外,所有符号都是公共 API。它们是实现细节,不应由 C 扩展使用。

只有 $repo_root/include/ruby/*.h 其对应宏在 $repo_root/include/ruby.h 头文件中定义,才能由 C 扩展 #include

$repo_root/internal/ 下或直接在根 $repo_root/*.h 下的头文件不会通过 make 安装。它们是仅包含内部 API 的内部头文件。

Ruby 语言核心

class.c

类和模块

error.c

异常类和异常机制

gc.c

内存管理

load.c

库加载

object.c

对象

variable.c

变量和常量

Ruby 语法解析器

parse.y

语法定义

parse.c

从 parse.y 自动生成

defs/keywords

保留关键字

lex.c

从关键字自动生成

Ruby 评估器(又称 YARV)

compile.c
eval.c
eval_error.c
eval_jump.c
eval_safe.c
insns.def           : definition of VM instructions
iseq.c              : implementation of VM::ISeq
thread.c            : thread management and context switching
thread_win32.c      : thread implementation
thread_pthread.c    : ditto
vm.c
vm_dump.c
vm_eval.c
vm_exec.c
vm_insnhelper.c
vm_method.c

defs/opt_insns_unif.def  : instruction unification
defs/opt_operand.def     : definitions for optimization

  -> insn*.inc           : automatically generated
  -> opt*.inc            : automatically generated
  -> vm.inc              : automatically generated

正则表达式引擎(Onigumo)

regcomp.c
regenc.c
regerror.c
regexec.c
regparse.c
regsyntax.c

实用函数

debug.c

C 调试器的调试符号

dln.c

动态加载

st.c

通用哈希表

strftime.c

格式化时间

util.c

杂项实用程序

Ruby 解释器实现

dmyext.c
dmydln.c
dmyencoding.c
id.c
inits.c
main.c
ruby.c
version.c

gem_prelude.rb
prelude.rb

Class

array.c

Array

bignum.c

大数字

compar.c

可比较

complex.c

复数

cont.c

FiberContinuation

dir.c

目录

enum.c

可枚举

enumerator.c

枚举器

file.c

文件

hash.c

哈希

io.c

IO

marshal.c

编组

math.c

数学

numeric.c

NumericInteger、Fixnum、Float

pack.c

Array#packString#unpack

proc.c

BindingProc

process.c

进程

random.c

随机数

range.c

范围

rational.c

有理数

re.c

RegexpMatchData

signal.c

信号

sprintf.c

String#sprintf

string.c

字符串

struct.c

结构

time.c

时间

defs/known_errors.def

Errno::* 异常类

-> known_errors.inc

自动生成

多语言

encoding.c

编码

transcode.c

Encoding::Converter

enc/*.c

编码类

enc/trans/*

代码点映射表

goruby 解释器实现

goruby.c
golf_prelude.rb     : goruby specific libraries.
  -> golf_prelude.c : automatically generated

附录 B. Ruby 扩展 API 参考

类型

VALUE

Ruby 对象的类型。实际结构在 ruby.h 中定义,例如 struct RString 等。要引用结构中的值,请使用强制转换宏,例如 RSTRING(obj)。

变量和常量

Qnil

nil 对象

Qtrue

true 对象(默认 true 值)

Qfalse

false 对象

C 指针包装

Data_Wrap_Struct(VALUE klass, void (*mark)(), void (*free)(), void *sval)

将 C 指针封装到 Ruby 对象中。如果对象引用其他 Ruby 对象,则应在 GC 过程中使用 mark 函数对其进行标记。否则,mark 应为 0。当不再引用此对象时,指针将由 free 函数丢弃。

Data_Make_Struct(klass, type, mark, free, sval)

此宏使用 malloc() 分配内存,将其分配给变量 sval,并返回封装指向内存区域的指针的 DATA。

Data_Get_Struct(data, type, sval)

此宏从 DATA 中检索指针值,并将其分配给变量 sval。

检查 VALUE 类型

RB_TYPE_P(value, type)

value 是否为内部类型(T_NIL、T_FIXNUM 等)?

TYPE(value)

内部类型(T_NIL、T_FIXNUM 等)

FIXNUM_P(value)

value 是否为 Fixnum?

NIL_P(value)

value 是否为 nil?

RB_INTEGER_TYPE_P(value)

value 是否为 Integer

RB_FLOAT_TYPE_P(value)

value 是否为 Float

void Check_Type(VALUE value, int type)

确保 value 为给定的内部 type,否则引发 TypeError

VALUE 类型转换

FIX2INT(value), INT2FIX(i)

Fixnum <-> 整数

FIX2LONG(value), LONG2FIX(l)

Fixnum <-> 长整数

NUM2INT(value), INT2NUM(i)

Numeric <-> 整数

NUM2UINT(value), UINT2NUM(ui)

Numeric <-> 无符号整数

NUM2LONG(value), LONG2NUM(l)

Numeric <-> 长整数

NUM2ULONG(value), ULONG2NUM(ul)

Numeric <-> 无符号长整数

NUM2LL(value), LL2NUM(ll)

Numeric <-> 长长整数

NUM2ULL(value), ULL2NUM(ull)

Numeric <-> 无符号长长整数

NUM2OFFT(value), OFFT2NUM(off)

Numeric <-> off_t

NUM2SIZET(value), SIZET2NUM(size)

Numeric <-> size_t

NUM2SSIZET(value), SSIZET2NUM(ssize)

Numeric <-> ssize_t

rb_integer_pack(value, words, numwords, wordsize, nails, flags), rb_integer_unpack(words, numwords, wordsize, nails, flags)

Numeric <-> 任意大小的整数缓冲区

NUM2DBL(value)

Numeric -> double

rb_float_new(f)

double -> Float

RSTRING_LEN(str)

String -> String 数据的长度(以字节为单位)

RSTRING_PTR(str)

String -> 指向 String 数据的指针。请注意,结果指针可能没有以 NUL 结尾

StringValue(value)

具有 #to_str -> StringObject

StringValuePtr(value)

具有 #to_str -> 指向 String 数据的指针的 Object

StringValueCStr(value)

具有 #to_str -> 指向不带 NUL 字节的 String 数据的指针。保证结果数据以 NUL 结尾

rb_str_new2(s)

char * -> String

定义类和模块

VALUE rb_define_class(const char *name, VALUE super)

将新的 Ruby 类定义为 super 的子类。

VALUE rb_define_class_under(VALUE module, const char *name, VALUE super)

将新的 Ruby 类创建为 super 的子类,在模块的命名空间下。

VALUE rb_define_module(const char *name)

定义新的 Ruby 模块。

VALUE rb_define_module_under(VALUE module, const char *name)

在模块的命名空间下定义新的 Ruby 模块。

void rb_include_module(VALUE klass, VALUE module)

将模块包含到类中。如果类已经包含该模块,则忽略。

void rb_extend_object(VALUE object, VALUE module)

使用模块的属性扩展对象。

定义全局变量

void rb_define_variable(const char *name, VALUE *var)

定义在 C 和 Ruby 之间共享的全局变量。如果 name 包含不允许作为符号一部分的字符,则无法从 Ruby 程序中看到它。

void rb_define_readonly_variable(const char *name, VALUE *var)

定义只读全局变量。与 rb_define_variable() 的工作方式相同,不同之处在于定义的变量是只读的。

void rb_define_virtual_variable(const char *name, VALUE (*getter)(), void (*setter)())

定义虚拟变量,其行为由一对 C 函数定义。在引用变量时调用 getter 函数。在将变量设置为值时调用 setter 函数。getter/setter 函数的原型为

VALUE getter(ID id)
void setter(VALUE val, ID id)

getter 函数必须返回访问的值。

void rb_define_hooked_variable(const char *name, VALUE *var, VALUE (*getter)(), void (*setter)())

定义挂钩变量。它是一个带有 C 变量的虚拟变量。getter 被调用为

VALUE getter(ID id, VALUE *var)

返回新值。setter 被调用为

void setter(VALUE val, ID id, VALUE *var)
void rb_global_variable(VALUE *var)

告诉 GC 保护 C 全局变量,该变量保存要标记的 Ruby 值。

void rb_gc_register_mark_object(VALUE object)

告诉 GC 保护 object,该对象可能在任何地方都没有引用。

常量定义

void rb_define_const(VALUE klass, const char *name, VALUE val)

在类/模块下定义新常量。

void rb_define_global_const(const char *name, VALUE val)

定义全局常量。这与

rb_define_const(rb_cObject, name, val)

Method 定义相同

rb_define_method(VALUE klass, const char *name, VALUE (*func)(ANYARGS), int argc)

为类定义方法。func 是函数指针。argc 是参数数量。如果 argc 为 -1,则该函数将接收 3 个参数:argc、argv 和 self。如果 argc 为 -2,则该函数将接收 2 个参数,self 和 args,其中 args 是方法参数的 Ruby 数组。

rb_define_private_method(VALUE klass, const char *name, VALUE (*func)(ANYARGS), int argc)

为类定义私有方法。参数与 rb_define_method() 相同。

rb_define_singleton_method(VALUE klass, const char *name, VALUE (*func)(ANYARGS), int argc)

定义单例方法。参数与 rb_define_method() 相同。

rb_check_arity(int argc, int min, int max)

检查参数数量,argc 在 min..max 范围内。如果 max 为 UNLIMITED_ARGUMENTS,则不检查上限。如果 argc 超出范围,将引发 ArgumentError

rb_scan_args(int argc, VALUE *argv, const char *fmt, …)

根据格式字符串,从 argc 和 argv 中检索参数到给定的 VALUE 引用。格式可以用 ABNF 描述如下

scan-arg-spec  := param-arg-spec [keyword-arg-spec] [block-arg-spec]

param-arg-spec := pre-arg-spec [post-arg-spec] / post-arg-spec /
                  pre-opt-post-arg-spec
pre-arg-spec   := num-of-leading-mandatory-args [num-of-optional-args]
post-arg-spec  := sym-for-variable-length-args
                  [num-of-trailing-mandatory-args]
pre-opt-post-arg-spec := num-of-leading-mandatory-args num-of-optional-args
                         num-of-trailing-mandatory-args
keyword-arg-spec := sym-for-keyword-arg
block-arg-spec := sym-for-block-arg

num-of-leading-mandatory-args  := DIGIT ; The number of leading
                                        ; mandatory arguments
num-of-optional-args           := DIGIT ; The number of optional
                                        ; arguments
sym-for-variable-length-args   := "*"   ; Indicates that variable
                                        ; length arguments are
                                        ; captured as a ruby array
num-of-trailing-mandatory-args := DIGIT ; The number of trailing
                                        ; mandatory arguments
sym-for-keyword-arg            := ":"   ; Indicates that keyword
                                        ; argument captured as a hash.
                                        ; If keyword arguments are not
                                        ; provided, returns nil.
sym-for-block-arg              := "&"   ; Indicates that an iterator
                                        ; block should be captured if
                                        ; given

例如,“12”表示该方法至少需要一个参数,最多接收三个(1+2)个参数。因此,格式字符串后面必须紧跟三个变量引用,这些引用将被分配给捕获的参数。对于省略的参数,变量将设置为 Qnil。可以将 NULL 放置在变量引用位置,这意味着相应的捕获参数应该被直接丢弃。

返回给定参数的数量,不包括选项哈希或迭代器块。

rb_scan_args_kw(int kw_splat, int argc, VALUE *argv, const char *fmt, …)

rb_scan_args 相同,但 kw_splat 参数指定是否提供了关键字参数(而不是由从 Ruby 到 C 函数的调用确定)。kw_splat 应为以下值之一

RB_SCAN_ARGS_PASS_CALLED_KEYWORDS

rb_scan_args 相同的行为。

RB_SCAN_ARGS_KEYWORDS

最后一个参数应为一个哈希,作为关键字处理。

RB_SCAN_ARGS_LAST_HASH_KEYWORDS

如果最后一个参数是哈希,则将其视为关键字,否则不视为关键字。

int rb_get_kwargs(VALUE keyword_hash, const ID *table, int required, int optional, VALUE *values)

检索绑定到关键字的参数 VALUE,由 table 指向 values,同时从 keyword_hash 中删除已检索的条目。table 引用的第一个 required 个 ID 是必需的,而后续的 optional 个 ID(如果 optional 为负数,则为 optional - 1)是可选的。如果 keyword_hash 中不包含必需的键,则引发“缺少关键字”ArgumentError。如果 keyword_hash 中不存在可选键,则 values 中的相应元素将设置为 Qundef。如果 optional 为负数,则忽略 keyword_hash 的其余部分,否则引发“未知关键字”ArgumentError

请注意,在 C API 中处理关键字参数的效率低于在 Ruby 中处理它们。考虑在非关键字 C 函数周围使用 Ruby 包装器方法。参考:bugs.ruby-lang.org/issues/11339

VALUE rb_extract_keywords(VALUE *original_hash)

original_hash 引用的哈希对象中提取键为符号的对到一个新哈希中。如果原始哈希包含非符号键,则它们将被复制到另一个哈希中,并且新哈希将通过 original_hash 存储,否则存储 0。

调用 Ruby 方法

VALUE rb_funcall(VALUE recv, ID mid, int narg, …)

调用一个方法。要从方法名中检索 mid,请使用 rb_intern()。能够调用私有/受保护的方法。

VALUE rb_funcall2(VALUE recv, ID mid, int argc, VALUE *argv)
VALUE rb_funcallv(VALUE recv, ID mid, int argc, VALUE *argv)

调用一个方法,将参数作为值数组传递。能够调用私有/受保护的方法。

VALUE rb_funcallv_kw(VALUE recv, ID mid, int argc, VALUE *argv, int kw_splat)

与 rb_funcallv 相同,使用 kw_splat 确定是否传递关键字参数。

VALUE rb_funcallv_public(VALUE recv, ID mid, int argc, VALUE *argv)

调用方法,将参数作为值数组传递。只能调用公共方法。

VALUE rb_funcallv_public_kw(VALUE recv, ID mid, int argc, VALUE *argv, int kw_splat)

与 rb_funcallv_public 相同,使用 kw_splat 确定是否传递关键字参数。

VALUE rb_funcall_passing_block(VALUE recv, ID mid, int argc, const VALUE* argv)

与 rb_funcallv_public 相同,但它在调用方法时将当前活动块作为块传递。

VALUE rb_funcall_passing_block_kw(VALUE recv, ID mid, int argc, const VALUE* argv, int kw_splat)

与 rb_funcall_passing_block 相同,使用 kw_splat 确定是否传递关键字参数。

VALUE rb_funcall_with_block(VALUE recv, ID mid, int argc, const VALUE *argv, VALUE passed_procval)

与 rb_funcallv_public 相同,但 passed_procval 指定要传递给方法的块。

VALUE rb_funcall_with_block_kw(VALUE recv, ID mid, int argc, const VALUE *argv, VALUE passed_procval, int kw_splat)

与 rb_funcall_with_block 相同,使用 kw_splat 确定是否传递关键字参数。

VALUE rb_eval_string(const char *str)

将字符串编译并执行为 Ruby 程序。

ID rb_intern(const char *name)

返回与名称对应的 ID。

char *rb_id2name(ID id)

返回与 ID 对应的名称。

char *rb_class2name(VALUE klass)

返回类的名称。

int rb_respond_to(VALUE obj, ID id)

如果对象对 id 指定的消息做出响应,则返回 true。

实例变量

VALUE rb_iv_get(VALUE obj, const char *name)

检索实例变量的值。如果名称没有以“@”为前缀,则无法从 Ruby 访问该变量。

VALUE rb_iv_set(VALUE obj, const char *name, VALUE val)

设置实例变量的值。

控制结构

VALUE rb_block_call(VALUE recv, ID mid, int argc, VALUE * argv, VALUE (*func) (ANYARGS), VALUE data2)

使用符号 mid 指定的方法名,使用 argv 中的 argc 个参数在 recv 上调用方法,并提供 func 作为块。当 func 作为块调用时,它将收到 yield 的值作为第一个参数,data2 作为第二个参数。当使用多个值进行 yield(在 C 中,rb_yield_values()、rb_yield_values2() 和 rb_yield_splat())时,data2 会打包为 Array,而 yield 的值可以通过第三/第四个参数的 argc/argv 获得。

VALUE rb_block_call_kw(VALUE recv, ID mid, int argc, VALUE * argv, VALUE (*func) (ANYARGS), VALUE data2, int kw_splat)

与 rb_funcall_with_block 相同,使用 kw_splat 确定是否传递关键字参数。

[已弃用] VALUE rb_iterate(VALUE (*func1)(), VALUE arg1, VALUE (*func2)(), VALUE arg2)

调用函数 func1,提供 func2 作为块。func1 将使用参数 arg1 调用。func2 接收 yield 的值作为第一个参数,arg2 作为第二个参数。

当在 1.9 中使用 rb_iterate 时,func1 必须调用一些 Ruby 级方法。此函数自 1.9 起已弃用;请改用 rb_block_call。

VALUE rb_yield(VALUE val)

将 val 作为单个参数传递给块。

VALUE rb_yield_values(int n, …)

n 个参数传递给块,每个 Ruby 参数使用一个 C 参数。

VALUE rb_yield_values2(int n, VALUE *argv)

n 个参数传递给块,所有 Ruby 参数都在 C argv 数组中。

VALUE rb_yield_values_kw(int n, VALUE *argv, int kw_splat)

与 rb_yield_values2 相同,使用 kw_splat 确定是否传递关键字参数。

VALUE rb_yield_splat(VALUE args)

与 rb_yield_values2 相同,除了参数由 Ruby 数组 args 指定。

VALUE rb_yield_splat_kw(VALUE args, int kw_splat)

与 rb_yield_splat 相同,使用 kw_splat 确定是否传递关键字参数。

VALUE rb_rescue(VALUE (*func1)(ANYARGS), VALUE arg1, VALUE (*func2)(ANYARGS), VALUE arg2)

使用 arg1 作为参数调用函数 func1。如果在 func1 期间发生异常,则使用 arg2 作为第一个参数和异常对象作为第二个参数调用 func2。如果未发生异常,则 rb_rescue() 的返回值是 func1 的返回值,否则是 func2 的返回值。

VALUE rb_ensure(VALUE (*func1)(ANYARGS), VALUE arg1, VALUE (*func2)(ANYARGS), VALUE arg2)

使用 arg1 作为参数调用函数 func1,然后在执行终止时使用 arg2 调用 func2。当未发生异常时,rb_ensure() 的返回值是 func1 的返回值。

VALUE rb_protect(VALUE (*func) (VALUE), VALUE arg, int *state)

使用 arg 作为参数调用函数 func。如果在 func 期间未发生异常,则返回 func 的结果,并且 *state 为零。否则,返回 Qnil 并将 *state 设置为非零。如果 state 为 NULL,则在两种情况下都不会设置。忽略捕获的异常时,必须使用 rb_set_errinfo(Qnil) 清除错误信息。

void rb_jump_tag(int state)

继续由 rb_protect() 和 rb_eval_string_protect() 捕获的异常。state 必须是这些函数返回的值。此函数绝不会返回到调用者。

void rb_iter_break()

退出当前最内层块。此函数绝不会返回到调用者。

void rb_iter_break_value(VALUE value)

退出当前最内部块并带有值。该块将返回给定的参数值。此函数永远不会返回调用方。

异常和错误

void rb_warn(const char *fmt, …)

根据类似 printf 的格式打印警告消息。

void rb_warning(const char *fmt, …)

如果 $VERBOSE 为 true,则根据类似 printf 的格式打印警告消息。

void rb_raise(rb_eRuntimeError, const char *fmt, …)

引发 RuntimeError。fmt 是一个类似 printf() 的格式字符串。

void rb_raise(VALUE exception, const char *fmt, …)

引发类异常。fmt 是一个类似 printf() 的格式字符串。

void rb_fatal(const char *fmt, …)

引发致命错误,终止解释器。致命错误不会进行异常处理,但会执行 ensure 块。

void rb_bug(const char *fmt, …)

立即终止解释器。此函数应在解释器中出现错误的情况下调用。不会执行异常处理或 ensure 执行。

注意:在格式字符串中,“%”PRIsVALUE 可用于 Object#to_s(或如果设置了“+”标志,则为 Object#inspect)输出(并且相关参数必须为 VALUE)。由于它与“%i”冲突,因此对于格式字符串中的整数,请使用“%d”。

线程

从 Ruby 1.9 开始,Ruby 支持本机 1:1 线程,每个 Ruby Thread 对象对应一个内核线程。目前,有一个 GVL(全局 VM 锁),它防止同时执行 Ruby 代码,该代码可以通过 rb_thread_call_without_gvl 和 rb_thread_call_without_gvl2 函数释放。这些函数使用起来很棘手,并在 thread.c 中有记录;在阅读 thread.c 中的注释之前,请勿使用它们。

void rb_thread_schedule(void)

向调度程序提供提示,以将执行传递给另一个线程。

单个文件描述符上的输入/输出 (IO)

int rb_io_wait_readable(int fd)

无限期地等待给定的 FD 变为可读,允许调度其他线程。如果可以执行读取,则返回 true 值;如果出现不可恢复的错误,则返回 false。

int rb_io_wait_writable(int fd)

类似于 rb_io_wait_readable,但用于可写性。

int rb_wait_for_single_fd(int fd, int events, struct timeval *timeout)

允许在单个 FD 上等待一个或多个事件,并指定超时时间。

events 是以下值的任意组合的掩码

  • RB_WAITFD_IN - 等待普通数据的可读性

  • RB_WAITFD_OUT - 等待可写性

  • RB_WAITFD_PRI - 等待紧急数据的可读性

使用 NULL timeout 无限期等待。

I/O 多路复用

Ruby 支持基于 select(2) 系统调用的 I/O 多路复用。Linux select_tut(2) 手册页 <man7.org/linux/man-pages/man2/select_tut.2.html> 提供了有关如何使用 select(2) 的良好概述,并且 Ruby API 具有与众所周知的 select API 类似的函数和数据结构。了解 select(2) 是理解本节的必要条件。

typedef struct rb_fdset_t

包装 select(2) 使用的 fd_set 位图的数据结构。这允许 Ruby 使用比现代平台上的历史限制允许的更大的 FD 集。

void rb_fd_init(rb_fdset_t *)

初始化 rb_fdset_t,在其他 rb_fd_* 操作之前必须对其进行初始化。类似于调用 malloc(3) 来分配 fd_set。

void rb_fd_term(rb_fdset_t *)

销毁 rb_fdset_t,释放其使用的任何内存和资源。在将来使用之前,必须使用 rb_fd_init 重新初始化它。类似于调用 free(3) 来释放 fd_set 的内存。

void rb_fd_zero(rb_fdset_t *)

从 rb_fdset_t 中清除所有 FD,类似于 FD_ZERO(3)。

void rb_fd_set(int fd, rb_fdset_t *)

在 rb_fdset_t 中添加给定的 FD,类似于 FD_SET(3)。

void rb_fd_clr(int fd, rb_fdset_t *)

从 rb_fdset_t 中移除给定的 FD,类似于 FD_CLR(3)。

int rb_fd_isset(int fd, const rb_fdset_t *)

如果在 rb_fdset_t 中设置了给定的 FD,则返回 true,否则返回 false。类似于 FD_ISSET(3)。

int rb_thread_fd_select(int nfds, rb_fdset_t *readfds, rb_fdset_t *writefds, rb_fdset_t *exceptfds, struct timeval *timeout)

类似于 select(2) 系统调用,但允许在等待时调度其他 Ruby 线程。

仅在单个 FD 上等待时,优先使用 rb_io_wait_readable、rb_io_wait_writable 或 rb_wait_for_single_fd 函数,因为它们可以针对特定平台进行优化(目前仅限于 Linux)。

初始化并启动解释器

嵌入式 API 函数如下(扩展库不需要)

void ruby_init()

初始化解释器。

void *ruby_options(int argc, char **argv)

Process解释器的命令行参数。并编译要执行的 Ruby 源代码。它返回一个指向已编译源代码的不透明指针或一个内部特殊值。

int ruby_run_node(void *n)

运行给定的已编译源代码并退出此进程。如果成功运行源代码,则返回 EXIT_SUCCESS。否则,它返回其他值。

void ruby_script(char *name)

指定脚本的名称 ($0)。

解释器事件的挂钩

void rb_add_event_hook(rb_event_hook_func_t func, rb_event_flag_t events, VALUE data)

为指定的解释器事件添加一个挂钩函数。events 应该是以下值的 OR 值

RUBY_EVENT_LINE
RUBY_EVENT_CLASS
RUBY_EVENT_END
RUBY_EVENT_CALL
RUBY_EVENT_RETURN
RUBY_EVENT_C_CALL
RUBY_EVENT_C_RETURN
RUBY_EVENT_RAISE
RUBY_EVENT_ALL

rb_event_hook_func_t 的定义如下

typedef void (*rb_event_hook_func_t)(rb_event_t event, VALUE data,
                                     VALUE self, ID id, VALUE klass)

rb_add_event_hook() 的第三个参数“data”作为第二个参数传递给挂钩函数,该参数在 1.8 中是指向当前 NODE 的指针。请参见下面的 RB_EVENT_HOOKS_HAVE_CALLBACK_DATA。

int rb_remove_event_hook(rb_event_hook_func_t func)

移除指定的挂钩函数。

内存使用

void rb_gc_adjust_memory_usage(ssize_t diff)

调整已注册外部内存的数量。您可以通过此函数告诉 GC 外部库使用了多少内存。使用正 diff 调用此函数表示内存使用量增加;分配新的内存块或将块重新分配为更大的大小。使用负 diff 调用此函数表示内存使用量减少;释放内存块或将块重新分配为更小的尺寸。此函数可能会触发 GC

兼容性宏

默认情况下提供了一些宏来检查 API 兼容性。

NORETURN_STYLE_NEW

表示 NORETURN 宏是函数式而不是前缀。

HAVE_RB_DEFINE_ALLOC_FUNC

表示提供了函数 rb_define_alloc_func(),这意味着使用了分配框架。这与 have_func(“rb_define_alloc_func”, “ruby.h”) 的结果相同。

HAVE_RB_REG_NEW_STR

表示提供了函数 rb_reg_new_str(),它从 String 对象创建 Regexp 对象。这与 have_func(“rb_reg_new_str”, “ruby.h”) 的结果相同。

HAVE_RB_IO_T

表示提供了类型 rb_io_t。

USE_SYMBOL_AS_METHOD_NAME

表示将符号作为方法名返回,例如 Module#methods、#singleton_methods 等。

HAVE_RUBY_*_H

在 ruby.h 中定义,表示可用的相应标头。例如,当定义 HAVE_RUBY_ST_H 时,应使用 ruby/st.h,而不用 st.h。

与这些宏相对应的标头文件可以直接从扩展库中 #include

RB_EVENT_HOOKS_HAVE_CALLBACK_DATA

表示 rb_add_event_hook() 采用第三个参数“data”,传递给给定的事件挂钩函数。

为关键字参数函数定义向后兼容的宏

大多数 Ruby C 扩展都旨在支持多个 Ruby 版本。为了在关键字参数分离方面正确支持 Ruby 2.7+,C 扩展需要使用 *_kw 函数。但是,这些函数在 Ruby 2.6 及更低版本中不存在,因此在这些情况下应定义宏以允许你在多个 Ruby 版本上使用相同的代码。以下是在使用 Ruby 2.7 中引入的 *_kw 函数时,在支持 Ruby 2.6(或更低版本)的扩展中可以使用的一些示例宏。

#ifndef RB_PASS_KEYWORDS
/* Only define macros on Ruby <2.7 */
#define rb_funcallv_kw(o, m, c, v, kw) rb_funcallv(o, m, c, v)
#define rb_funcallv_public_kw(o, m, c, v, kw) rb_funcallv_public(o, m, c, v)
#define rb_funcall_passing_block_kw(o, m, c, v, kw) rb_funcall_passing_block(o, m, c, v)
#define rb_funcall_with_block_kw(o, m, c, v, b, kw) rb_funcall_with_block(o, m, c, v, b)
#define rb_scan_args_kw(kw, c, v, s, ...) rb_scan_args(c, v, s, __VA_ARGS__)
#define rb_call_super_kw(c, v, kw) rb_call_super(c, v)
#define rb_yield_values_kw(c, v, kw) rb_yield_values2(c, v)
#define rb_yield_splat_kw(a, kw) rb_yield_splat(a)
#define rb_block_call_kw(o, m, c, v, f, p, kw) rb_block_call(o, m, c, v, f, p)
#define rb_fiber_resume_kw(o, c, v, kw) rb_fiber_resume(o, c, v)
#define rb_fiber_yield_kw(c, v, kw) rb_fiber_yield(c, v)
#define rb_enumeratorize_with_size_kw(o, m, c, v, f, kw) rb_enumeratorize_with_size(o, m, c, v, f)
#define SIZED_ENUMERATOR_KW(obj, argc, argv, size_fn, kw_splat) \
    rb_enumeratorize_with_size((obj), ID2SYM(rb_frame_this_func()), \
                               (argc), (argv), (size_fn))
#define RETURN_SIZED_ENUMERATOR_KW(obj, argc, argv, size_fn, kw_splat) do { \
        if (!rb_block_given_p())                                            \
            return SIZED_ENUMERATOR(obj, argc, argv, size_fn);              \
    } while (0)
#define RETURN_ENUMERATOR_KW(obj, argc, argv, kw_splat) RETURN_SIZED_ENUMERATOR(obj, argc, argv, 0)
#define rb_check_funcall_kw(o, m, c, v, kw) rb_check_funcall(o, m, c, v)
#define rb_obj_call_init_kw(o, c, v, kw) rb_obj_call_init(o, c, v)
#define rb_class_new_instance_kw(c, v, k, kw) rb_class_new_instance(c, v, k)
#define rb_proc_call_kw(p, a, kw) rb_proc_call(p, a)
#define rb_proc_call_with_block_kw(p, c, v, b, kw) rb_proc_call_with_block(p, c, v, b)
#define rb_method_call_kw(c, v, m, kw) rb_method_call(c, v, m)
#define rb_method_call_with_block_kw(c, v, m, b, kw) rb_method_call_with_block(c, v, m, b)
#define rb_eval_cmd_kwd(c, a, kw) rb_eval_cmd(c, a, 0)
#endif

附录 C. 可用于 extconf.rb 的函数

请参阅 mkmf 的文档。

附录 D. 分代 GC

Ruby 2.1 引入了分代垃圾收集器(称为 RGenGC)。RGenGC(基本上)保持兼容性。

通常,在分代 GCen.wikipedia.org/wiki/Garbage_collection_%28computer_science%29)的扩展库中需要使用称为写屏障的技术。RGenGC 在扩展库中正常工作,无需写屏障。

如果你的库遵循以下提示,则可以进一步提高性能。尤其是,“不要直接接触指针”部分很重要。

不兼容性

你不能直接编写 RBASIC(obj)->klass 字段,因为它现在是 const 值。

基本上你不应该编写此字段,因为 MRI 希望它是一个不可变字段,但如果你想在扩展中执行此操作,可以使用以下函数

VALUE rb_obj_hide(VALUE obj)

清除 RBasic::klass 字段。对象将成为内部对象。 ObjectSpace::each_object 无法找到此对象。

VALUE rb_obj_reveal(VALUE obj, VALUE klass)

将 RBasic::klass 重置为 klass。我们希望“klass”是通过 rb_obj_hide() 隐藏的类。

写入屏障

RGenGC 不需要写入屏障来支持代际 GC。但是,关注写入屏障可以提高 RGenGC 的性能。请查看以下提示。

不要直接接触指针

在 MRI(include/ruby/ruby.h)中,支持一些用于获取内部数据结构的指针的宏,例如 RARRAY_PTR()、RSTRUCT_PTR() 等。

不要使用这些宏,而应使用相应的 C-API,例如 rb_ary_aref()、rb_ary_store() 等。

考虑是否插入写入屏障

如果你只使用内置类型,则无需关注写入屏障。

如果你支持 T_DATA 对象,则可以考虑使用写入屏障。

仅对以下类型的对象将写入屏障插入 T_DATA 对象有效:(a) 长寿命对象,(b) 生成大量对象时,(c) 具有对其他对象的引用的容器类型对象。如果你的扩展提供了此类 T_DATA 对象,请考虑插入写入屏障。

(a):短寿命对象不会成为老生代对象。(b):只有少数老生代对象没有性能影响。(c):只有少数引用没有性能影响。

插入写入屏障是一个非常困难的技巧,很容易引入严重错误。插入写入屏障还有几个开销领域。基本上,我们不建议你插入写入屏障。请仔细考虑风险。

与内置类型结合

请考虑利用内置类型。大多数内置类型都支持写入屏障,因此你可以使用它们来避免手动插入写入屏障。

例如,如果你的 T_DATA 引用其他对象,则可以将这些引用移动到 Array 中。T_DATA 对象仅引用数组对象。或者,你还可以使用 Struct 对象来收集 T_DATA 对象(没有任何引用)和包含引用的 Array

使用此类技术后,你无需再插入写入屏障。

插入写屏障

[再次] 插入写屏障是一种非常困难的 hack,并且很容易引入严重错误。而且插入写屏障有几个方面开销。基本上我们不建议你插入写屏障。请仔细考虑风险。

在插入写屏障之前,你需要了解 RGenGC 算法(gc.c 将帮助你)。插入写屏障的宏和函数在 include/ruby/ruby.h 中提供。iseq.c 中提供了一个示例。

有关 RGenGC 和写屏障的完整指南,请参阅 <bugs.ruby-lang.org/projects/ruby-master/wiki/RGenGC>。

附录 E. RB_GC_GUARD 以防止过早 GC

C Ruby 目前使用保守垃圾回收,因此 VALUE 变量必须在堆栈或寄存器上保持可见,以确保任何关联数据仍然可用。优化 C 编译器并非针对保守垃圾回收而设计,因此即使代码依赖于与该 VALUE 关联的数据,它们也可能会优化掉原始 VALUE。

以下示例说明了如何使用 RB_GC_GUARD 来确保 sptr 的内容在 rb_str_new_cstr 的第二次调用运行时仍然有效。

VALUE s, w;
const char *sptr;

s = rb_str_new_cstr("hello world!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
sptr = RSTRING_PTR(s);
w = rb_str_new_cstr(sptr + 6); /* Possible GC invocation */

RB_GC_GUARD(s); /* ensure s (and thus sptr) do not get GC-ed */

在上述示例中,RB_GC_GUARD 必须放在 sptr 的最后一次使用之后。在取消引用 sptr 之前放置 RB_GC_GUARD 将毫无用处。RB_GC_GUARD 仅对 VALUE 数据类型有效,而不适用于转换后的 C 数据类型。

如果在取消引用 sptr 之后对“s”VALUE 进行了非内联函数调用,则在上述示例中根本不需要 RB_GC_GUARD。因此,在上述示例中,对“s”调用任何未内联的函数,例如

rb_str_modify(s);

将确保“s”保留在堆栈或寄存器上,以防止 GC 调用过早地释放它。

使用 RB_GC_GUARD 宏比在 C 中使用“volatile”关键字更好。RB_GC_GUARD 具有以下优点

  1. 宏使用的意图很明确

  2. RB_GC_GUARD 仅影响其调用站点,“volatile”在每次使用变量时都会生成一些额外的代码,从而损害优化。

  3. “volatile”实现可能在某些编译器和架构中存在错误/不一致。RB_GC_GUARD 可以针对损坏的系统/编译器进行定制,而不会对其他系统产生负面影响。

附录 F. Ractor 支持

Ractor 是 Ruby 3.0 中引入的并行执行机制。所有 ractor 都可以在不同的 OS 线程上并行运行(使用底层系统提供的线程),因此 C 扩展应该是线程安全的。可以在多个 ractor 中运行的 C 扩展称为“Ractor 安全”。

Ractor围绕 C 扩展的安全性具有以下属性

  1. 默认情况下,所有 C 扩展都被识别为 Ractor 不安全。

  2. Ractor 不安全的 C 方法只能从主 Ractor 调用。如果由非主 Ractor 调用,则会引发 Ractor::UnsafeError

  3. 如果扩展希望被标记为 Ractor 安全,则扩展应在扩展的 Init_ 函数中调用 rb_ext_ractor_safe(true),并且所有定义的方法将被标记为 Ractor 安全。

要制作“Ractor 安全”的 C 扩展,我们需要检查以下几点

(1) 不要在 ractor 之间共享不可共享的对象

例如,C 的全局变量可能导致在 ractor 之间共享不可共享的对象。

VALUE g_var;
VALUE set(VALUE self, VALUE v){ return g_var = v; }
VALUE get(VALUE self){ return g_var; }

set() 和 get() 对可以使用 g_var 共享不可共享的对象,并且它是不安全的 Ractor。

不仅直接使用全局变量,一些间接数据结构(如全局 st_table)可以共享对象,所以请小心。

请注意,类和模块对象是可共享对象,因此您可以使用 C 的全局变量保留代码“cFoo = rb_define_class(…)”。

(2) 检查扩展的线程安全性

扩展应该是线程安全的。例如,以下代码不是线程安全的

bool g_called = false;
VALUE call(VALUE self) {
  if (g_called) rb_raise("recursive call is not allowed.");
  g_called = true;
  VALUE ret = do_something();
  g_called = false;
  return ret;
}

因为 g_called 全局变量应该由其他 ractor 的线程同步。为了避免这种数据竞争,应该使用一些同步。检查 include/ruby/thread_native.h 和 include/ruby/atomic.h。

使用 Ractors,作为方法参数和接收器 (self) 给出的所有对象都保证来自当前 Ractor 或可共享。因此,使代码 ractor 安全比使代码通常线程安全更容易。例如,我们不需要锁定数组对象来访问它的元素。

(3) 检查任何所用库的线程安全性

如果扩展依赖于外部库,例如库 libfoo 中的函数 foo(),则函数 libfoo foo() 应该是线程安全的。

(4) 使对象可共享

这不一定需要使扩展 Ractor 安全。

如果扩展提供了由 rb_data_type_t 定义的特殊对象,请考虑这些对象是否可以成为可共享的。

RUBY_TYPED_FROZEN_SHAREABLE 标志表示如果对象被冻结,这些对象可以是可共享对象。这意味着如果对象被冻结,则不允许修改包装数据。

(5) 其他

在制作 Ractor 安全扩展时,可能还有其他必须考虑的要点或要求。在发现这些要点或要求时,本文档将予以扩展。