Skip to content

Commit

Permalink
Implement Javascript Binding v2 (#2247)
Browse files Browse the repository at this point in the history
* JSB Rewrite - Add CefSharp.RegisterBoundObject javascript method

To get around the problem of IPC timing, I've reversed the communication, now it's the render process requesting
the bound objects, the implementation is incomplete at this stage, more just a basic proof of concept

Rewrite BindingTest.html using QUnit
One of the tests doesn't map correctly as it requires def user interaction

TODO:

- Get objects by name
- Rename Messages.h values that relate to this change so they are more descriptive
- Better error handling
- Caching of objects within a render process
- Investigate global context options, don't think it's possible to call an async method directly in the global context

# Conflicts:
#	CefSharp.Example/Resources/BindingTest.html

* JSB Improve Request/Response message names

* BindingTest.html - Output stress test results

* Remove JavascriptRootObject was leftover from time we used WCF to transmit objects

* JavascriptObjectRepository - Objects are now sent in the same list with an IsAsync flag

Separation when sending over IPC with two unique lists isn't necessary

* JavascriptObjectRepository - Return objects by name or return all by default

Untabify CefAppUnmanagedWrapper.cpp
Add minor comment to CefBrowserWrapper for a reminder that we use Frame Identifier as dictionary key

* Add //TODO: JSB comments for work that's left to be done

Ideally within a render process we cache bound objects once they've been communicated,
It would also be nice to make multiple binding requests so objects can later be added dynamically

* JSB Allow multiple calls to CefSharp.RegisterBoundObject

Update BindingTest.html to name two distinct calls to different bound objects

* JSB - Add some notes about caching, no working implementation yet

* JSB - Dynamically Register object on Request

* JSB Add ability to unbind an object and rename RegisterBoundObject to BindObjectAsync

# Conflicts:
#	CefSharp.Example/Resources/BindingTest.html

* JSB BindObjectAsync - if objects already bound then return false

* JSB - Ignore Indexer properties

Were previously throwing an exception

* JSB - Add LegacyJavascriptBindingEnabled option so preserve existing behaviour

This is only useful for SPA application and those that only navigate to pages
hosting within a single domain. Any sort of cross-site navigation and a new
render process will be spawned and objects won't be bound automatically
(You can use the new methods to request they're bound yourself, at least in theory,
more testing required on this)

* Add LegacyBindingTest.html

Current disabled by default. To enable uncomment CefSharpSettings.LegacyJavascriptBindingEnabled = true;
in CefExample.cs

* RegisterJsObject and RegisterAsyncJsObject can now only be used when CefSharpSettings.LegacyJavascriptBindingEnabled = true

See #2246 for details
  • Loading branch information
amaitland authored Jan 22, 2018
1 parent a245196 commit 9b01b69
Show file tree
Hide file tree
Showing 39 changed files with 1,077 additions and 185 deletions.
124 changes: 102 additions & 22 deletions CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include "include\base\cef_logging.h"
#include "CefBrowserWrapper.h"
#include "CefAppUnmanagedWrapper.h"
#include "RegisterBoundObjectHandler.h"
#include "JavascriptRootObjectWrapper.h"
#include "Serialization\V8Serialization.h"
#include "Serialization\JsObjectsSerialization.h"
Expand Down Expand Up @@ -72,29 +73,40 @@ namespace CefSharp
browser->SendProcessMessage(CefProcessId::PID_BROWSER, contextCreatedMessage);
}

auto browserWrapper = FindBrowserWrapper(browser->GetIdentifier(), true);

auto rootObjectWrappers = browserWrapper->JavascriptRootObjectWrappers;
auto frameId = frame->GetIdentifier();

JavascriptRootObjectWrapper^ rootObject;
if (!rootObjectWrappers->TryGetValue(frameId, rootObject))
if (_legacyBindingEnabled)
{
rootObject = gcnew JavascriptRootObjectWrapper(browser->GetIdentifier(), browserWrapper->BrowserProcess);
rootObjectWrappers->TryAdd(frameId, rootObject);
}
auto browserWrapper = FindBrowserWrapper(browser->GetIdentifier(), true);

