The CEF3 framwork operates using a multi-process architecture where the Browser and Renderer live in separate address spaces. While the Browser process typically aligns with the application's main window thread, the Renderer runs independently. Both processes maintain their own instances of browser and frame objects.
Traditionally, interacting with C++ from JavaScript involves subclassing CefV8Handler and implementing the execution method:
bool MyV8Handler::Execute(const CefString& funcName,
CefRefPtr<CefV8Value> targetObject,
const CefV8ValueList& args,
CefRefPtr<CefV8Value>& retVal,
CefString& errorMsg)
This synchronous approach works well for immediate tasks where the result is readily available. However, complications arise when the Renderer needs data residing in the Browser process. While you can dispatch a message to the Browser using frame->SendProcessMessage(PID_BROWSER, message), the inter-process communication (IPC) is asynchronous. This means the Execute method cannot simply wait for a return value without blocking the render thread.
To solve this, we can leverage JavaScript's native support for asynchronous operations via callbacks. The strategy involves the following workflow:
- JS Invocation: The JavaScript code calls a bound function (e.g.,
queryBackend) and passes a callback function as an argument. This binding usually occurs within the Render process'sOnContextCreatedmethod. - Handler Logic: Inside the
Executemethod, the C++ handler extracts the callback argument (aCefV8Value) and stores it in a static map or cache associated with a unique request ID. - IPC Dispatch: The handler sends a process message to the Browser process requesting the necessary operation, including the unique ID.
- Browser Processing: The Browser process handles the message in
OnProcessMessageReceived, performs the required logic, and sends a response message back to the Renderer containing the result and the original ID. - Callback Executtion: The Renderer receives the response, looks up the ID in the cache, retrieves the stored JavaScript callback, and invokes it with the returned data.
Here is a conceptual example of how the V8 handler might manage the asynchronous request:
// Snippet of the V8 Handler logic
std::map<int, CefRefPtr<CefV8Value>> g_callbackCache;
int g_nextId = 0;
bool MyV8Handler::Execute(...) {
if (funcName == "queryBackend") {
// 1. Extract the JS callback
CefRefPtr<CefV8Value> jsCallback = args[0];
// 2. Generate ID and cache the callback
int reqId = g_nextId++;
g_callbackCache[reqId] = jsCallback;
// 3. Create message and send to Browser
CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("fetch_data");
msg->GetArgumentList()->SetInt(0, reqId);
frame->SendProcessMessage(PID_BROWSER, msg);
return true;
}
return false;
}
When the response arrives in the Renderer's OnProcessMessageReceived:
bool MyRenderApp::OnProcessMessageReceived(...) {
if (message->GetName() == "fetch_data_response") {
int reqId = message->GetArgumentList()->GetInt(0);
CefString result = message->GetArgumentList()->GetString(1);
// Retrieve and execute the callback
auto it = g_callbackCache.find(reqId);
if (it != g_callbackCache.end()) {
CefV8ValueList callbackArgs;
callbackArgs.push_back(CefV8Value::CreateString(result));
it->second->ExecuteFunction(nullptr, callbackArgs);
g_callbackCache.erase(it);
}
return true;
}
return false;
}
Key Considerations:
- Context Creation: If your applicasion uses multiple iframes,
OnContextCreatedfires for each one. To avoid registering the function multiple times, consider initializing bindings inOnWebKitInitializedif appropriate for your scope. - Cross-Origin Frames: Be aware that iframes from different domains might face restrictions regarding message reception.
- Handler Lifetime: It is recommended to initialize your
CefV8Handleronce during the Render process setup to prevent the function cache from becoming inaccessible or empty across different frame contexts.