-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Add JSRT support for native callbacks with new.target
#4529
Add JSRT support for native callbacks with new.target
#4529
Conversation
bin/NativeTests/JsRTApiTest.cpp
Outdated
@@ -660,6 +660,215 @@ namespace JsRTApiTest | |||
JsRTApiTest::RunWithAttributes(JsRTApiTest::ExternalFunctionTest); | |||
} | |||
|
|||
JsValueRef CALLBACK ExternaEnhancedFunctionTestCallback(JsValueRef callee, JsValueRef *arguments, unsigned short argumentCount, JsNativeFunctionInfo *info, void *callbackData) |
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.
Typo: should be ExternalEnhancedFunctionTestCallback
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.
Thanks for that, good catch.
lib/Jsrt/ChakraCore.h
Outdated
{ | ||
JsValueRef thisArg; | ||
JsValueRef newTargetArg; | ||
bool isConstructCall; |
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.
Is it worth documenting how these work? e.g. if isConstructCall
is true, then newTargetArg
will be non-null, otherwise it will be null?
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 newTargetArg
cannot be non-null unless isConstructCall
is true but not sure if the example above is always true. Thinking about it, new.target
should really be undefined and not null, I believe.
lib/Jsrt/ChakraCore.h
Outdated
/// Requires an active script context. | ||
/// </remarks> | ||
/// <param name="nativeFunction">The method to call when the function is invoked.</param> | ||
/// <param name="metadata">If this is a string, it is used as the name of the function.</param> |
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.
What if this is not a string? If it must be a string (or undefined/not provided) perhaps name it functionName
instead of metadata
?
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.
For now it's acting as a stand-in for the name parameter from JsCreateNamedFunction
but the idea is to extend it so that it could be a Var
with properties like length
, name
, or constructor
.
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.
Should we enforce the shape to be an Object with a single name
property right now to make upgrading this easier in the future?
lib/Jsrt/Jsrt.cpp
Outdated
@@ -2791,44 +2791,65 @@ CHAKRA_API JsConstructObject(_In_ JsValueRef function, _In_reads_(cargs) JsValue | |||
}); | |||
} | |||
|
|||
CHAKRA_API JsCreateFunction(_In_ JsNativeFunction nativeFunction, _In_opt_ void *callbackState, _Out_ JsValueRef *function) | |||
#ifndef _CHAKRACOREBUILD | |||
struct JsNativeFunctionInfo |
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.
Having this duplicated between the header and here feels like it could cause issues in future (e.g. someone updates one spot but not the other). Since it seems that this struct is required in both core and full (since it is used internally there), is it worth moving the struct definition to chakra.h instead of charkacore.h?
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 took the conservative approach of not adding this struct or the declaration for JsEnhancedNativeFunction
in ChakraCommon.h because I don't want them to be considered part of the Windows API surface and have to go through the API review board, etc.
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.
Makes sense. I do wish there was a better way than having essentially the same structure duplicated in 3 places.
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.
Could it be in a separate header for the time being that is just included in each place? Its also a bit hacky but the pattern is used elsewhere in ChakraCore.
|
||
StdCallJavascriptMethodInfo info = { | ||
callInfo.Count > 0 ? args[0] : scriptContext->GetLibrary()->GetNull(), | ||
args.HasNewTarget() ? args.GetNewTarget() : args.IsNewCall() ? function : scriptContext->GetLibrary()->GetNull(), |
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.
As discussed in another comment; perhaps this should default to undefined
instead of null
? And is it worth asserting that args.HasNewTarget()
implies that args.IsNewCall()
?
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'll make the change to undefined rather than null - this is per spec even. I don't think it's worth adding that assert here. It might actually be possible to get new.target
without being in a new call. Don't think there are any ways for it to happen yet, but spec doesn't forbid that case.
Var result = nullptr; | ||
|
||
StdCallJavascriptMethodInfo info = { | ||
callInfo.Count > 0 ? args[0] : scriptContext->GetLibrary()->GetNull(), |
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 believe that we should always have a this
parameter, so args[0]
should always be passed; I'd prefer an assert that callInfo.count > 0
and just putting args[0]
here rather than null (which isn't possible in normal JS scenarios)
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 onboard with this.
@@ -424,9 +430,15 @@ namespace Js | |||
TTD::TTDNestingDepthAutoAdjuster logPopper(scriptContext->GetThreadContext()); | |||
TTD::NSLogEvents::EventLogEntry* callEvent = elog->RecordExternalCallEvent(externalFunction, scriptContext->GetThreadContext()->TTDRootNestingCount, args.Info.Count, args.Values, true); |
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.
Do these TTD events need to be updated with the new.target
information? Right now only the arguments are passed in.
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 we need to do something more here for TTD cases because RecordExternalCallEvent
doesn't even record the flags for the call at all right now. new.target
is located in the args, so not sure it should be handled specially but we should probably care if the function was called as a new expression or not.
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.
Since the TTD tests are passing again, this may not need to change. I think that since the call flags aren't directly visible in JS, when replaying we probably don't need to know the flags, the external function is a black-box that has some js-visible side effects and a return value, all of which are currently recorded.
@@ -1091,6 +1091,7 @@ namespace Js | |||
JavascriptExternalFunction* CreateExternalFunction(ExternalMethod entryPointer, Var nameId, Var signature, UINT64 flags, bool isLengthAvailable = false); | |||
JavascriptExternalFunction* CreateStdCallExternalFunction(StdCallJavascriptMethod entryPointer, PropertyId nameId, void *callbackState); | |||
JavascriptExternalFunction* CreateStdCallExternalFunction(StdCallJavascriptMethod entryPointer, Var name, void *callbackState); | |||
|
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.
Is this intentional?
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.
No, actually. Leftover from reverted changes, I guess.
lib/Jsrt/ChakraCore.h
Outdated
/// <summary> | ||
/// A structure containing information about a native function callback. | ||
/// </summary> | ||
struct JsNativeFunctionInfo |
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.
You should also typedef
this struct as JsNativeFunctionInfo
since clang won't automatically do so for you.
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, thanks.
lib/Jsrt/Jsrt.cpp
Outdated
PERFORM_JSRT_TTD_RECORD_ACTION_RESULT(scriptContext, function); | ||
struct JsNativeFunctionWrapperHolder | ||
{ | ||
void *callbackState; |
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.
There's also xplat compile errors claiming that these two values need a Field
barrier, since they are in recycler memory. I think they both actually be a FieldNoBarrier
since neither value is recycler allocated.
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.
@digitalinfinity I'd appreciate if you could double-check my thoughts here.
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.
Hmm I'll add FieldNoBarrier
. 👍
lib/Jsrt/Jsrt.cpp
Outdated
|
||
CHAKRA_API JsCreateFunction(_In_ JsNativeFunction nativeFunction, _In_opt_ void *callbackState, _Out_ JsValueRef *function) | ||
{ | ||
return JsCreateNamedFunction(JS_INVALID_REFERENCE, nativeFunction, callbackState, function); |
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.
JsCreateNamedFunction
states that the name
property is an _In_
parameter, but JS_INVALID_REFERENCE
is null so prefast is complaining here.
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.
Noticed and this is unfortunate. I'll change JsCreateFunction
to call JsCreateEnhancedFunctionHelper
directly since the metadata
argument is marked as _In_opt_
.
bin/NativeTests/JsRTApiTest.cpp
Outdated
JsValueRef name; | ||
REQUIRE(JsCreateString("BaseClass", 10, &name) == JsNoError); | ||
JsValueRef base = JS_INVALID_REFERENCE; | ||
REQUIRE(JsCreateEnhancedFunction(ExternaEnhancedBaseClassFunctionTestCallback, name, &info, &base) == JsNoError); |
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.
Prefast complains that info
is uninitialized here
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.
Thanks, have fixed this one.
lib/Jsrt/Jsrt.cpp
Outdated
PERFORM_JSRT_TTD_RECORD_ACTION(scriptContext, RecordJsRTAllocateFunction, true, name); | ||
|
||
VALIDATE_INCOMING_REFERENCE(name, scriptContext); | ||
PERFORM_JSRT_TTD_RECORD_ACTION(scriptContext, RecordJsRTAllocateFunction, false, nullptr); |
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.
You probably still need to record the metadata
variable if it is provided. There is a TTD test which is failing, but I'm not sure if it's for this reason.
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 good catch, thank you.
lib/Jsrt/ChakraCore.h
Outdated
/// Requires an active script context. | ||
/// </remarks> | ||
/// <param name="nativeFunction">The method to call when the function is invoked.</param> | ||
/// <param name="metadata">If this is a string, it is used as the name of the function.</param> |
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 this is a string [](start = 27, length = 19)
Consider rewording as if this is not null you are calling ToString, so any non-null is converted to a string.
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.
Thanks, I made a note here as such.
@@ -318,6 +320,7 @@ class ChakraRTInterface | |||
static JsErrorCode WINAPI JsCreateObject(JsValueRef *object) { return HOOK_JS_API(CreateObject(object)); } | |||
static JsErrorCode WINAPI JsCreateExternalObject(void *data, JsFinalizeCallback callback, JsValueRef *object) { return HOOK_JS_API(CreateExternalObject(data, callback, object)); } | |||
static JsErrorCode WINAPI JsCreateFunction(JsNativeFunction nativeFunction, void *callbackState, JsValueRef *function) { return HOOK_JS_API(CreateFunction(nativeFunction, callbackState, function)); } | |||
static JsErrorCode WINAPI JsCreateEnhancedFunction(JsEnhancedNativeFunction nativeFunction, JsValueRef metadata, void *callbackState, JsValueRef *function) { return HOOK_JS_API(CreateEnhancedFunction(nativeFunction, metadata, callbackState, function)); } |
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 don't know if this is a particularly productive bikeshed, but how set in stone are we on this name? I feel like we could instead call these ES6Functions rather than EnhancedFunctions? I fear of a situation like https://msdn.microsoft.com/en-us/library/dd317812(v=vs.85).aspx, where we might need to change this API again in the future and we are stuck with CreateFunction, CreateEnhancedFunction, and... CreateExtraEnhancedFunction? ES6Function might be confused with arrow functions, so maybe this doesn't have a good solution.
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.
Agree with the sentiment. We can not change JsCreateFunction
or JsNativeFunction
easily, though. I considered just naming the new one JsCreateFunction2
to avoid giving it a name and just acknowledge that it's newer. Don't think ES6Function means much, though. I had named it JsNativeFunctionWithInfo
but the function JsCreateFunctionWithInfo
sounds like you need to pass info in order to create the function. JsNativeFunctionWithNewTarget
is too specific. Maybe JsNativeSubclassableFunction
or JsSubclassableNativeFunction
but that may discourage use for ordinary function callbacks?
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.
My thinking for JsCreateES6Function was that the important difference here compared to JsCreateFunction was that you gained access to an ES6 feature, new.target. If the eventual goal is to get this into Windows, is there any value in actually following the naming scheme with JsCreateFunctionEx? The only concern is that a new spec will add a new feature and we will be in the same bad situation right now, except that we will have a new naming ambiguity.
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.
As long as it hasn't shipped in Windows, we're treating these API as experimental. So I suppose we can change the name until that point. Not that we should change it many times or anything, but the option is open.
lib/Jsrt/ChakraCore.h
Outdated
/// <summary> | ||
/// A structure containing information about a native function callback. | ||
/// </summary> | ||
struct JsNativeFunctionInfo |
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.
Is this struct one that we will be able to add fields to in the future, even if this API becomes part of Windows? (Along the same lines as my other comment about needing future updates to this API surface).
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.
We should be able to add more fields to the struct later if we want to. If it becomes part of Windows API surface, changing it will become very difficult. We'll have to go through Windows API review board.
lib/Jsrt/ChakraCore.h
Outdated
/// Requires an active script context. | ||
/// </remarks> | ||
/// <param name="nativeFunction">The method to call when the function is invoked.</param> | ||
/// <param name="metadata">If this is a string, it is used as the name of the function.</param> |
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.
Should we enforce the shape to be an Object with a single name
property right now to make upgrading this easier in the future?
lib/Jsrt/Jsrt.cpp
Outdated
@@ -2791,44 +2791,65 @@ CHAKRA_API JsConstructObject(_In_ JsValueRef function, _In_reads_(cargs) JsValue | |||
}); | |||
} | |||
|
|||
CHAKRA_API JsCreateFunction(_In_ JsNativeFunction nativeFunction, _In_opt_ void *callbackState, _Out_ JsValueRef *function) | |||
#ifndef _CHAKRACOREBUILD | |||
struct JsNativeFunctionInfo |
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.
Could it be in a separate header for the time being that is just included in each place? Its also a bit hacky but the pattern is used elsewhere in ChakraCore.
@@ -424,9 +430,15 @@ namespace Js | |||
TTD::TTDNestingDepthAutoAdjuster logPopper(scriptContext->GetThreadContext()); | |||
TTD::NSLogEvents::EventLogEntry* callEvent = elog->RecordExternalCallEvent(externalFunction, scriptContext->GetThreadContext()->TTDRootNestingCount, args.Info.Count, args.Values, true); | |||
|
|||
StdCallJavascriptMethodInfo info = { | |||
callInfo.Count > 0 ? args[0] : scriptContext->GetLibrary()->GetNull(), |
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.
Just a comment to note that the above conversation should also be updated here (re: null or undefined, callInfo.Count > 0)
@dotnet-bot test Windows ci_lite_x64_debug please |
@Cellule looks like the new bytecode test is failing here, but I don't think this PR should have modified bytecode generation. Do you know if master is in a good state? |
@Cellule Ah, it looks like this is comparing the base version with the nojit version |
58d6375
to
322242a
Compare
Should 322242a be its own commit that goes into 1.7, where the bytecode tests went, to make sure we don't have this issue elsewhere? |
I've backported the commit undoing the bytecode verification, so it shouldn't be necessary on this PR any more. |
322242a
to
7bba9a4
Compare
Thanks, @kfarnung, @jackhorton, and @MSLaguana for porting back that change. I removed the change from my PR and rebased. |
Existing `JsNativeFunction` callbacks have no concept of `new.target` and, as a consequence, native functions cannot be used as the base class for a derived class constructor in Javascript. An API like `JsGetNewTarget` doesen't make a lot of sense so we propose here a new type of callback which will be called with `new.target` directly. `JsCreateEnhancedFunction` acts similarly to the way `JsCreateFunction` did but takes a `JsEnhancedNativeFunction` callback instead. This callback is called passing a struct (`JsNativeFunctionInfo` containing `new.target`, `this`, and `isConstructCall`) as well as ordinary `callee`, `arguments`, and `argumentsCount`. To simplify `JavascriptExternalFunction` and handling of native callback functions in ChakraCore, I refactored `JsCreateFunction` to use a `JsEnhancedNativeFunction` wrapper function which forwards to the `JsNativeFunction` callback. Add native unit tests where an enhanced function (`JavascriptExternalFunction`) is used as the base class for a Javascript derived class. Fixes: chakra-core#3762
7bba9a4
to
ac00296
Compare
…new.target` Merge pull request #4529 from boingoing:NativeCallbackSupportNewTarget Existing `JsNativeFunction` callbacks have no concept of `new.target` and, as a consequence, native functions cannot be used as the base class for a derived class constructor in Javascript. An API like `JsGetNewTarget` doesen't make a lot of sense so we propose here a new type of callback which will be called with `new.target` directly. `JsCreateEnhancedFunction` acts similarly to the way `JsCreateFunction` did but takes a `JsEnhancedNativeFunction` callback instead. This callback is called passing a struct (`JsNativeFunctionInfo` containing `new.target`, `this`, and `isConstructCall`) as well as ordinary `callee`, `arguments`, and `argumentsCount`. To simplify `JavascriptExternalFunction` and handling of native callback functions in ChakraCore, I refactored `JsCreateFunction` to use a `JsEnhancedNativeFunction` wrapper function which forwards to the `JsNativeFunction` callback. Add native unit tests where an enhanced function (`JavascriptExternalFunction`) is used as the base class for a Javascript derived class. Fixes: #3762
This isn’t 100% true as I found out - calling a JsNativeFunction with That said, this way is much cleaner so I welcome the change nonetheless. :) |
Existing
JsNativeFunction
callbacks have no concept ofnew.target
and, as a consequence, native functions cannot be used as the base class for a derived class constructor in Javascript. An API likeJsGetNewTarget
doesen't make a lot of sense so we propose here a new type of callback which will be called withnew.target
directly.JsCreateEnhancedFunction
acts similarly to the wayJsCreateFunction
did but takes aJsEnhancedNativeFunction
callback instead. This callback is called passing a struct (JsNativeFunctionInfo
containingnew.target
,this
, andisConstructCall
) as well as ordinarycallee
,arguments
, andargumentsCount
.To simplify
JavascriptExternalFunction
and handling of native callback functions in ChakraCore, I refactoredJsCreateFunction
to use aJsEnhancedNativeFunction
wrapper function which forwards to theJsNativeFunction
callback.Add native unit tests where an enhanced function (
JavascriptExternalFunction
) is used as the base class for a Javascript derived class.Fixes: #3762