if (rootObject->IsBound)
{
LOG(WARNING) << "A context has been created for the same browser / frame without context released called previously";
}
else
{
if (!Object::ReferenceEquals(_javascriptRootObject, nullptr) || !Object::ReferenceEquals(_javascriptAsyncRootObject, nullptr))
auto rootObjectWrappers = browserWrapper->JavascriptRootObjectWrappers;
auto frameId = frame->GetIdentifier();

if (_javascriptObjects->Count > 0)
{
rootObject->Bind(_javascriptRootObject, _javascriptAsyncRootObject, context->GetGlobal());
JavascriptRootObjectWrapper^ rootObject;
if (!rootObjectWrappers->TryGetValue(frameId, rootObject))
{
rootObject = gcnew JavascriptRootObjectWrapper(browser->GetIdentifier(), browserWrapper->BrowserProcess);
rootObjectWrappers->TryAdd(frameId, rootObject);
}

rootObject->Bind(_javascriptObjects->Values, context->GetGlobal());
}
}

auto global = context->GetGlobal();

auto cefSharpObj = CefV8Value::CreateObject(NULL, NULL);
global->SetValue("CefSharp", cefSharpObj, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_READONLY);

auto bindObjAsyncFunction = CefV8Value::CreateFunction("BindObjectAsync", new RegisterBoundObjectHandler(_registerBoundObjectRegistry, _javascriptObjects));
cefSharpObj->SetValue("BindObjectAsync", bindObjAsyncFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);

auto unBindObFunction = CefV8Value::CreateFunction("DeleteBoundObject", new RegisterBoundObjectHandler(_registerBoundObjectRegistry, _javascriptObjects));
cefSharpObj->SetValue("DeleteBoundObject", unBindObFunction, CefV8Value::PropertyAttribute::V8_PROPERTY_ATTRIBUTE_NONE);

//TODO: JSB We could in theory auto bind all the cached objects which would resemble the original JSB behaviour, if
//the cache is empty which would be the case for any cross-site navigation request or the first request made to a browser instance
//then no objects would be bound by default
};

