为 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 的值替换为“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

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

其他指针值

强制转换为 VALUE。

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

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

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

INT2FIX()

用于 31 位范围内的整数。

INT2NUM()

用于任意大小的整数。

如果整数超出 FIXNUM 范围,INT2NUM() 会将其转换为 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 字符串 format 和后续参数追加到 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)

常量定义

我们有 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)

如果参数不是 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)

此函数定义由两个环境共享的变量。可以通过名为“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 填充保留的字段。

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

您可以将“data”填充为任意值以供您使用。Ruby 不会对此成员执行任何操作。

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

RUBY_TYPED_FREE_IMMEDIATELY

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

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

RUBY_TYPED_WB_PROTECTED

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

但是,当设置它时,您有责任在对象的所有方法实现中适当地放置写屏障。否则,Ruby 在运行时可能会崩溃。

有关写屏障的更多信息,请参见分代 GC

RUBY_TYPED_FROZEN_SHAREABLE

此标志指示如果对象被冻结,则该对象是可共享的对象。有关更多详细信息,请参见Ractor 支持

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

请注意,此宏可能会引发异常。如果 sval 需要释放资源(例如,分配的内存,来自外部库的句柄等),则必须使用 rb_protect。

您可以一步分配和包装结构,这种方式更可取。

TypedData_Make_Struct(klass, type, data_type, sval)

此宏返回一个分配的 T_DATA 对象,该对象包装了指向结构的指针,该指针也被分配。此宏的工作方式类似于

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

但是,如果它只是简单分配的,则应该使用此宏而不是像上面代码一样的“分配然后包装”,因为后者可能会引发 NoMemoryError,并且在这种情况下 sval 将会发生内存泄漏。

参数 klass 和 data_type 的工作方式与 TypedData_Wrap_Struct() 中的对应参数相同。指向分配的结构的指针将分配给 sval,它应该是指针指定的类型。

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

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

这样做可以使 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 将 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(可选)

如果存在名为 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 扩展。除了以 rbimpl_RBIMPL_ 开头的符号外,所有符号都是公共 API。它们是实现细节,不应被 C 扩展使用。

仅允许 C 扩展包含 $repo_root/include/ruby/*.h,其对应的宏定义在 $repo_root/include/ruby.h 标头中。

$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

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

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 对象(默认真值)

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 <-> integer

FIX2LONG(value), LONG2FIX(l)

Fixnum <-> long

NUM2INT(value), INT2NUM(i)

Numeric <-> integer

NUM2UINT(value), UINT2NUM(ui)

Numeric <-> unsigned integer

NUM2LONG(value), LONG2NUM(l)

Numeric <-> long

NUM2ULONG(value), ULONG2NUM(ul)

Numeric <-> unsigned long

NUM2LL(value), LL2NUM(ll)

Numeric <-> long long

NUM2ULL(value), ULL2NUM(ull)

Numeric <-> unsigned long long

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 的 Object -> String

StringValuePtr(value)

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

StringValueCStr(value)

具有 #to_str 的 Object -> 指向没有 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 (如果 optional 为负数,则为 - optional - 1)个 ID 是可选的。如果 keyword_hash 中不包含强制性键,则会引发 “missing keyword” ArgumentError。如果 keyword_hash 中不存在可选键,则 values 中的相应元素设置为 Qundef。如果 optional 为负数,则忽略 keyword_hash 的其余部分,否则会引发 “unknown keyword” 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 指定,带有 argv 中的 argc 个参数,并将 func 作为块提供。当 func 作为块调用时,它将接收来自 yield 的值作为第一个参数,data2 作为第二个参数。当使用多个值(在 C 中,rb_yield_values()、rb_yield_values2() 和 rb_yield_splat())yield 时,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 作为单个参数 yield 到块。

VALUE rb_yield_values(int n, …)

n 个参数 yield 到块,每个 Ruby 参数使用一个 C 参数。

VALUE rb_yield_values2(int n, VALUE *argv)

n 个参数 yield 到块,所有 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 为 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(全局虚拟机锁),它阻止 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 *)

如果给定的 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(),它从 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。

与这些宏对应的头文件可以直接从扩展库中包含。

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(主要)保持兼容性。

一般来说,在分代 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 来确保在第二次调用 rb_str_new_cstr 时,sptr 的内容保持有效。

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 的最后一次使用之后。 将 RB_GC_GUARD 放在解引用 sptr 之前将毫无用处。 RB_GC_GUARD 仅对 VALUE 数据类型有效,而不是转换后的 C 数据类型。

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

rb_str_modify(s);

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

在 C 中,使用 RB_GC_GUARD 宏比使用“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 安全”。

关于 C 扩展的 Ractor 安全性具有以下属性

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

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

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

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

  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)也可以共享对象,因此请注意。

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

  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 安全扩展时,可能还有其他要点或要求需要考虑。本文档将在发现后进行扩展。