6

在为 Ruby 编写 C++ 扩展时,我一直在努力解决的一个问题是,即使用户做了一些愚蠢的事情,也让它变得非常安全。那么他应该得到异常,但绝不会出现SegFault。一个具体的问题如下:我的 C++ 类有一个重要的构造函数。然后我使用 Rice API 来包装我的 C++ 类。如果用户在他的 Ruby 代码中重新定义了 initialize(),那么由 Rice 创建的 initialize() 函数将被覆盖,并且对象既不会被分配也不会被初始化。一个玩具示例可能如下:

class Person {
public:
  Person(const string& name): m_name (name) {}
  const string& name() const { return m_name; }
private:
  string m_name;
}

然后我像这样创建 Ruby 类:

define_class<Person>("Person")
  .define_constructor(Constructor<Person, const string&>(), Arg("name"))
  .define_method("name", &Person::name);

然后以下 Ruby 代码会导致 Segfault

require 'MyExtension'
class Person
  def initialize
  end
end
p = Person.new
puts p.name

有两种可能性我会很高兴:禁止以某种方式覆盖 Ruby 中的初始化函数或检查 C++,如果对象已正确分配,否则抛出异常。

我曾经直接使用过 Ruby C API,然后就很简单了。我刚刚分配了一个由空指针和在 allocate() 函数中设置为 false 的标志组成的虚拟对象,在初始化方法中,我分配了真实对象并将标志设置为 true。在每个方法中,我检查了那个标志并引发了一个异常,如果它是假的。然而,我用 Ruby C API 写了很多愚蠢的重复代码,我首先必须包装我的 C++ 类,以便它们可以从 C 访问,然后包装和解包 Ruby 类型等,另外我必须检查这个愚蠢的标志每一种方法,所以我迁移到了赖斯,这真的很好,我很高兴。

然而,在 Rice 中,程序员只能提供一个构造函数,该构造函数在 rice 创建的 initialize() 函数中调用,而 allocate() 函数是预定义的,什么也不做。我认为没有一种简单的方法可以改变这一点或以“官方”方式提供自己的分配功能。当然,我仍然可以使用 C API 来定义 allocate 函数,所以我尝试以某种方式将 C API 和 Rice 混合,但后来我变得非常讨厌,我得到了奇怪的 SegFaults 并且真的很丑,所以我放弃了这个想法.

这里有没有人有过大米的经验,或者有没有人知道如何保证这个安全?

4

4 回答 4

4

这个怎么样

class Person
  def initialize
    puts "old"
  end
  alias_method :original_initialize, :initialize

  def self.method_added(n)
    if n == :initialize && !@adding_initialize_method
      method_name = "new_initialize_#{Time.now.to_i}"
      alias_method method_name, :initialize
      begin
        @adding_initialize_method = true
        define_method :initialize do |*args|
          original_initialize(*args)
          send method_name, *args
        end
      ensure
        @adding_initialize_method = false
      end
    end
  end
end

class Person
  def initialize
    puts "new"
  end
end

然后调用Person.new输出

old
new

即我们的旧初始化方法仍然被调用

这使用了method_added每当添加(或重新定义)方法时调用的钩子,此时新方法已经存在,因此阻止它们执行此操作为时已晚。相反,我们为新定义的initialize方法取别名(您可能需要更加努力地确保方法名称是唯一的)并定义另一个初始化,它先调用旧的初始化方法,然后再调用新的方法。

如果这个人是明智的并且super从他们的初始化中调用,那么这将导致您的原始初始化方法被调用两次 - 您可能需要防止这种情况

您可以只抛出一个异常method_added来警告用户他们正在做坏事,但这并不能阻止添加该方法:该类现在处于不稳定状态。您当然可以在他们的方法之上实现您的原始初始化方法。

于 2012-10-03T09:07:30.187 回答
2

在您的评论中,您说在 c++ 代码中,this是一个空指针。如果可以从 ruby​​ 以这种方式调用 c++ 类,恐怕没有真正的解决方案。C++ 的设计初衷并非万无一失。基本上这发生在 c++ 中;

Person * p = 0;
p->name();

一个好的 c++ 编译器会阻止你这样做,但你总是可以以编译器无法检测到正在发生的事情的方式重写它。这会导致未定义的行为,程序可以做任何事情,包括崩溃。

当然,您可以在每个非静态函数中检查这一点;

const string& Person::name() const 
{ 
    if (!this) throw "object not allocated";
    return m_name; 
}

为了使其更容易并避免重复代码,请创建一个#define;

#define CHECK if (!this) { throw "object not allocated"; }

const string& name() const { CHECK; return m_name; }
int age() const { CHECK; return m_age; }

然而,最好避免在 ruby​​ 中用户可以重新定义初始化。

于 2012-10-03T08:37:00.047 回答
1

这是一个有趣的问题,不是 Rice 特有的,而是任何将分配和初始化分离为单独方法的扩展。我没有看到明显的解决方案。

在 Ruby 1.6 天,我们没有分配/初始化;我们有新的/初始化的。Rice 中可能仍然存在定义 MyClass.new 而不是 MyClass.allocate 和 MyClass#initialize 的代码。Ruby 1.8 将分配和初始化分离为单独的方法(http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/23358),Rice使用新的“分配框架”。但它有你指出的问题。

allocate 方法无法构造对象,因为它没有要传递给构造函数的参数。

Rice 可以定义 .new(就像在 1.6 上所做的那样),但这不适用于#dup 和 Marshal.load。然而,这可能是更安全(和正确)的解决方案。

于 2013-05-23T17:39:19.277 回答
0

我现在认为这是赖斯图书馆的问题。如果您按照记录的方式使用 Rice,您会遇到这些问题,并且没有明显的方法可以解决它,并且所有解决方法都有缺点并且很糟糕。所以我想解决方案是分叉 Rice 并修复这个问题,因为他们似乎忽略了错误报告。

于 2012-10-05T15:53:40.147 回答