void CefAppUnmanagedWrapper::OnContextReleased(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context)
Expand Down Expand Up @@ -190,7 +202,7 @@ namespace CefSharp
if (browserWrapper == nullptr)
{
if (name == kJavascriptCallbackDestroyRequest ||
name == kJavascriptRootObjectRequest ||
name == kJavascriptRootObjectResponse ||
name == kJavascriptAsyncMethodCallResponse)
{
//If we can't find the browser wrapper then we'll just
Expand Down Expand Up @@ -398,10 +410,78 @@ namespace CefSharp

handled = true;
}
else if (name == kJavascriptRootObjectRequest)
else if (name == kJavascriptRootObjectResponse)
{
_javascriptAsyncRootObject = DeserializeJsRootObject(argList, 0);
_javascriptRootObject = DeserializeJsRootObject(argList, 1);
auto useLegacyBehaviour = argList->GetBool(0);

//For the old legacy behaviour we add the objects
//to the cache
if (useLegacyBehaviour)
{
_legacyBindingEnabled = true;

auto javascriptObjects = DeserializeJsObjects(argList, 4);

for each (JavascriptObject^ obj in Enumerable::OfType<JavascriptObject^>(javascriptObjects))
{
_javascriptObjects->Add(obj->JavascriptName, obj);
}
}
else
{
auto browserId = argList->GetInt(1);
auto frameId = GetInt64(argList, 2);
auto callbackId = GetInt64(argList, 3);
auto javascriptObjects = DeserializeJsObjects(argList, 4);

//TODO: JSB Implement Caching of JavascriptObjects
//Should caching be configurable? On a per object basis?
/*for each (JavascriptObject^ obj in Enumerable::OfType<JavascriptObject^>(javascriptObjects))
{
if (_javascriptObjects->ContainsKey(obj->JavascriptName))
{
_javascriptObjects->Remove(obj->JavascriptName);
}
_javascriptObjects->Add(obj->JavascriptName, obj);
}*/

auto browserWrapper = FindBrowserWrapper(browser->GetIdentifier(), true);

auto rootObjectWrappers = browserWrapper->JavascriptRootObjectWrappers;
auto frame = browser->GetFrame(frameId);
if (frame.get())
{
JavascriptRootObjectWrapper^ rootObject;
if (!rootObjectWrappers->TryGetValue(frameId, rootObject))
{
rootObject = gcnew JavascriptRootObjectWrapper(browser->GetIdentifier(), browserWrapper->BrowserProcess);
rootObjectWrappers->TryAdd(frameId, rootObject);
}

auto context = frame->GetV8Context();

if (context.get() && context->Enter())
{
try
{
rootObject->Bind(javascriptObjects, context->GetGlobal());

JavascriptAsyncMethodCallback^ callback;
if (_registerBoundObjectRegistry->TryGetAndRemoveMethodCallback(callbackId, callback))
{
callback->Success(CefV8Value::CreateBool(true));

//TODO: JSB deal with failure - no object matching bound
}
}
finally
{
context->Exit();
}
}
}
}

handled = true;
}
else if (name == kJavascriptAsyncMethodCallResponse)
Expand Down
12 changes: 8 additions & 4 deletions CefSharp.BrowserSubprocess.Core/CefAppUnmanagedWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "include/cef_base.h"

#include "CefBrowserWrapper.h"
#include "RegisterBoundObjectRegistry.h"

using namespace System::Collections::Generic;

Expand All @@ -26,12 +27,12 @@ namespace CefSharp
gcroot<List<CefExtension^>^> _extensions;
gcroot<List<CefCustomScheme^>^> _schemes;
bool _focusedNodeChangedEnabled;

// The serialized registered object data waiting to be used (only contains methods and bound async).
gcroot<JavascriptRootObject^> _javascriptAsyncRootObject;
bool _legacyBindingEnabled;

// The serialized registered object data waiting to be used.
gcroot<JavascriptRootObject^> _javascriptRootObject;
gcroot<Dictionary<String^, JavascriptObject^>^> _javascriptObjects;

gcroot<RegisterBoundObjectRegistry^> _registerBoundObjectRegistry;

public:
static const CefString kPromiseCreatorFunction;
Expand All @@ -44,6 +45,9 @@ namespace CefSharp
_extensions = gcnew List<CefExtension^>();
_schemes = schemes;
_focusedNodeChangedEnabled = enableFocusedNodeChanged;
_javascriptObjects = gcnew Dictionary<String^, JavascriptObject^>();
_registerBoundObjectRegistry = gcnew RegisterBoundObjectRegistry();
_legacyBindingEnabled = false;
}

~CefAppUnmanagedWrapper()
Expand Down
2 changes: 1 addition & 1 deletion CefSharp.BrowserSubprocess.Core/CefBrowserWrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
#include "TypeUtils.h"
#include "Stdafx.h"
#include "JavascriptRootObjectWrapper.h"
#include "Async/JavascriptAsyncMethodCallback.h"

using namespace CefSharp::Internals::Async;
using namespace System::ServiceModel;
Expand All @@ -27,6 +26,7 @@ namespace CefSharp
MCefRefPtr<CefBrowser> _cefBrowser;

internal:
//Frame Identifier is used as Key
property ConcurrentDictionary<int64, JavascriptRootObjectWrapper^>^ JavascriptRootObjectWrappers;

public:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@
<ClInclude Include="Async\JavascriptAsyncMethodWrapper.h" />
<ClInclude Include="Async\JavascriptAsyncObjectWrapper.h" />
<ClInclude Include="CefAppUnmanagedWrapper.h" />
<ClInclude Include="RegisterBoundObjectHandler.h" />
<ClInclude Include="RegisterBoundObjectRegistry.h" />
<ClInclude Include="resource.h" />
<ClInclude Include="WcfEnabledSubProcess.h" />
<ClInclude Include="SubProcess.h" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="RegisterBoundObjectHandler.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="RegisterBoundObjectRegistry.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="AssemblyInfo.cpp">
Expand Down
51 changes: 18 additions & 33 deletions CefSharp.BrowserSubprocess.Core/JavascriptRootObjectWrapper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,29 @@ using namespace System::Threading;

