Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finalizers usage #1585

Closed
wenq1 opened this issue Jun 24, 2017 · 5 comments
Closed

Finalizers usage #1585

wenq1 opened this issue Jun 24, 2017 · 5 comments

Comments

@wenq1
Copy link

wenq1 commented Jun 24, 2017

After reading the guide and the issues related to the finalizer I'm still quite confused with the usage of finalizers.

Example code is illustrated below.

Inside object constructor, I set finalizer to the object. It is a game engine I'm building so that inside each frame, many TestObj are created and many nativebuf (of size 1000) are attached as "userdata" property. However, only until real heap_destruct will these objects (and native bindings) gets destroyed. This leads to out-of-memory very soon as heap is not destroyed until the game exited.

I'm really confused as whether I should really be doing this or should I just free nativebuf regardless of the value of heap_destruct? The guide says that finalizers may "rescue" the objects - meaning that I shouldn't be freeing nativebuf unconditionally. Is this true?

duk_ret_t dukjs_TestObject_finalize (duk_context * ctx)
{
        // arg0: this
        duk_get_prop_string (ctx, 0, "userdata"); // [this.userdata]
        float     * nativebuf    = duk_get_pointer (ctx, -1);
        duk_pop (ctx);      // []

        duk_bool_t      heap_destruct   = duk_require_boolean (ctx, 1);

        // should I free disregarding or respecting heap_destruct?
        if (heap_destruct) {
                free (nativebuf);
        }
}

duk_ret_t dukjs_TestObj_constructor (duk_context * ctx)
{
        if (!duk_is_constructor_call (ctx)) {
                return DUK_RET_TYPE_ERROR;
        }

        float     * nativebuf        = malloc (1000);

        duk_push_this (ctx);      // [this]
        duk_push_pointer (ctx, nativebuf);       // [this, nativebuf]
        duk_put_prop_string (ctx, -2, "userdata");   // [this]

        duk_push_c_function (ctx, dukjs_TestObject_finalize, 2);  // [this, fin]
        duk_set_finalizer (ctx, -2);    // [this]

        duk_pop (ctx);      // []

        return 0;
}


@wenq1
Copy link
Author

wenq1 commented Jun 24, 2017

Also according to #473, finalizer are only run once. If this is the case, what's the point of adding the second argument heap_destruct?

@svaarala
Copy link
Owner

svaarala commented Jun 24, 2017

Lots of questions, I'll try to cover them below.

Finalizers execute when Duktape has determined that Duktape itself has no knowledge of a reference to the object. This may happen (almost) immediately if the object's reference count drops to zero, or with a significant delay if the object is in a reference loop and is reclaimed by mark-and-sweep.

A finalizer can "rescue" an object by creating a reference visible to Duktape in the finalizer. When this is done, the finalizer will be executed again when the object becomes unreachable. However, if a finalizer would always rescue an object, even during heap destruction, the heap couldn't be destroyed because we'd just be running finalizers over and over again. So, there's a mechanism in heap destruction where Duktape eventually gives up and indicates heapDestruct=true, meaning that even if the finalizer rescues the object, the finalizer won't be called again.

If your use case doesn't require object rescuing, you don't need to worry about any of that: you don't need to check heapDestruct. Just release any native resources and don't create a live reference (for Duktape) for the object being finalized. The best practice is still to make the finalizer robust re: native resources, e.g.:

if (nativebuf) {
    free(nativebuf);
    /* set userdata to NULL to prevent double free, just in case */
}

Note that "rescuing" is up to you - it doesn't happen by accident.

@svaarala
Copy link
Owner

Assuming TestObjs don't participate in reference loops, they should get their finalizers called (almost) immediately when they become unreachable. If that doesn't happen, there are two possibilities:

  • They don't actually become unreachable from Duktape's point of view.
  • They do participate in reference loops, and are only collected by mark-and-sweep.

If the latter is true, calling duk_gc() would run the finalizers. Also voluntary periodic mark-and-sweep or mark-and-sweep triggered by out of memory will run the finalizers.

@wenq1
Copy link
Author

wenq1 commented Jun 26, 2017

That clears things out - I think it is the term "rescue" that confused me. At first I thought it is "automatically done" by the finalizer which employs some magical algorithm to determine that some objects is not ready for a death sentence.

Maybe the wording in the doc:

If false (normal case), the finalizer may rescue the object by creating a live reference to the object before returning and the finalizer is guaranteed to be called again later (heap destruction at the latest).

can be changed to

If false (normal case), user may explicitly rescue the object by creating a live reference to the object before returning and the finalizer is guaranteed to be called again later (heap destruction at the latest).

@svaarala
Copy link
Owner

I see what you mean, the "finalizer" here means the user finalizer code but it's indeed a bit ambiguous. I'll change the wording to be a bit more explicit.

@wenq1 wenq1 closed this as completed Jun 27, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants