我正在寻找为什么在 ruby 中扩展基类不是一个好主意的例子。我需要向一些人展示为什么它是一种需要小心使用的武器。
有什么恐怖故事可以分享吗?
大约 2.5 年前,Rubinius发生了一个非常著名的猴子修补程序出错的例子。
这个案例的有趣之处在于,违规代码和受害者都非常明显且非常不寻常。通常,冒犯者是某个 PHP 脚本小子编写的一段代码,他们在他的 1337 元编程 h4X0r Skillz 上喝醉了。而失败模式是一个简单的ArgumentError
例外,因为原始方法和monkeypatch 有不同的arity。
然而,在这种情况下,攻击者是 stdlib ( mathn
) 中的一个库,故障模式是 Rubinius VM 完全崩溃。
所以发生了什么事?好吧,mathn
monkey 修补Fixnum
类并改变Fixnum
算术的工作方式。特别是,它改变了几种核心方法的结果和类型。例如:
r = 4/3 # => 1
r.class # => Fixnum
require 'mathn'
r = 4/3 # => (4/3)
r.class # => Rational
问题当然是在 Rubinius 中,整个 Ruby 编译器、整个 Ruby 内核、大部分 Ruby 核心库、部分 Rubinius VM 和其他部分 Rubinius 基础设施,都是用 Ruby 编写的。当然,所有这些都使用Fixnum
算术。
该类Hash
是用 Ruby 编写的,它使用Fixnum
算术来计算散列桶的大小,计算散列函数等等。Array
是用 Ruby 编写的,需要计算元素大小和数组长度。FFI 库是用 Ruby 编写的,需要计算内存地址(!)和结构大小。Rubinius 的许多部分都假设他们可以进行一些Fixnum
算术运算,然后将结果作为指针或 传递给某个 C 函数int
。
而且由于 Ruby 不支持任何类型的选择器命名空间或类装箱或类似的(尽管 Ruby 2.0 计划使用类似的东西),只要一些随机用户代码需要该mathn
库,所有这些部分就会爆炸,因为所有突然之间,Fixnum
操作的结果不再是 a Fixnum
(它基本上与机器相同int
,可以这样传递),而是 a Rational
(它是一个成熟的 Ruby 对象)。
基本上,会发生什么,一些代码会require 'mathn'
(或者你将它输入到 IRb 中),然后虚拟机就会立即死掉。
在这种情况下,解决方案是编译器的安全数学插件:当编译器检测到它正在编译内核或 Rubinius 的其他核心部分时,它会自动将对Fixnum
方法的调用重写为对这些方法的私有不可变副本的调用。[注意:我认为在当前版本的 Rubinius 中,问题以不同的方式解决。]
失败的三重奏;或者,如何为 Ruby 1.8.7 修补 Rails 2.0有一个 Rails 示例(这是一个经过仔细审查的大型项目),由于他们通过猴子补丁String
添加方法而导致问题chars
。
一个明显的缺陷是名称冲突——如果两个或多个包为行为不同的方法选择相同的名称。