如果仍在范围内,将全局 js 对象存储在 JS::Heap<T> 中会在 JS_DestroyRuntime 中引发崩溃

Storing the global js object in JS::Heap<T> provokes crash in JS_DestroyRuntime if still in scope

学习this question后, 我正在尝试将 SpiderMonkey 的全局对象存储在堆上。只要在调用 JS_DestroyRuntime 之前超出范围,这似乎就可以工作。然而,在我的代码中,全局对象是一个 class 成员,因此在运行时在 class 的析构函数中被销毁之前不能超出范围。

不幸的是,这导致 Monkey 在 JS::~Heap 中崩溃,堆栈跟踪如下:

1  js::gc::Cell::storeBuffer() const                                                    Heap.h       1339 0x10004f905 
2  JSObject::writeBarrierPost(void *, JSObject *, JSObject *)                           jsobj.h      655  0x1000a6fc8 
3  js::InternalGCMethods<JSObject *>::postBarrier(JSObject * *, JSObject *, JSObject *) Barrier.h    254  0x1000a6df0 
4  JS::HeapObjectPostBarrier(JSObject * *, JSObject *, JSObject *)                      Barrier.cpp  173  0x100bc1636 
5  js::GCMethods<JSObject *>::postBarrier(JSObject * *, JSObject *, JSObject *)         RootingAPI.h 551  0x100003065 
6  JS::Heap<JSObject *>::post(JSObject * const&, JSObject * const&)                     RootingAPI.h 271  0x10000302b 
7  JS::Heap<JSObject *>::~Heap()                                                        RootingAPI.h 237  0x10000369e 
8  JS::Heap<JSObject *>::~Heap()                                                        RootingAPI.h 236  0x100002f75 
9  MyMonkeyClass::~MyMonkeyClass()                                                      main.cpp     64   0x100003725 
10 MyMonkeyClass::~MyMonkeyClass()                                                      main.cpp     58   0x100002aa5 
11 main                                                                                 main.cpp     110  0x100002a12 
12 start                                                                                                  0x1000029d4 

这是一个触发问题的最小示例。我故意离开了 GC 跟踪调用,它们不会改变结果。

#include <js/Initialization.h>
#include <jsapi.h>
#include <QDebug>

// The class of the global object. Just a dummy.
static JSClass global_class = {
    "global",
    JSCLASS_GLOBAL_FLAGS,
    nullptr,
    nullptr,
    nullptr,
    nullptr,
    nullptr,
    nullptr,
    nullptr,
    nullptr,
    nullptr,
    nullptr,
    nullptr,
    JS_GlobalObjectTraceHook
};

class MyMonkeyClass
{
public:
    MyMonkeyClass() {
        Q_ASSERT( JS_Init() );
        auto runtime = JS_NewRuntime( 5 * 1024 * 1024 /*heap space*/ );
        Q_ASSERT( runtime );
        m_context = JS_NewContext( runtime, 8192 /*default. keep.*/ );
        Q_ASSERT( m_context );

        JSAutoRequest ar( m_context );

        // Not sure which of these alternatives is the correct way with JS::Heap
        m_global = JS::RootedObject( m_context,
                                   JS_NewGlobalObject( m_context, &global_class, nullptr, JS::FireOnNewGlobalHook ) );
        //global = JS_NewGlobalObject( context, &global_class, nullptr, JS::FireOnNewGlobalHook );
        Q_ASSERT( m_global.get() );
    }

    ~MyMonkeyClass() {
        auto runtime = JS_GetRuntime( m_context );
        JS_DestroyContext( m_context );
        JS_DestroyRuntime( runtime );
        JS_ShutDown();
    }

private:
    JSContext *m_context;
    JS::Heap<JSObject*> m_global;
};

int main( int, char** )
{
    MyMonkeyClass mmc;
    return 0;
}

在撰写这个问题时,我发现在销毁东西之前在 dtor 中设置 m_global = nullptr; 实际上可以避免崩溃。现在我的最后一个问题是:

这是正确的解决方法吗?如果是,为什么? SM 是否可能假设非空 JS::Heap 指针指向仍在使用的内存,因此它会出现恐慌?

您最初的问题是由错误的对象释放顺序引起的。

C++ class 以相反的构造顺序析构,即调用 class 析构函数后跟 class 成员析构函数。成员以其声明的相反顺序被销毁。这意味着在你的情况下:

  1. ~MyMonkeyClass() 进入
  2. JS_Destroy* 和 JS_ShutDown 被称为
  3. JS::~堆被调用。但是,JS 引擎已经消失,这会导致未定义的行为 - 在您的情况下会崩溃。

通过将 m_global 设置为 nullptr 可以防止 JS::~Heap() 下降到 JS API 来清理内存。这样可以避免崩溃,但可能会导致内存泄漏。

正确的解决方法是以正确的顺序清理所有内容。

一种方法是定义另一个 class "JsUpDown" 来初始化 JS 引擎,在构造函数中创建 JS 运行时和 JS 上下文并调用 JS_Destroy* 和 JS_ShutDown在析构函数中。然后 MyMonkeyClass 看起来像:

class MyMonkeyClass
{
  MyMonkeyClass() : m_engine(), m_global() // just to make it explicit
  {
     JSAutoRequest ar(m_engine.m_context);
     m_global = JS::RootedObject(...);
  }
  ~MyMonkeyClass() {} // no need for the destructor now

  JsUpDown m_engine;  // Should be first. The order is important:
                      // Members are destroyed in reverse order.
  JS::Heap<JSObject*> m_global;
};