-
-
Notifications
You must be signed in to change notification settings - Fork 13
Add Isolate.SetPromiseRejectedCallback
#108
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
base: master
Are you sure you want to change the base?
Conversation
07d3c41
to
016650f
Compare
There's apparently some confusing naming as "resolve" means the promise is successful (this is what I always thought of "resolve" means). It's also called "fulfilled". But "resolved" means it has reached it's final state, also called settled. So a "rejected" promise is also "resolved" - which I found confusing. I only ever used the term "settled" for the latter, so I had a bit of trouble understanding the enum values (lacking documentation in the V8 sources) |
016650f
to
34b6a53
Compare
// rejected. This includes rejections that may occur after a script value has | ||
// been evaluated and V8 is running microtasks. | ||
func (i *Isolate) SetPromiseRejectedCallback(cb RejectedPromiseCallback) { | ||
handle := unsafe.Pointer(uintptr(i.addHandle(cgo.NewHandle(cb)))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there was already a callback, this should deallocate the handle used by that callback. (which means I'm skeptical of the generic handles
implementation.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch - didn't think about this as I'd only set it up once.
Can you elaborate on the skeptical part?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a side note, The library has (at least) two Go error
implementations. JSError
and Exception
.
*Exception
represents a JavaScript Error
object (at least "native errors" - don't know if class MyErr extends Error{}
counts)
JSError
is generated by v8go when a script invocation fails - but it doesn't include the thrown value.
In this particular case, the value is the *Exception
value - so I can retrieve all the properties on the actual error object.
JSError
does have some useful properties, e.g. for JavaScript code like throw "I'm a string but should have been an error object"
- as you now have the stack trace - but the original error is missing, so if the error is returned back to JS - the original error value is lost. E.g., a function callback that itself calls back into JS - which fails with an error.
I might take a look at that eventually - but it's not a priority for my use cast ATM - just wanted to mention this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you elaborate on the skeptical part?
I think SetPromiseRejectedCallback
should "own" its handle, which suggests the handle should be stored in a promiseRejectedCallback
field, and not be mixed in with whatever else might end up in .handles
. Right now, it's a philosophical question, since there's only one user of that field.
On a side note, The library has (at least) two Go error implementations. JSError and Exception.
I added Exception
(rogchap#195) because there was no way to for functions to signal failure to Go code. It's one of the reasons for this fork. That created a discussion in rogchap#274, which stalled. Merging them would indeed be nice, and it sounds like I created a new type to avoid modifying too much of the code base without owner blessing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think SetPromiseRejectedCallback should "own" its handle
Ah, I think you're right, but maybe the promiseRejectedCallback
shouldn't even have a handle. As I assume that only one callback can be set, it could just be a field on the Go Isolate? And then optionally a handle to the isolate?
type Isolate struct {
selfHandle cgo.Handle
promiseRejectedCallback RejectedPromiseCallback
}
//export goCallback
func goCallback(C.uintptr_t handle, /* ... reset */) {
iso, ok := cgo.Handle(handle).Value().(*Isolate)
iso.promiseRejectedCalback( /* ... */ );
}
func (i *Isolate) SetPromiseRejectedCallback(cb RejectedPromiseCallback) {
if i.selfHandle == 0 {
i.selfHandle = cgo.NewHandle(i)
}
// This could potentially be moved _inside_ the if statement - as we just set the
// same callback with the same handle. But could be dangerous if the handle is
// used for different purposes.
C.IsolateSetPromiseRejectedCallback(C.uintptrt(i.selfHandle), /* ... */ )
...
}
func (i *Isolate) Dispose() {
if i.selfHandle != 0 {
i.selfHandle.Delete()
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added Exception
Nice 💪 I'm so glad you did :) I also experimented with Goja to have a pure Go alternative - and it uses panic
s to return with errors, e.g., you need to panic(NewTypeError(...))
or something like that - but it also looses track of the error value. It feels very wrong.
The stalled discussion was titled, "Store error object in JSError for propagation". I would probably have suggested just to add a *Value
field to JSError
- as it's not only objects you can throw, e.g. throw 1
is valid, but poor JS. You might even make it an anonymous/embedded field, so all Value
methods are promoted. I.e., it's just a value, but has additional information about error site.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A side note on the side note. Exception
values don't work with AsObject()
, as it checks IsObject()
- and that returns false for functions, exceptions, and module namespace objects.
In my ESM experimental branch, I have
func (v *Value) AsObject() (*Object, error) {
if !v.IsObject() && !v.IsModuleNamespaceObject() && !v.IsFunction() && !v.IsNativeError() {
return nil, errors.New("v8go: value is not an Object")
}
return &Object{v}, nil
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, just checked the readme - I see you're also to thank for Symbol support 🙌 without which I wouldn't have proper iterables in my browser
} | ||
|
||
//export goRejectedPromiseCallback | ||
func goRejectedPromiseCallback(ctxref int, handle unsafe.Pointer, event PromiseRejectEvent, promise C.ValuePtr, value C.ValuePtr) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename handle
to cb
to link it to SetPromiseRejectedCallback(cb)
.
delete ctx; | ||
} | ||
|
||
m_value* track_value(m_ctx* ctx, Local<Value> value) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea to make a helper.
Shouldn't this be an m_value
constructor (without tracked_value()
)? Seems RAII-worthy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have had similar ideas, but something about cleanup I didn't think regarding freeing up of resources.
The Context
has a list of values to free, but that's an unordered_map<long, m_value*>
- which is released in
void ContextFree(ContextPtr ctx) {
// ...
for (auto it = ctx->vals.begin(); it != ctx->vals.end(); ++it) {
auto value = it->second;
value->ptr.Reset();
delete value;
}
// ...
delete ctx;
}
If this had been unordered_map<long, m_value>
- that iteration shouldn't be necessary at all AFAICT - as that happens automatically in delete ctx
Btw, I think that there's some mechanism in Go - which is also used some places in the code to cleanup on GC - but not all places. So a lot of temporary Value
s are never freed until you delete the context. It's not a big problem for my intended use case as contexts are short-lived - but it's something I've wanted to look into.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On a related note - in a different branch, I added helper functions to the struct, also to simplify commonly occurring patterns.
I'm hoping I'm not making a huge mistake - that a value could suddenly be used in a different isolate than the one that created it? If so, the iso should be an argument - and probably not present as a field in the struct.
struct m_value {
long id;
v8::Isolate* iso;
m_ctx* ctx;
v8::Global<v8::Value> ptr;
v8::Local<v8::Value> ToLocal() { return this->ptr.Get(this->iso); }
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And, what's with the m_
prefix? Is it a C/C++ convention?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm hoping I'm not making a huge mistake - that a value could suddenly be used in a different isolate than the one that created it? If so, the iso should be an argument - and probably not present as a field in the struct.
While Value itself doesn't seem tied to an Isolate, all concrete constructors (well New()
) seem to take an Isolate
. So I think we're good with that assumption.
m_
is used extensively in C++ standard libraries for member data, so it does feel weird to me too to have it on type names. And I can't find that it would be a libv8 thing either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes - remember the m_
prefix from ATL I think (the MS lib to build COM objects). I adopted when I first started coding .NET 1.0 😆
// rejected. This includes rejections that may occur after a script value has | ||
// been evaluated and V8 is running microtasks. | ||
func (i *Isolate) SetPromiseRejectedCallback(cb RejectedPromiseCallback) { | ||
handle := unsafe.Pointer(uintptr(i.addHandle(cgo.NewHandle(cb)))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
go vet is complaining that this is possible misuse of unsafe.Pointer
, probably because of the uintptr
cast.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah - honestly, I am totally confused about the relation between C's uintptr_t
and void*
- maybe you know? (I have a strong suspicion your C/C++ knowledge far exceeds mine)
As far as I could tell, conversion between the two should be safe, and for passing cgo.Handle
values in the ExternalValue
branch, I passed the uintptr_t
, and converts to/from void*
in C++.
Any thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uintptr_t is an unsigned integer type as least as large as the platform's pointers. There's also intptr_t and ptrdiff_t for relative pointers. I.e. very similar in scope of size_t and ssize_t...
- https://rgambord.github.io/c99-doc/sections/7/18/1/4/index.html
- https://rgambord.github.io/c99-doc/sections/7/17/index.html
Seems you shouldn't use unsafe.Pointer
with Handle
, and instead do the conversion to pointer on the C-side: https://pkg.go.dev/runtime/[email protected]#Handle
1bf1023
to
b86924b
Compare
I'd love to complete this, I'm a little in doubt what would be best - what data to store as "data" for the handler. The Go callback, or just a handle to the Isolate? I was a little uncertain what your suggestion was - but I sketched an alternate solution, where a handle to the isolate is used, rather than a handle to the callback - as the function signature strongly indicates you can have only one callback registered at a time. (One thing I didn't check is if you can set it to type Isolate struct {
selfHandle cgo.Handle
promiseRejectedCallback RejectedPromiseCallback
}
//export goCallback
func goCallback(C.uintptr_t handle, /* ... reset */) {
iso, ok := cgo.Handle(handle).Value().(*Isolate)
iso.promiseRejectedCalback( /* ... */ );
}
func (i *Isolate) SetPromiseRejectedCallback(cb RejectedPromiseCallback) {
if i.selfHandle == 0 {
i.selfHandle = cgo.NewHandle(i)
}
// This could potentially be moved _inside_ the if statement - as we just set the
// same callback with the same handle. But could be dangerous if the handle is
// used for different purposes.
C.IsolateSetPromiseRejectedCallback(C.uintptrt(i.selfHandle), /* ... */ )
...
}
func (i *Isolate) Dispose() {
if i.selfHandle != 0 {
i.selfHandle.Delete()
}
}```
You mentioned renaming `handle` to `cb` - but that does depend on what to actually store.
But let me know if you have other suggestions for an implementation.
|
Allows the embedder to be notified when
In the process I added a small helper, as these 7 lines appear everywhere
Btw, On the Go side,
Value
has actx *Context
- I don't think it's used - will check if it can be removed.