namespace CefSharp
{
void JavascriptRootObjectWrapper::Bind(JavascriptRootObject^ rootObject, JavascriptRootObject^ asyncRootObject, const CefRefPtr<CefV8Value>& v8Value)
void JavascriptRootObjectWrapper::Bind(ICollection<JavascriptObject^>^ objects, const CefRefPtr<CefV8Value>& v8Value)
{
if (_isBound)
if (objects->Count > 0)
{
throw gcnew InvalidOperationException("This root object has already been bound.");
}

_isBound = true;

if (rootObject != nullptr)
{
auto memberObjects = rootObject->MemberObjects;
for each (JavascriptObject^ obj in Enumerable::OfType<JavascriptObject^>(memberObjects))
{
auto wrapperObject = gcnew JavascriptObjectWrapper(_browserProcess);
wrapperObject->Bind(obj, v8Value, _callbackRegistry);

_wrappedObjects->Add(wrapperObject);
}
}

if (asyncRootObject != nullptr)
{
auto memberObjects = asyncRootObject->MemberObjects;
auto saveMethod = gcnew Func<JavascriptAsyncMethodCallback^, int64>(this, &JavascriptRootObjectWrapper::SaveMethodCallback);
auto promiseCreator = v8Value->GetValue(CefAppUnmanagedWrapper::kPromiseCreatorFunction);
for each (JavascriptObject^ obj in Enumerable::OfType<JavascriptObject^>(memberObjects))
{
auto wrapperObject = gcnew JavascriptAsyncObjectWrapper(_callbackRegistry, saveMethod);
wrapperObject->Bind(obj, v8Value, promiseCreator);

_wrappedAsyncObjects->Add(wrapperObject);
for each (JavascriptObject^ obj in Enumerable::OfType<JavascriptObject^>(objects))
{
if (obj->IsAsync)
{
auto wrapperObject = gcnew JavascriptAsyncObjectWrapper(_callbackRegistry, saveMethod);
wrapperObject->Bind(obj, v8Value, promiseCreator);

_wrappedAsyncObjects->Add(wrapperObject);
}
else
{
auto wrapperObject = gcnew JavascriptObjectWrapper(_browserProcess);
wrapperObject->Bind(obj, v8Value, _callbackRegistry);

_wrappedObjects->Add(wrapperObject);
}
}
}
}
Expand All @@ -53,12 +44,6 @@ namespace CefSharp
return _callbackRegistry;
}

bool JavascriptRootObjectWrapper::IsBound::get()
{
return _isBound;
}


int64 JavascriptRootObjectWrapper::SaveMethodCallback(JavascriptAsyncMethodCallback^ callback)
{
auto callbackId = Interlocked::Increment(_lastCallback);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ namespace CefSharp
initonly List<JavascriptObjectWrapper^>^ _wrappedObjects;
initonly List<JavascriptAsyncObjectWrapper^>^ _wrappedAsyncObjects;
initonly Dictionary<int64, JavascriptAsyncMethodCallback^>^ _methodCallbacks;
bool _isBound;
int64 _lastCallback;
IBrowserProcess^ _browserProcess;
// The entire set of possible JavaScript functions to
Expand All @@ -42,11 +41,6 @@ namespace CefSharp
CefSharp::Internals::JavascriptCallbackRegistry^ get();
}

property bool IsBound
{
bool get();
}

public:
JavascriptRootObjectWrapper(int browserId, IBrowserProcess^ browserProcess)
{
Expand All @@ -55,7 +49,6 @@ namespace CefSharp
_wrappedAsyncObjects = gcnew List<JavascriptAsyncObjectWrapper^>();
_callbackRegistry = gcnew JavascriptCallbackRegistry(browserId);
_methodCallbacks = gcnew Dictionary<int64, JavascriptAsyncMethodCallback^>();
_isBound = false;
}

~JavascriptRootObjectWrapper()
Expand Down Expand Up @@ -87,7 +80,7 @@ namespace CefSharp

bool TryGetAndRemoveMethodCallback(int64 id, JavascriptAsyncMethodCallback^% callback);

void Bind(JavascriptRootObject^ rootObject, JavascriptRootObject^ asyncRootObject, const CefRefPtr<CefV8Value>& v8Value);
void Bind(ICollection<JavascriptObject^>^ objects, const CefRefPtr<CefV8Value>& v8Value);
};
}

Loading

0 comments on commit 9b01b69

Please sign in to comment.