为 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 位。T_FIXNUM 可以使用 FIX2INT() 或 FIX2LONG() 宏转换为 C 整数。虽然您必须在使用它们之前检查数据是否确实是 FIXNUM,但它们更快。FIX2LONG() 永远不会引发异常,但 FIX2INT() 会引发 RangeError,如果结果大于或小于 int 的大小。还有 NUM2INT() 和 NUM2LONG(),它们将任何 Ruby 数字转换为 C 整数。这些宏包含类型检查,因此如果转换失败,将引发异常。NUM2DBL() 可以用来以相同的方式获取双精度浮点数的值。

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

您还可以使用名为 StringValueCStr() 的宏。这与 StringValuePtr() 相同,但始终在结果末尾添加一个 NUL 字符。如果结果包含 NUL 字符,此宏将导致 ArgumentError 异常。StringValuePtr() 不保证结果末尾存在 NUL,结果可能包含 NUL。

其他数据类型有相应的 C 结构,例如 T_ARRAY 的 struct RArray 等。具有相应结构的类型的 VALUE 可以被强制转换为检索指向该结构的指针。强制转换宏将采用 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

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

其他指针值

强制转换为 VALUE。

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

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

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

INT2FIX()

用于 31 位内的整数。

INT2NUM()

用于任意大小的整数。

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

操作 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)

要定义和取消定义“allocate”类方法,请使用

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)

常量定义

我们有两个函数用于定义常量

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)

如果参数不是 Symbol 也不不是 String,这些函数会尝试将其转换为 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, ...)

此函数在 recv 上调用一个方法,方法名称由符号 mid 指定。

访问变量和常量

您可以使用访问函数访问类变量和实例变量。此外,全局变量可以在两个环境之间共享。无法访问 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 节所述,以下 Ruby 常量可以从 C 中引用。

Qtrue
Qfalse

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

Qnil

C 范围内的 Ruby nil。

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

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

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

此函数定义了由两个环境共享的变量。可以通过 Ruby 的全局变量“name”访问指向“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() 进行更新。

您必须将 reserved 填充为 0。

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

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

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

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,sval 应该是指定类型的指针。

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

在您的结构引用简单值(而不是包装在条件逻辑或复杂数据结构中)的 Ruby 对象的情况下,提供了一种替代的标记和引用更新方法,方法是声明对结构中 VALUES 的偏移引用。

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

您必须定义一个指向结构中引用所在偏移量的静态 VALUE 指针列表,并将“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 在 C 环境中包装 dbm 结构。

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(可选)

如果名为 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> 包含。所有符号都是公共 API,除了以 rbimpl_RBIMPL_ 为前缀的符号。它们是实现细节,不应被 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

从 keywords 自动生成

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

Bignum

compar.c

Comparable

complex.c

Complex

cont.c

Fiber, Continuation

dir.c

Dir

enum.c

Enumerable

enumerator.c

Enumerator

file.c

File

hash.c

Hash

io.c

IO

marshal.c

Marshal

math.c

Math

numeric.c

Numeric, Integer, Fixnum, Float

pack.c

Array#pack, String#unpack

proc.c

Binding, Proc

process.c

Process

random.c

随机数

range.c

Range

rational.c

Rational

re.c

Regexp, MatchData

signal.c

Signal

sprintf.c

String#sprintf

string.c

String

struct.c

Struct

time.c

Time

defs/known_errors.def

Errno::* 异常类

-> known_errors.inc

自动生成

多语言化

encoding.c

Encoding

transcode.c

编码::转换器

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)

valueInteger 吗?

RB_FLOAT_TYPE_P(value)

valueFloat 吗?

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 -> 双精度浮点数

rb_float_new(f)

双精度浮点数 -> Float

RSTRING_LEN(str)

String -> String 数据的字节长度

RSTRING_PTR(str)

String -> 指向 String 数据的指针 注意结果指针可能不是 NUL 结尾的

StringValue(value)

Object with #to_str -> String

StringValuePtr(value)

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

StringValueCStr(value)

Object with #to_str -> 指向 String 数据的指针,不包含 NUL 字节 保证结果数据以 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 之间共享。如果名称包含不允许作为符号一部分的字符,则 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(- optional - 1 如果 optional 为负)个 ID 是可选的。如果 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)

在 recv 上调用一个方法,方法名称由符号 mid 指定,参数数量为 argc,参数列表为 argv,并将 func 作为 block 提供。当 func 作为 block 被调用时,它将接收来自 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 作为 block 提供。func1 将使用参数 arg1 被调用。func2 接收来自 yield 的值作为第一个参数,arg2 作为第二个参数。

当 rb_iterate 在 1.9 中使用时,func1 必须调用一些 Ruby 级别的函数。此函数自 1.9 以来已过时;请使用 rb_block_call 代替。

VALUE rb_yield(VALUE val)

将 val 作为单个参数 yield 给 block。

VALUE rb_yield_values(int n, …)

n 个参数 yield 给 block,每个 Ruby 参数使用一个 C 参数。

VALUE rb_yield_values2(int n, VALUE *argv)

n 个参数 yield 给 block,所有 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)

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

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

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

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

调用函数 func,并将 arg 作为参数。如果在 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 为真,则根据类似 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, …)

立即终止解释器。此函数应在解释器错误导致的情况下调用。不会进行异常处理或确保执行。

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

线程

从 Ruby 1.9 开始,Ruby 支持原生 1:1 线程,每个 Ruby Thread 对象对应一个内核线程。目前,存在一个 GVL(全局虚拟机锁),它阻止了 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 变为可读,允许调度其他线程。如果可以执行读取操作,则返回真值;如果存在不可恢复的错误,则返回假值。

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 *)

如果给定的 FD 在 rb_fdset_t 中被设置,则返回 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(),该函数从 Regexp 对象创建 String 对象。这与 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.6(或更低版本)时在扩展中使用的示例宏,当使用 Ruby 2.7 中引入的 *_kw 函数时。

#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 中使用的函数

请参阅 MakeMakefile 的文档。

附录 D. 代际 GC

Ruby 2.1 引入了代际垃圾收集器(称为 RGenGC)。RGenGC(大部分)保持兼容性。

通常,在扩展库中使用称为写屏障的技术对于代际 GC (en.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

使用这些技术,您不再需要插入写入屏障。

插入写入屏障

[再次] 插入写屏障是一个非常困难的黑客行为,很容易引入严重的错误。插入写屏障还会带来一些开销。基本上我们不建议您插入写屏障。请仔细考虑风险。

在插入写屏障之前,您需要了解 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 都可以在不同的操作系统线程(使用底层系统提供的线程)上并行运行,因此 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。

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

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

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

(4) 使对象可共享

这对于使扩展 Ractor 安全不是必需的。

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

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

(5) 其他

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