Chromium Embedded Framework documentation
This page provides a hands-on tutorial for building a CEF application by progressively modifying the cefsimple example.
Contents
This tutorial teaches CEF development by modifying the cefsimple example step-by-step. You’ll start with a working application and progressively add features, learning CEF concepts along the way.
Who is this for?
Why start from cefsimple?
.pak files, frameworks) already in the right placePrerequisites:
What you’ll build:
Starting from cefsimple, you’ll progressively add features to create a custom browser with JavaScript integration and popup window customization.
Using the C API? This tutorial uses the C++ API. For pure C implementation, see cefsimple_capi and Using The CAPI.
Before diving into code, here are the key CEF concepts you’ll encounter:
Multi-process architecture: CEF runs multiple processes for security and stability:
Reference counting: CEF objects use reference counting for memory management:
CefRefPtr<T> - Smart pointer that automatically manages reference countsIMPLEMENT_REFCOUNTING macro - Adds thread-safe reference counting to your classesThreading: CEF callbacks run on specific threads:
CEF_REQUIRE_UI_THREAD(), CEF_REQUIRE_RENDERER_THREAD(), etc. to assert correct thread usageBrowser styles:
--use-alloy-style command-line flag for AlloyGetBrowserRuntimeStyle() and GetWindowRuntimeStyle()Views framework: Cross-platform UI system:
CefWindow - Top-level windowCefBrowserView - Embeds the browser in a viewCefWindowDelegate, CefBrowserViewDelegate) - Control window/browser behaviorFor deeper coverage of CEF architecture and concepts, see Tutorial.md.
Download CEF binary distribution:
Visit https://cef-builds.spotifycdn.com/index.html and download the appropriate binary distribution for your platform (e.g., cef_binary_<version>_windows64.tar.bz2).
Extract and enter the directory:
tar -xjf cef_binary_<version>_<platform>.tar.bz2
cd cef_binary_<version>_<platform>
Build cefsimple as-is:
Windows:
# Build with sandbox disabled for simplicity
cmake -B build -G "Visual Studio 17 2022" -A x64 -DUSE_SANDBOX=OFF
cmake --build build --config Release --target cefsimple
build\tests\cefsimple\Release\cefsimple.exe
Note: This tutorial builds with sandbox disabled (-DUSE_SANDBOX=OFF) for simplicity. For production applications, you should enable the sandbox using the bootstrap.exe approach. The sandbox provides important security isolation for renderer processes.
Linux:
cmake -B build
cmake --build build --target cefsimple
build/tests/cefsimple/cefsimple
macOS:
cmake -B build -G Xcode
cmake --build build --config Release --target cefsimple
open build/tests/cefsimple/Release/cefsimple.app
Using an IDE (optional):
Instead of building from the command line, you can open the generated project in your IDE:
build/cef.sln in Visual Studio 2022build/cef.xcodeproj in Xcodebuild directory in CLion or another CMake-supporting IDEVerify it works:
If this works, your environment is set up correctly! Now we can start learning CEF by modifying the code.
tests/cefsimple/
├── simple_app.h # Application handler (process-level)
├── simple_app.cc # Browser creation logic
├── simple_handler.h # Browser handler interface
├── simple_handler.cc # Browser-level event handling
├── simple_handler_win.cc # Windows-specific (window title)
├── simple_handler_linux.cc # Linux-specific (window title)
├── simple_handler_mac.mm # macOS-specific (window title)
├── cefsimple_win.cc # Windows entry point
├── cefsimple_linux.cc # Linux entry point
├── cefsimple_mac.mm # macOS browser process entry point
└── process_helper_mac.cc # macOS helper process entry point
Cross-platform code: simple_app.*, simple_handler.h/cc work identically on all platforms.
Platform-specific code: Entry points and window title updates for platform-specific implementations.
Goal: Strip cefsimple down to the bare minimum (~150 lines total) to understand the essential parts.
cefsimple has features we don’t need yet:
Let’s remove these to see the core clearly.
Before we dive into the code, here’s what each piece does:
SimpleApp - Application-level handler
SimpleHandler - Browser-level event handler
Views Framework Delegates (in simple_app.cc)
SimpleWindowDelegate - Controls window size, close behavior, runtime styleSimpleBrowserViewDelegate - Specifies Alloy browser stylePlatform Entry Point Methods
GetInstance() - Returns the singleton instance (fully functional)CloseAllBrowsers(), IsClosing() - Empty stubs for now (implemented in Step 7 for multi-browser support)ShowMainWindow() - Called on macOS when user clicks dock icon. Empty stub in this tutorial (a proper implementation would show/activate the main window)Now let’s see the actual code:
This is the browser-level event handler. Note the singleton pattern (GetInstance()) and stub methods for platform entry points.
Replace the entire file with:
#ifndef CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
#define CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
#include "include/cef_client.h"
// Simple browser event handler
// Inherits from CefClient (main handler interface) and CefLifeSpanHandler
// (lifecycle events). This allows one class to handle multiple callback types.
class SimpleHandler : public CefClient, public CefLifeSpanHandler {
public:
SimpleHandler();
~SimpleHandler() override;
// Provide access to the single global instance of this object.
// Used by platform entry points (cefsimple_mac.mm, etc.)
static SimpleHandler* GetInstance();
// CefClient methods:
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
// CefLifeSpanHandler methods:
void OnBeforeClose(CefRefPtr<CefBrowser> browser) override;
// Request that all existing browser windows close.
// Used by platform entry points (cefsimple_mac.mm, etc.)
void CloseAllBrowsers(bool force_close);
// Always returns false - stub for platform entry points
bool IsClosing() const { return false; }
void ShowMainWindow();
private:
// Provides thread-safe reference counting (CefRefPtr).
IMPLEMENT_REFCOUNTING(SimpleHandler);
// Prevents copying (CEF objects shouldn't be copied).
DISALLOW_COPY_AND_ASSIGN(SimpleHandler);
};
#endif // CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
Implementation of SimpleHandler. The key method is OnBeforeClose() which quits the app when the browser closes.
Replace the entire file with:
#include "tests/cefsimple/simple_handler.h"
#include "include/cef_app.h"
#include "include/wrapper/cef_helpers.h"
namespace {
// Anonymous namespace - variables here are only visible in this file
SimpleHandler* g_instance = nullptr;
} // namespace
SimpleHandler::SimpleHandler() {
DCHECK(!g_instance); // Debug assertion - crashes if instance already exists
g_instance = this;
}
SimpleHandler::~SimpleHandler() {
g_instance = nullptr;
}
// static
SimpleHandler* SimpleHandler::GetInstance() {
return g_instance;
}
void SimpleHandler::OnBeforeClose(CefRefPtr<CefBrowser> browser) {
CEF_REQUIRE_UI_THREAD();
// Browser closed - quit the application
// This simplified version only supports a single browser
CefQuitMessageLoop();
}
void SimpleHandler::CloseAllBrowsers(bool force_close) {
// Empty stub - required by platform entry points but not used
// in this simplified Views-based version
}
void SimpleHandler::ShowMainWindow() {
// Empty stub. A proper implementation would show/activate the main window.
// Called on macOS when user clicks the dock icon while app is running.
}
Application-level handler that receives callbacks when CEF is initialized. Declares OnContextInitialized() where we create the browser.
Replace the entire file with:
#ifndef CEF_TESTS_CEFSIMPLE_SIMPLE_APP_H_
#define CEF_TESTS_CEFSIMPLE_SIMPLE_APP_H_
#include "include/cef_app.h"
// Implement application-level callbacks for the browser process.
class SimpleApp : public CefApp, public CefBrowserProcessHandler {
public:
SimpleApp();
// CefApp methods:
CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() override {
return this;
}
// CefBrowserProcessHandler methods:
void OnContextInitialized() override;
private:
IMPLEMENT_REFCOUNTING(SimpleApp);
DISALLOW_COPY_AND_ASSIGN(SimpleApp);
};
#endif // CEF_TESTS_CEFSIMPLE_SIMPLE_APP_H_
Implementation of SimpleApp. Creates the browser and window using Views framework. Contains inline delegate classes that control window and browser behavior.
Replace the entire file with:
#include "tests/cefsimple/simple_app.h"
#include "tests/cefsimple/simple_handler.h"
#include "include/cef_browser.h"
#include "include/views/cef_browser_view.h"
#include "include/views/cef_window.h"
#include "include/wrapper/cef_helpers.h"
namespace {
// Window delegate - controls window behavior
class SimpleWindowDelegate : public CefWindowDelegate {
public:
explicit SimpleWindowDelegate(CefRefPtr<CefBrowserView> browser_view)
: browser_view_(browser_view) {}
void OnWindowCreated(CefRefPtr<CefWindow> window) override {
// Add the browser view and show the window
window->AddChildView(browser_view_);
window->Show();
}
bool CanClose(CefRefPtr<CefWindow> window) override {
// Allow the window to close if the browser says it's OK
CefRefPtr<CefBrowser> browser = browser_view_->GetBrowser();
if (browser) {
return browser->GetHost()->TryCloseBrowser();
}
return true;
}
CefSize GetPreferredSize(CefRefPtr<CefView> view) override {
return CefSize(800, 600);
}
cef_runtime_style_t GetWindowRuntimeStyle() override {
return CEF_RUNTIME_STYLE_ALLOY;
}
private:
CefRefPtr<CefBrowserView> browser_view_;
IMPLEMENT_REFCOUNTING(SimpleWindowDelegate);
DISALLOW_COPY_AND_ASSIGN(SimpleWindowDelegate);
};
// Browser view delegate
class SimpleBrowserViewDelegate : public CefBrowserViewDelegate {
public:
SimpleBrowserViewDelegate() = default;
cef_runtime_style_t GetBrowserRuntimeStyle() override {
return CEF_RUNTIME_STYLE_ALLOY;
}
private:
IMPLEMENT_REFCOUNTING(SimpleBrowserViewDelegate);
DISALLOW_COPY_AND_ASSIGN(SimpleBrowserViewDelegate);
};
} // namespace
SimpleApp::SimpleApp() = default;
void SimpleApp::OnContextInitialized() {
CEF_REQUIRE_UI_THREAD();
CefRefPtr<SimpleHandler> handler(new SimpleHandler());
CefBrowserSettings browser_settings;
// Create the browser view (Views framework - cross-platform)
CefRefPtr<CefBrowserView> browser_view = CefBrowserView::CreateBrowserView(
handler, "https://www.google.com", browser_settings, nullptr, nullptr,
new SimpleBrowserViewDelegate());
// Create the window (Views framework - cross-platform)
CefWindow::CreateTopLevelWindow(new SimpleWindowDelegate(browser_view));
}
Since we removed window title updates, delete these files:
tests/cefsimple/simple_handler_win.cctests/cefsimple/simple_handler_linux.cctests/cefsimple/simple_handler_mac.mmCMakeLists.txt tells CMake which source files to compile. Since we deleted the platform-specific handler files, we need to remove them from the build configuration.
Update tests/cefsimple/CMakeLists.txt to remove references to the deleted files:
Find these lines:
set(CEFSIMPLE_SRCS_LINUX
cefsimple_linux.cc
simple_handler_linux.cc
)
set(CEFSIMPLE_SRCS_MAC
cefsimple_mac.mm
simple_handler_mac.mm
)
set(CEFSIMPLE_SRCS_WINDOWS
cefsimple_win.cc
resource.h
simple_handler_win.cc
)
Replace with:
set(CEFSIMPLE_SRCS_LINUX
cefsimple_linux.cc
)
set(CEFSIMPLE_SRCS_MAC
cefsimple_mac.mm
)
set(CEFSIMPLE_SRCS_WINDOWS
cefsimple_win.cc
resource.h
)
This removes the references to simple_handler_linux.cc, simple_handler_mac.mm, and simple_handler_win.cc that we deleted.
Don’t modify these files - they contain platform-specific entry point code that’s already correct:
This simplified code has the following limitations:
CEF_RUNTIME_STYLE_ALLOY in both SimpleWindowDelegate and SimpleBrowserViewDelegate. The original cefsimple supports both Chrome and Alloy styles via command-line flag.CloseAllBrowsers() implementation does nothing). You must close the window to quit the app. (Fixed in Step 7)ShowMainWindow() implementation does nothing). This remains a stub throughout the tutorial.CloseAllBrowsers() and IsClosing() are empty stubs (implemented in Step 7 for multi-browser support). ShowMainWindow() remains a stub throughout (a proper implementation would show/activate the main window on macOS).These limitations are intentional to keep the code minimal for learning. We’ll add features back in subsequent steps.
# Rebuild using the same commands as Step 0
# Windows
cmake --build build --config Release --target cefsimple
# Linux
cmake --build build --target cefsimple
# macOS
cmake --build build --config Release --target cefsimple
Run: See Step 0 for platform-specific run instructions.
You should see a window open with google.com. Note: The window title won’t update with the page title anymore (we removed that feature to simplify), but otherwise the app works the same as the original. Now you understand every line of code!
Three key pieces:
CefApp and CefBrowserProcessHandlerOnContextInitialized() - Called when CEF is ready to create browsersCefClient and CefLifeSpanHandlerGetLifeSpanHandler() - Returns this handler for lifecycle eventsOnBeforeClose() - Called when browser is closing, quits the appSimpleWindowDelegate - Controls window size, close behavior, runtime styleSimpleBrowserViewDelegate - Specifies Alloy browser styleWhy Alloy style? Simpler than Chrome style - you control the UI completely. We explicitly set CEF_RUNTIME_STYLE_ALLOY in both delegates.
Entry points: Platform-specific files (cefsimple_*.cc/mm) handle:
CefInitialize)CefRunMessageLoop)CefShutdown)Program Flow: Here’s the complete execution sequence:
simple_handler_*.cc/mm)CMakeLists.txt to remove references to those files (see instructions above)Goal: Load different URLs by modifying the code (not command-line yet).
Just change one line to demonstrate how browser creation works.
In OnContextInitialized(), replace the URL:
From: "https://www.google.com"
To: "https://www.wikipedia.org"
# Rebuild using the same commands as Step 0
# Windows
cmake --build build --config Release --target cefsimple
# Linux
cmake --build build --target cefsimple
# macOS
cmake --build build --config Release --target cefsimple
Run: See Step 0 for platform-specific run instructions.
Now it loads Wikipedia instead! This shows that the URL is set when creating the CefBrowserView.
Try these:
file:///path/to/local.html - Local files (use absolute path)data:text/html,<h1>Hello CEF!</h1> - Inline HTMLThe second parameter to CefBrowserView::CreateBrowserView is the initial URL. The browser navigates to this URL immediately after creation.
Goal: Pass custom URLs via command-line: cefsimple --url=https://example.com
Read the command-line and use it to set the URL.
Add at the top:
#include "include/cef_command_line.h"
Replace OnContextInitialized with:
void SimpleApp::OnContextInitialized() {
CEF_REQUIRE_UI_THREAD();
// Get command-line arguments
CefRefPtr<CefCommandLine> command_line =
CefCommandLine::GetGlobalCommandLine();
// Check for --url= parameter
std::string url = command_line->GetSwitchValue("url");
if (url.empty()) {
url = "https://www.google.com"; // Default URL
}
CefRefPtr<SimpleHandler> handler(new SimpleHandler());
CefBrowserSettings browser_settings;
CefRefPtr<CefBrowserView> browser_view = CefBrowserView::CreateBrowserView(
handler, url, browser_settings, nullptr, nullptr,
new SimpleBrowserViewDelegate());
CefWindow::CreateTopLevelWindow(new SimpleWindowDelegate(browser_view));
}
# Rebuild using the same commands as Step 0
# Windows
cmake --build build --config Release --target cefsimple
# Linux
cmake --build build --target cefsimple
# macOS
cmake --build build --config Release --target cefsimple
Run with custom URL:
# Windows
build\tests\cefsimple\Release\cefsimple.exe --url=https://github.com
# Linux
build/tests/cefsimple/cefsimple --url=https://github.com
# macOS
open build/tests/cefsimple/Release/cefsimple.app --args --url=https://github.com
New concepts:
CefCommandLine::GetGlobalCommandLine() - Access command-line arguments parsed by CEFGetSwitchValue("url") - Read the value of --url= parameterOnContextInitializedTry it:
# Load local file
cefsimple --url=file:///home/user/test.html
# Load data URL (note the quotes to escape shell characters)
cefsimple --url="data:text/html,<h1>Hello!</h1>"
Goal: Update window title to match the page title as it loads.
Update to add display handler:
#ifndef CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
#define CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
#include "include/cef_client.h"
class SimpleHandler : public CefClient,
// NEW: Add display handler for title change events
public CefDisplayHandler,
public CefLifeSpanHandler {
public:
SimpleHandler();
~SimpleHandler() override;
static SimpleHandler* GetInstance();
// CefClient methods:
// NEW: Return display handler
CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
// CefDisplayHandler methods:
// NEW: Handle page title changes
void OnTitleChange(CefRefPtr<CefBrowser> browser,
const CefString& title) override;
// CefLifeSpanHandler methods:
void OnBeforeClose(CefRefPtr<CefBrowser> browser) override;
void CloseAllBrowsers(bool force_close);
bool IsClosing() const { return false; }
void ShowMainWindow();
private:
IMPLEMENT_REFCOUNTING(SimpleHandler);
DISALLOW_COPY_AND_ASSIGN(SimpleHandler);
};
#endif // CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
Add new includes at the top:
#include "include/views/cef_browser_view.h"
#include "include/views/cef_window.h"
Add the OnTitleChange implementation (after the existing methods, before OnBeforeClose()):
void SimpleHandler::OnTitleChange(CefRefPtr<CefBrowser> browser,
const CefString& title) {
CEF_REQUIRE_UI_THREAD();
// Get the browser view from the browser
CefRefPtr<CefBrowserView> browser_view =
CefBrowserView::GetForBrowser(browser);
if (browser_view) {
// Get the window and update its title
CefRefPtr<CefWindow> window = browser_view->GetWindow();
if (window) {
window->SetTitle(title);
}
}
}
Note: OnBeforeClose() and other methods from Step 1 remain unchanged.
# Rebuild using the same commands as Step 0
# Windows
cmake --build build --config Release --target cefsimple
# Linux
cmake --build build --target cefsimple
# macOS
cmake --build build --config Release --target cefsimple
Run: See Step 0 for platform-specific run instructions.
Now the window title updates to match the page title as you navigate!
New concepts:
CefDisplayHandler::OnTitleChange() - Called when the page title changesCefBrowserView::GetForBrowser() - Get the browser view from a browser instanceCefBrowserView::GetWindow() - Get the window containing the browser viewCefWindow::SetTitle() - Update the window titleFlow:
OnTitleChange() called with new titleGoal: Execute JavaScript code in the page from C++.
Add load handler:
#ifndef CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
#define CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
#include "include/cef_client.h"
class SimpleHandler : public CefClient,
public CefDisplayHandler,
public CefLifeSpanHandler,
// NEW: Add load handler for page load events
public CefLoadHandler {
public:
SimpleHandler();
~SimpleHandler() override;
static SimpleHandler* GetInstance();
// CefClient methods:
CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
// NEW: Return load handler
CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
// CefDisplayHandler methods:
void OnTitleChange(CefRefPtr<CefBrowser> browser,
const CefString& title) override;
// CefLifeSpanHandler methods:
void OnBeforeClose(CefRefPtr<CefBrowser> browser) override;
// CefLoadHandler methods:
// NEW: Handle page load completion
void OnLoadEnd(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int httpStatusCode) override;
void CloseAllBrowsers(bool force_close);
bool IsClosing() const { return false; }
void ShowMainWindow();
private:
IMPLEMENT_REFCOUNTING(SimpleHandler);
DISALLOW_COPY_AND_ASSIGN(SimpleHandler);
};
#endif // CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
Add the OnLoadEnd implementation (after OnTitleChange(), before OnBeforeClose()):
void SimpleHandler::OnLoadEnd(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int httpStatusCode) {
CEF_REQUIRE_UI_THREAD();
// Only execute on the main frame
if (!frame->IsMain()) {
return;
}
// Execute JavaScript to change the page background color
frame->ExecuteJavaScript(
"document.body.style.backgroundColor = 'lightblue';",
frame->GetURL(), 0);
}
# Rebuild using the same commands as Step 0
# Windows
cmake --build build --config Release --target cefsimple
# Linux
cmake --build build --target cefsimple
# macOS
cmake --build build --config Release --target cefsimple
Run: See Step 0 for platform-specific run instructions.
Now when pages load, the background turns light blue!
New concepts:
CefLoadHandler::OnLoadEnd() - Called when a frame finishes loadingCefFrame::IsMain() - Check if this is the main frame (not an iframe)CefFrame::ExecuteJavaScript() - Execute arbitrary JavaScript codeTry modifying the JavaScript:
// Alert box
frame->ExecuteJavaScript("alert('Hello from C++!');", frame->GetURL(), 0);
// DOM manipulation
frame->ExecuteJavaScript(
"var h1 = document.createElement('h1');"
"h1.textContent = 'Added by CEF!';"
"document.body.insertBefore(h1, document.body.firstChild);",
frame->GetURL(), 0);
// Console log
frame->ExecuteJavaScript("console.log('CEF executed this');",
frame->GetURL(), 0);
Important notes:
ExecuteJavaScript() doesn’t return the result directlyEvaluateJavaScript() (not shown) or process messages (next step)Goal: Call C++ functions from JavaScript code in the page.
This requires implementing both browser process and renderer process handlers.
Update to add renderer process handler:
#ifndef CEF_TESTS_CEFSIMPLE_SIMPLE_APP_H_
#define CEF_TESTS_CEFSIMPLE_SIMPLE_APP_H_
#include "include/cef_app.h"
class SimpleApp : public CefApp,
public CefBrowserProcessHandler,
// NEW: Add renderer process handler for JavaScript integration
public CefRenderProcessHandler {
public:
SimpleApp();
// CefApp methods:
CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() override {
return this;
}
// NEW: Return renderer process handler
CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler() override {
return this;
}
// CefBrowserProcessHandler methods:
void OnContextInitialized() override;
// NEW: CefRenderProcessHandler methods for JavaScript integration
// NEW: Called when the JavaScript context is created in the renderer process
void OnContextCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context) override;
private:
IMPLEMENT_REFCOUNTING(SimpleApp);
DISALLOW_COPY_AND_ASSIGN(SimpleApp);
};
#endif // CEF_TESTS_CEFSIMPLE_SIMPLE_APP_H_
Add new includes at the top (after the existing includes):
#include "include/cef_v8.h"
Add V8 handler class in the anonymous namespace (add after the includes, before the existing SimpleWindowDelegate and SimpleBrowserViewDelegate classes which remain unchanged):
// V8 handler to handle JavaScript function calls in the renderer process
class MyV8Handler : public CefV8Handler {
public:
MyV8Handler() = default;
bool Execute(const CefString& name,
CefRefPtr<CefV8Value> object,
const CefV8ValueList& arguments,
CefRefPtr<CefV8Value>& retval,
CefString& exception) override {
if (name == "sayHello") {
// Get the argument
if (arguments.size() == 1 && arguments[0]->IsString()) {
CefString message = arguments[0]->GetStringValue();
// Send message to browser process
CefRefPtr<CefProcessMessage> msg =
CefProcessMessage::Create("greeting");
msg->GetArgumentList()->SetString(0, message);
CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
CefRefPtr<CefFrame> frame = context->GetFrame();
// Results in an asynchronous call to
// SimpleHandler::OnProcessMessageReceived in the browser process
frame->SendProcessMessage(PID_BROWSER, msg);
// Return value to JavaScript
retval = CefV8Value::CreateString("Message sent to C++!");
return true;
}
}
return false;
}
private:
IMPLEMENT_REFCOUNTING(MyV8Handler);
DISALLOW_COPY_AND_ASSIGN(MyV8Handler);
};
Add the OnContextCreated implementation (after OnContextInitialized, before the end of the file):
// Called when the JavaScript context is created in the renderer process
void SimpleApp::OnContextCreated(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefV8Context> context) {
// Debug assertion - crashes if called on wrong thread
// V8 APIs must only be called from the renderer thread
CEF_REQUIRE_RENDERER_THREAD();
// Register JavaScript function 'sayHello' on the global object (window.sayHello)
CefRefPtr<CefV8Value> object = context->GetGlobal();
CefRefPtr<CefV8Handler> handler = new MyV8Handler();
CefRefPtr<CefV8Value> func =
CefV8Value::CreateFunction("sayHello", handler);
object->SetValue("sayHello", func, V8_PROPERTY_ATTRIBUTE_NONE);
}
Note: The SimpleWindowDelegate, SimpleBrowserViewDelegate, and OnContextInitialized implementations remain unchanged from previous steps.
Important: For JavaScript-to-C++ communication to work, the renderer process needs access to SimpleApp::OnContextCreated to register the JavaScript function.
process_helper_mac.cccefsimple_win.cc and cefsimple_linux.ccUpdate the main function to create SimpleApp before CefExecuteProcess:
Find:
// CEF applications have multiple sub-processes (render, GPU, etc) that share
// the same executable. This function checks the command-line and, if this is
// a sub-process, executes the appropriate logic.
int exit_code = CefExecuteProcess(main_args, nullptr, nullptr);
if (exit_code >= 0) {
// The sub-process has completed so return here.
return exit_code;
}
Replace with:
// SimpleApp implements application-level callbacks. It will create the first
// browser instance in OnContextInitialized() after CEF has initialized.
CefRefPtr<SimpleApp> app(new SimpleApp);
// CEF applications have multiple sub-processes (render, GPU, etc) that share
// the same executable. This function checks the command-line and, if this is
// a sub-process, executes the appropriate logic.
int exit_code = CefExecuteProcess(main_args, app, nullptr);
if (exit_code >= 0) {
// The sub-process has completed so return here.
return exit_code;
}
Then find the later SimpleApp creation:
// SimpleApp implements application-level callbacks for the browser process.
// It will create the first browser instance in OnContextInitialized() after
// CEF has initialized.
CefRefPtr<SimpleApp> app(new SimpleApp);
Delete these lines (since we now create app earlier).
Update the RunMain function to create SimpleApp before CefExecuteProcess:
Find:
// CEF applications have multiple sub-processes (render, GPU, etc) that share
// the same executable. This function checks the command-line and, if this is
// a sub-process, executes the appropriate logic.
exit_code = CefExecuteProcess(main_args, nullptr, sandbox_info);
if (exit_code >= 0) {
// The sub-process has completed so return here.
return exit_code;
}
Replace with:
// SimpleApp implements application-level callbacks. It will create the first
// browser instance in OnContextInitialized() after CEF has initialized.
CefRefPtr<SimpleApp> app(new SimpleApp);
// CEF applications have multiple sub-processes (render, GPU, etc) that share
// the same executable. This function checks the command-line and, if this is
// a sub-process, executes the appropriate logic.
exit_code = CefExecuteProcess(main_args, app, sandbox_info);
if (exit_code >= 0) {
// The sub-process has completed so return here.
return exit_code;
}
Then find the later SimpleApp creation:
// SimpleApp implements application-level callbacks for the browser process.
// It will create the first browser instance in OnContextInitialized() after
// CEF has initialized.
CefRefPtr<SimpleApp> app(new SimpleApp);
Delete these lines (since we now create app earlier).
Add include at the top:
#include "tests/cefsimple/simple_app.h"
Update the main function to create SimpleApp before CefExecuteProcess:
Find:
return CefExecuteProcess(main_args, nullptr, nullptr);
Replace with:
// SimpleApp implements application-level callbacks for the sub-process.
// This allows SimpleApp::OnContextCreated to be called in the renderer process.
CefRefPtr<SimpleApp> app(new SimpleApp);
// Execute the sub-process.
return CefExecuteProcess(main_args, app, nullptr);
Note: Unlike the main executable, the macOS helper doesn’t have a separate browser process initialization section, so there’s no duplicate SimpleApp creation to remove.
Update the helper sources to include simple_app:
Find:
# cefsimple helper sources.
set(CEFSIMPLE_HELPER_SRCS_MAC
process_helper_mac.cc
)
Replace with:
# cefsimple helper sources.
set(CEFSIMPLE_HELPER_SRCS_MAC
process_helper_mac.cc
simple_app.cc
simple_app.h
simple_handler.cc
simple_handler.h
)
This ensures the helper process can compile and link the SimpleApp code.
Note: We’re including simple_handler.cc/h in the helper sources even though SimpleHandler is never used in renderer processes (only SimpleApp::OnContextCreated is called). For a production application, you could create a separate CefApp-derived class for the renderer process (e.g., SimpleRendererApp) to avoid linking browser-process-only dependencies like SimpleHandler into the helper executables.
Reconfigure CMake (macOS only) to pick up the CMakeLists.txt changes:
cmake -B build -G Xcode
Add process message handler:
#ifndef CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
#define CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
#include "include/cef_client.h"
class SimpleHandler : public CefClient,
public CefDisplayHandler,
public CefLifeSpanHandler,
public CefLoadHandler {
public:
SimpleHandler();
~SimpleHandler() override;
static SimpleHandler* GetInstance();
// CefClient methods:
CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
// NEW: Handle messages from renderer process
bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) override;
// CefDisplayHandler methods:
void OnTitleChange(CefRefPtr<CefBrowser> browser,
const CefString& title) override;
// CefLifeSpanHandler methods:
void OnBeforeClose(CefRefPtr<CefBrowser> browser) override;
// CefLoadHandler methods:
void OnLoadEnd(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int httpStatusCode) override;
void CloseAllBrowsers(bool force_close);
bool IsClosing() const { return false; }
void ShowMainWindow();
private:
IMPLEMENT_REFCOUNTING(SimpleHandler);
DISALLOW_COPY_AND_ASSIGN(SimpleHandler);
};
#endif // CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
Add at the top (after the existing includes):
#include <sstream>
Update OnLoadEnd to inject a test button (replace the background color change with button injection):
// Frame load finished in the browser process
void SimpleHandler::OnLoadEnd(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int httpStatusCode) {
CEF_REQUIRE_UI_THREAD();
if (!frame->IsMain()) {
return;
}
// Inject a test button to call our C++ function
std::stringstream ss;
ss << "var button = document.createElement('button');"
<< "button.textContent = 'Call C++ Function';"
<< "button.style.position = 'fixed';"
<< "button.style.top = '10px';"
<< "button.style.left = '10px';"
<< "button.style.zIndex = '10000';"
<< "button.style.backgroundColor = '#4CAF50';"
<< "button.style.color = 'white';"
<< "button.style.padding = '10px 15px';"
<< "button.style.border = 'none';"
<< "button.style.borderRadius = '4px';"
<< "button.style.cursor = 'pointer';"
<< "button.onclick = function() {"
<< " var result = sayHello('Hello from JavaScript!');"
<< " alert(result);"
<< "};"
<< "document.body.appendChild(button);";
frame->ExecuteJavaScript(ss.str(), frame->GetURL(), 0);
}
Add OnProcessMessageReceived (after OnLoadEnd, before OnBeforeClose):
// Process message received in the browser process
bool SimpleHandler::OnProcessMessageReceived(
CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) {
CEF_REQUIRE_UI_THREAD();
// Handle message from MyHandler::Execute() in the renderer process
if (message->GetName() == "greeting") {
CefString msg = message->GetArgumentList()->GetString(0);
// Show the message in a JavaScript alert
std::stringstream ss;
ss << "alert('C++ received: " << msg.ToString() << "');";
frame->ExecuteJavaScript(ss.str(), frame->GetURL(), 0);
return true;
}
return false;
}
Note: OnTitleChange and OnBeforeClose remain unchanged from previous steps.
# Rebuild using the same commands as Step 0
# Windows
cmake --build build --config Release --target cefsimple
# Linux
cmake --build build --target cefsimple
# macOS
cmake --build build --config Release --target cefsimple
Run: See Step 0 for platform-specific run instructions.
Now you’ll see a button in the top-left corner of every page. Click it to:
sayHello() functionNew concepts:
OnContextCreated() - Called when JavaScript context is createdCefV8Value::CreateFunction() - Register a JavaScript functionCefV8Handler::Execute() - Handle JavaScript function callsCefProcessMessage::Create() - Create a messageCefFrame::SendProcessMessage() - Send from renderer to browserCefClient::OnProcessMessageReceived() - Receive in browser processFlow:
OnContextCreated() registers sayHello() functionsayHello('Hello from JavaScript!')MyV8Handler::Execute() receives call, creates process messageSimpleHandler::OnProcessMessageReceived() handles message in browser processImportant notes:
Learn more:
Goal: Track multiple browser instances and only quit the application when all browsers are closed.
In Steps 1-6, the application only supported a single browser. When that browser closed, the app quit immediately. Now we’ll add:
CloseAllBrowsers() implementationThe single-browser approach from earlier steps has limitations:
Multi-browser tracking is essential for real applications that support popups, new windows, or multiple browser instances.
Add browser tracking and lifecycle methods:
#ifndef CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
#define CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
#include <map>
#include "include/cef_client.h"
class SimpleHandler : public CefClient,
public CefDisplayHandler,
public CefLifeSpanHandler,
public CefLoadHandler {
public:
SimpleHandler();
~SimpleHandler() override;
static SimpleHandler* GetInstance();
// CefClient methods:
CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) override;
// CefDisplayHandler methods:
void OnTitleChange(CefRefPtr<CefBrowser> browser,
const CefString& title) override;
// CefLifeSpanHandler methods:
// NEW: Track browser creation
void OnAfterCreated(CefRefPtr<CefBrowser> browser) override;
void OnBeforeClose(CefRefPtr<CefBrowser> browser) override;
// CefLoadHandler methods:
void OnLoadEnd(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int httpStatusCode) override;
void CloseAllBrowsers(bool force_close);
bool IsClosing() const { return is_closing_; }
void ShowMainWindow();
private:
// NEW: Track all browser instances (keyed by browser ID)
// These members are only accessed on the UI thread (OnAfterCreated,
// OnBeforeClose, and CloseAllBrowsers all run on TID_UI), so no
// additional synchronization is needed.
using BrowserMap = std::map<int, CefRefPtr<CefBrowser>>;
BrowserMap browser_map_;
// NEW: Track whether we're closing
bool is_closing_ = false;
IMPLEMENT_REFCOUNTING(SimpleHandler);
DISALLOW_COPY_AND_ASSIGN(SimpleHandler);
};
#endif // CEF_TESTS_CEFSIMPLE_SIMPLE_HANDLER_H_
Changes:
#include <map> for browser tracking containerOnAfterCreated() to track browser creationbrowser_map_ member to store all browser instances (keyed by browser ID)is_closing_ flag to prevent new browsers during shutdownIsClosing() to return the actual flag instead of hardcoded falseAdd includes for callbacks and closure tasks (after the existing includes):
#include "include/base/cef_callback.h"
#include "include/wrapper/cef_closure_task.h"
Add OnAfterCreated (new method, add after OnTitleChange):
void SimpleHandler::OnAfterCreated(CefRefPtr<CefBrowser> browser) {
CEF_REQUIRE_UI_THREAD();
// Add browser to our tracking map
browser_map_[browser->GetIdentifier()] = browser;
}
Update OnLoadEnd to add second button (replace the existing OnLoadEnd):
void SimpleHandler::OnLoadEnd(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
int httpStatusCode) {
CEF_REQUIRE_UI_THREAD();
if (!frame->IsMain()) {
return;
}
// Inject test buttons
std::stringstream ss;
// "Call C++ Function" button
ss << "var button1 = document.createElement('button');"
<< "button1.textContent = 'Call C++ Function';"
<< "button1.style.position = 'fixed';"
<< "button1.style.top = '10px';"
<< "button1.style.left = '10px';"
<< "button1.style.zIndex = '10000';"
<< "button1.style.backgroundColor = '#4CAF50';"
<< "button1.style.color = 'white';"
<< "button1.style.padding = '10px 15px';"
<< "button1.style.border = 'none';"
<< "button1.style.borderRadius = '4px';"
<< "button1.style.cursor = 'pointer';"
<< "button1.onclick = function() {"
<< " var result = sayHello('Hello from JavaScript!');"
<< " alert(result);"
<< "};"
<< "document.body.appendChild(button1);"
// "Create Popup Window" button
<< "var button2 = document.createElement('button');"
<< "button2.textContent = 'Create Popup Window';"
<< "button2.style.position = 'fixed';"
<< "button2.style.top = '50px';"
<< "button2.style.left = '10px';"
<< "button2.style.zIndex = '10000';"
<< "button2.style.backgroundColor = '#2196F3';"
<< "button2.style.color = 'white';"
<< "button2.style.padding = '10px 15px';"
<< "button2.style.border = 'none';"
<< "button2.style.borderRadius = '4px';"
<< "button2.style.cursor = 'pointer';"
<< "button2.onclick = function() {"
<< " window.open('https://www.wikipedia.org', '_blank', 'width=800,height=600');"
<< "};"
<< "document.body.appendChild(button2);";
frame->ExecuteJavaScript(ss.str(), frame->GetURL(), 0);
}
Update OnBeforeClose (replace the existing OnBeforeClose):
void SimpleHandler::OnBeforeClose(CefRefPtr<CefBrowser> browser) {
CEF_REQUIRE_UI_THREAD();
// Remove browser from our tracking map
browser_map_.erase(browser->GetIdentifier());
// Only quit when all browsers are closed
if (browser_map_.empty()) {
CefQuitMessageLoop();
}
}
Update CloseAllBrowsers (replace the existing stub implementation):
void SimpleHandler::CloseAllBrowsers(bool force_close) {
// CloseAllBrowsers can be called from anywhere (e.g., macOS menu's Quit command),
// but CEF browser operations must run on the UI thread. Check if we're already
// on the UI thread; if not, post a task to run this method on the UI thread.
if (!CefCurrentlyOn(TID_UI)) {
CefPostTask(TID_UI, base::BindOnce(&SimpleHandler::CloseAllBrowsers, this,
force_close));
return;
}
is_closing_ = true;
if (browser_map_.empty()) {
return;
}
// Iterate over browsers and request close
// Use a copy because browser_map_ will be modified during iteration (OnBeforeClose)
BrowserMap browser_map_copy = browser_map_;
for (auto& pair : browser_map_copy) {
pair.second->GetHost()->CloseBrowser(force_close);
}
}
Note: Constructor, OnTitleChange, OnProcessMessageReceived, and ShowMainWindow remain unchanged from previous steps.
Changes:
OnAfterCreated(): Adds browser to browser_map_ when created (keyed by browser ID)OnLoadEnd(): Added second button “Create Popup Window” (blue, positioned below first button)OnBeforeClose(): Removes browser from map by ID, only quits when map is emptyCloseAllBrowsers(): Sets is_closing_ flag, iterates over browser_map_ copy, closes each browserCefPostTask() to ensure execution on UI thread# Rebuild using the same commands as Step 0
# Windows
cmake --build build --config Release --target cefsimple
# Linux
cmake --build build --target cefsimple
# macOS
cmake --build build --config Release --target cefsimple
Run: See Step 0 for platform-specific run instructions.
New concepts:
std::map<int, CefRefPtr<CefBrowser>> keyed by browser IDbrowser->GetIdentifier() - unique integer for each browserOnAfterCreated() - Called when browser is created, add to mapOnBeforeClose() - Called when browser is closing, remove from map by IDCefRefPtr comparison doesn’t work as expected for erase(), so we key by integer IDCloseAllBrowsers() uses CefPostTask() to execute on UI threadbase::BindOnce() creates a callback that captures this and argumentsinclude/base/cef_callback.h - provides base::BindOnceinclude/wrapper/cef_closure_task.h - converts callbacks to CefTask objects for CefPostTaskbrowser_map_ before iteratingOnBeforeClose() modifies the map during iteration (would cause crashes)pair.second to get browser from map entryTesting:
macOS note: The Quit menu now works! CloseAllBrowsers() is properly implemented, so the macOS application menu’s Quit option (which calls CloseAllBrowsers()) will close all browsers and quit the app.
Learn more:
Goal: Customize the appearance and behavior of popup browser windows using the Views framework.
In Step 7, clicking “Create Popup Window” opened a new browser, but it used default styling (same size as main window). Now we’ll add custom styling for popup windows:
OnPopupBrowserViewCreated() callback in SimpleBrowserViewDelegateOnBeforePopup vs OnPopupBrowserViewCreatedCEF provides two callbacks for popup customization with different purposes:
OnBeforePopup (CefLifeSpanHandler) - Low-level popup control:
CefWindowInfo, CefClient and CefBrowserSettingsOnPopupBrowserViewCreated (CefBrowserViewDelegate) - Views-specific window creation:
In this step, we use OnPopupBrowserViewCreated to create smaller popup windows (600x400) compared to the main window (800x600).
Update SimpleBrowserViewDelegate to add OnPopupBrowserViewCreated (add after GetBrowserRuntimeStyle, before the private section):
// Browser view delegate
class SimpleBrowserViewDelegate : public CefBrowserViewDelegate {
public:
SimpleBrowserViewDelegate() = default;
// NEW: Called when a popup browser view is created
bool OnPopupBrowserViewCreated(
CefRefPtr<CefBrowserView> browser_view,
CefRefPtr<CefBrowserView> popup_browser_view,
bool is_devtools) override {
// Create a smaller window for popups (600x400 instead of 800x600)
// We use a custom SimpleWindowDelegate that returns a different size
CefWindow::CreateTopLevelWindow(
new SimpleWindowDelegate(popup_browser_view));
// Returning true indicates we created the window
return true;
}
cef_runtime_style_t GetBrowserRuntimeStyle() override {
return CEF_RUNTIME_STYLE_ALLOY;
}
private:
IMPLEMENT_REFCOUNTING(SimpleBrowserViewDelegate);
DISALLOW_COPY_AND_ASSIGN(SimpleBrowserViewDelegate);
};
Update SimpleWindowDelegate to support custom popup sizes (replace the GetPreferredSize method):
CefSize GetPreferredSize(CefRefPtr<CefView> view) override {
// Check if this is a popup by seeing if we have a browser yet
CefRefPtr<CefBrowser> browser = browser_view_->GetBrowser();
if (browser && browser->IsPopup()) {
// Popup windows are smaller
return CefSize(600, 400);
}
// Main window is larger
return CefSize(800, 600);
}
Note: The SimpleWindowDelegate, MyV8Handler, and OnContextInitialized/OnContextCreated implementations remain unchanged from Step 7.
# Rebuild using the same commands as Step 0
# Windows
cmake --build build --config Release --target cefsimple
# Linux
cmake --build build --target cefsimple
# macOS
cmake --build build --config Release --target cefsimple
Run: See Step 0 for platform-specific run instructions.
New concepts:
browser_view - The parent browser view that initiated the popuppopup_browser_view - The newly created popup browser viewis_devtools - True if this is a DevTools popuptrue - You created the window (CEF won’t create a default window)false - CEF will create a default windowCefWindow for the popup using CefWindow::CreateTopLevelWindow()popup_browser_view to SimpleWindowDelegate (same pattern as main window)GetPreferredSize() returns different sizes for popups vs main windowsbrowser->IsPopup() identifies popup browsersCefBrowser::IsPopup() returns true for browsers created via window.open() or similarOnContextInitialized) returns falseCefBrowserViewDelegate::GetDelegateForPopupBrowserView() to return a different delegate for popupsPopupBrowserViewDelegate class with different implementations of OnPopupBrowserViewCreated(), etc.IsPopup()) is simpler and works well for most casesGetDelegateForPopupBrowserView when you need significantly different delegate behavior for popupsTesting:
When to use each callback:
| Scenario | Use OnBeforePopup | Use OnPopupBrowserViewCreated |
|---|---|---|
| Block all popups | ✅ Return true to cancel | ❌ Too late (browser already created) |
| Custom popup window size (Views) | ❌ Limited control | ✅ Full window control |
| Custom popup window styling (Views) | ❌ Limited control | ✅ Full window control |
| Popup policy enforcement | ✅ Can examine URL, features | ❌ Too late |
| Native window popups (non-Views) | ✅ Can modify CefWindowInfo | ❌ Not called for native windows |
Important notes:
OnPopupBrowserViewCreated is only called for Views-based browsers (our tutorial uses Views)OnBeforePopup and CefWindowInfo insteadis_devtools parameter)Congratulations! You’ve built a CEF application from scratch and learned:
cefclient - A comprehensive sample application demonstrating:
client:// URLs)cef-project - Focused example applications demonstrating specific features:
General Usage - Complete CEF documentation covering:
Using The CAPI - Complete guide to the CEF C API:
Want help implementing additional features in your CEF application? The CEF project includes instructions for using Claude Code to assist with tasks like adding custom URL schemes, JavaScript-to-C++ communication, request interception, and more.
See the CEF Client Development Guide for step-by-step instructions.