我正在寻找为什么在 ruby 中扩展基类不是一个好主意的例子。我需要向一些人展示为什么它是一种需要小心使用的武器。
有什么恐怖故事可以分享吗?
大约 2.5 年前,Rubinius发生了一个非常著名的猴子修补程序出错的例子。
这个案例的有趣之处在于,违规代码和受害者都非常明显且非常不寻常。通常,冒犯者是某个 PHP 脚本小子编写的一段代码,他们在他的 1337 元编程 h4X0r Skillz 上喝醉了。而失败模式是一个简单的ArgumentError例外,因为原始方法和monkeypatch 有不同的arity。
然而,在这种情况下,攻击者是 stdlib ( mathn) 中的一个库,故障模式是 Rubinius VM 完全崩溃。
所以发生了什么事?好吧,mathnmonkey 修补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。
一个明显的缺陷是名称冲突——如果两个或多个包为行为不同的方法选择相同的名称。