☰
VS2010 MFC 内嵌 CEF3 90.6.0:JS 与 C++ 双向调用实战
2026/10/17 7:01:40 网站建设 项目流程

简介:这份资源面向需要在VS2010环境下做桌面端Web集成的C++开发者,核心是用CEF3 90.6.0版本把Chromium浏览器内核嵌入MFC窗口,并打通JavaScript与C++的双向调用。压缩包共629个文件,约457.22MB,以269个h头文件、168个pak资源包、28个dll动态库、14个lib静态库和9个cpp源码为主,另含obj、pdb、tlog等编译中间产物与sln、vcxproj工程文件,基本覆盖从依赖库到可编译工程的完整链路。已有1556人学习下载。资源价值在于提供了一套可直接参考的MFC+CEF3集成骨架:借助CEFRenderWidgetHost子类完成窗口嵌入,通过消息循环同步处理WM_PAINT、WM_SIZE,并利用CefMessageRouterBrowserSide::Handler与自定义scheme工厂实现JS调用C++函数,配合browser_subprocess_path、multi_threaded_message_loop等启动参数配置,能帮助读者快速理解多进程消息循环、渲染宿主与脚本交互的排错思路,适合具备一定MFC与C++基础、希望少走集成弯路的开发者。

1. 把 CEF3 90.6.0 塞进 VS2010 的 MFC 窗口:一个还在维护老工控项目的工程师会碰上的事

如果你手上有一个 VS2010 写的 MFC 老项目,客户突然要求内嵌一个能跑现代前端页面的浏览器控件,还要让页面里的 JS 能调到 C++ 侧的函数,那你大概率会搜到 CEF3。CEF3 全称 Chromium Embedded Framework,本质是把 Chromium 内核封装成一套 C++ 库,让你在自己的进程里开一个浏览器。问题在于,CEF3 官方从很早就把 VS2010 的支持砍掉了,而 90.6.0 这个版本对应 Chromium 90,编译工具链默认是 VS2017 起步。所以「VS2010 + CEF3 90.6.0 + MFC」这个组合,本身就是逆着官方推荐走的,能跑通靠的是 ABI 兼容和一堆手工配置。这篇笔记面向的是那些没法升级 IDE、又必须让老 MFC 程序长出浏览器能力的从业者,我会把从拿到二进制包到 JS 双向调用的完整链路拆开,包括我踩过的编译期和运行期坑。适合能看懂 C++ 和 MFC 消息循环、愿意动手改工程属性的人,纯前端背景的读者可能会觉得配置部分偏重。

2. 选型与工程结构:为什么是 90.6.0 而不是更新的版本

2.1 版本锁定的现实理由

CEF3 的版本号跟 Chromium 主版本对齐,90.6.0 对应 Chromium 90,发布于 2021 年前后。选它通常不是因为它新,而是因为它「够用且相对好拿二进制包」。CEF 官方提供的标准分发(Standard Distribution)里包含预编译的 libcef.dll、头文件和 lib 文件,但预编译包的工具链版本决定了你能不能直接链接。90.6.0 的官方二进制包一般用 VS2017 或 VS2019 编译,而 VS2010 的 C++ 编译器版本是 MSVC 10.0(_MSC_VER 1600),跟 VS2017 的 1910 以上差距很大。直接链接会碰到 C++ 运行时库不匹配、STL 符号找不到的问题。

常见做法是:不直接链接官方预编译的 libcef.lib,而是用 CEF 的 C API(libcef_dll_wrapper 那层)或者干脆走动态加载。但更稳的路子是找一份用 VS2010 工具链重新编译过的 CEF3 二进制包,或者自己用 CMake 配合 VS2010 生成器重新编 libcef_dll_wrapper。后者工作量大,但可控。我一般会先确认手头的包是不是已经带了 VS2010 可用的 wrapper,判断方法看后面章节。

2.2 MFC 工程里 CEF 的目录布局

不管包从哪来,落到工程里建议按固定结构放,避免路径混乱。我习惯在解决方案根目录下建一个third_party/cef,里面分几个子目录:

目录内容说明
include/cef 头文件全部 .h,工程附加包含目录指向这里
lib/libcef.lib、libcef_dll_wrapper.lib链接用,注意 x86/x64 要分开
bin/libcef.dll、chrome_elf.dll、locales、resources运行时必须跟 exe 同目录或按 cef 规则放
wrapper/libcef_dll_wrapper 源码需要自己编时用

MFC 工程本身建议用 Unicode 字符集,CEF3 的接口大量使用CefString,底层是 UTF-16,跟 MFC 的CString(Unicode 配置下)能较好互转。如果工程还是多字节字符集,后面 JS 传中文会出乱码,这个坑在排查章节会细说。

2.3 初始化 CEF 的时机与参数

CEF 要求在主进程启动早期做一次全局初始化,且必须在 MFC 的InitInstance里、创建任何窗口之前完成。核心是填CefSettings,其中multi_threaded_message_loop这个参数决定消息循环由谁驱动。MFC 有自己的消息泵,如果设成 false(单线程消息循环),就需要在 MFC 的PreTranslateMessage或OnIdle里手动调CefDoMessageLoopWork();如果设成 true,CEF 自己起 UI 线程,MFC 侧不用管,但跨线程调用要小心。

我一般对 MFC 项目设multi_threaded_message_loop = true,省去手动泵消息的麻烦,代价是 CEF 的 UI 线程不是 MFC 主线程,所有跟窗口句柄相关的操作要走 CEF 的线程模型。下面是一段初始化代码,放在InitInstance开头:

// 初始化 CEF,必须在创建主窗口前调用 CefEnableHighDPISupport(); // 高分屏支持,Chromium 90 里还有效 CefSettings settings; settings.multi_threaded_message_loop = true; // CEF 自管 UI 线程 settings.no_sandbox = true; // 老项目常关沙箱,避免权限问题 settings.log_severity = LOGSEVERITY_WARNING; // 日志级别,排查时调成 INFO // 指定 locales 和 resources 路径,相对 exe 目录 CefString(&settings.locales_dir_path).FromString("locales"); CefString(&settings.resources_dir_path).FromString("resources"); // 子进程路径,CEF 多进程模型需要 CefString(&settings.browser_subprocess_path).FromString("cef_subprocess.exe"); HINSTANCE hInst = GetModuleHandle(NULL); if (!CefInitialize(CefMainArgs(hInst), settings, nullptr, nullptr)) { AfxMessageBox(_T("CEF 初始化失败")); return FALSE; }

逻辑说明:CefMainArgs接收当前模块句柄,让 CEF 能定位自身资源。multi_threaded_message_loop = true后,CEF 会创建自己的 UI 线程,MFC 主线程继续跑原来的消息循环,两者通过 CEF 的 task runner 通信。no_sandbox = true在老项目里几乎是默认选择,因为沙箱对进程权限和目录结构有额外要求,工控环境往往不满足。browser_subprocess_path指向一个独立的子进程 exe,这个 exe 的main里要调CefExecuteProcess,否则浏览器起不来。参数改动的边界:如果设multi_threaded_message_loop = false,就必须在 MFC 的OnIdle里周期性调CefDoMessageLoopWork(),否则页面会卡死不动。

3. 把浏览器嵌进 MFC 窗口:窗口句柄、消息循环与 JS 调用打通

3.1 创建浏览器窗口并绑定到 MFC 子窗口

CEF 的浏览器窗口可以有两种模式:一种是 CEF 自己创建顶层窗口,另一种是作为子窗口嵌入到已有的 HWND 里。MFC 场景下我们要的是后者,用CefWindowInfo的SetAsChild把浏览器绑到一个 MFC 控件或对话框的客户区。通常做法是在对话框上放一个静态文本控件(Static)作为占位,拿到它的 HWND 和矩形,传给 CEF。

// 假设对话框上有个 IDC_BROWSER_PLACEHOLDER 的 Static 控件 CWnd* pPlaceholder = GetDlgItem(IDC_BROWSER_PLACEHOLDER); CRect rect; pPlaceholder->GetClientRect(&rect); pPlaceholder->ClientToScreen(&rect); ScreenToClient(&rect); // 转成对话框客户区坐标 CefWindowInfo windowInfo; windowInfo.SetAsChild(pPlaceholder->GetSafeHwnd(), CefRect(0, 0, rect.Width(), rect.Height())); CefBrowserSettings browserSettings; browserSettings.web_security = STATE_DISABLED; // 本地页面调试时常用,生产慎用 CefBrowserHost::CreateBrowser(windowInfo, browserClient, "http://localhost:8080/index.html", browserSettings, nullptr, nullptr);

逻辑说明:SetAsChild的第一个参数是父窗口句柄,CEF 会在它下面创建一个子窗口来渲染页面。CefRect是相对父窗口客户区的坐标,这里用占位控件的尺寸。browserSettings.web_security = STATE_DISABLED会关掉同源策略等安全限制,方便本地文件或跨域调试,但上线前一定要改回默认,否则等于把浏览器安全模型拆了。CreateBrowser是异步的,返回时浏览器还没创建完,真正的窗口句柄要在CefLifeSpanHandler::OnAfterCreated里拿。

3.2 实现 CefClient 与 JS 绑定的关键接口

要让 JS 能调到 C++,核心是实现CefRenderProcessHandler的OnContextCreated,在里面往 V8 上下文注册一个 C++ 对象。这个对象继承CefV8Handler,在Execute方法里根据函数名分发到具体的 C++ 逻辑。注意OnContextCreated是在渲染进程里执行的,所以注册的 C++ 对象实际上活在渲染进程,跟主进程的 MFC 对象不是同一个地址空间。如果 JS 调用需要操作 MFC 窗口,必须通过进程间通信(CEF 的SendProcessMessage)把请求发回主进程。

// 渲染进程侧:注册 JS 可调用的对象 class MyV8Handler : public CefV8Handler { public: virtual bool Execute(const CefString& name, CefRefPtr<CefV8Value> object, const CefV8ValueList& arguments, CefRefPtr<CefV8Value>& retval, CefString& exception) override { if (name == "callNative") { // 把参数打包,发消息给主进程 CefRefPtr<CefProcessMessage> msg = CefProcessMessage::Create("js_call_native"); msg->GetArgumentList()->SetString(0, arguments[0]->GetStringValue()); CefV8Context::GetCurrentContext()->GetBrowser() ->SendProcessMessage(PID_BROWSER, msg); retval = CefV8Value::CreateBool(true); return true; } return false; } IMPLEMENT_REFCOUNTING(MyV8Handler); }; // 在 OnContextCreated 里绑定 void MyRenderHandler::OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) { CefRefPtr<CefV8Value> global = context->GetGlobal(); CefRefPtr<CefV8Value> fn = CefV8Value::CreateFunction( "callNative", new MyV8Handler()); global->SetValue("nativeBridge", fn, V8_PROPERTY_ATTRIBUTE_NONE); }

逻辑说明:OnContextCreated在每个 frame 的 JS 上下文创建时触发,这里把nativeBridge这个函数挂到 window 上,页面里就能写window.nativeBridge.callNative("hello")。Execute里拿到参数后,通过SendProcessMessage发到 browser 进程(主进程),主进程在CefClient::OnProcessMessageReceived里接收并处理。retval是同步返回值,但真正的 C++ 处理是异步的,如果 JS 需要拿到 C++ 的返回结果,得用回调或者 Promise 封装,这是常见做法。

3.3 主进程侧接收 JS 消息并回调 MFC

主进程的CefClient子类里重写OnProcessMessageReceived,根据消息名分发。这里要注意线程:OnProcessMessageReceived在 CEF 的 UI 线程里执行,如果multi_threaded_message_loop = true,这个线程不是 MFC 主线程,直接操作 MFC 控件会崩。稳妥做法是用PostMessage把数据丢回 MFC 窗口,让主线程处理。

bool MyClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefProcessId source_process, CefRefPtr<CefProcessMessage> message) { if (message->GetName() == "js_call_native") { CefString arg = message->GetArgumentList()->GetString(0); // 转成 CString 后 PostMessage 给 MFC 窗口 CString* pData = new CString(arg.c_str()); ::PostMessage(m_hMainWnd, WM_JS_CALL_NATIVE, 0, (LPARAM)pData); return true; } return false; }

逻辑说明:m_hMainWnd是 MFC 主窗口句柄,在创建浏览器时保存下来。WM_JS_CALL_NATIVE是自定义消息,在 MFC 消息映射里处理,收到后delete掉堆上的CString,避免泄漏。参数传递用裸指针加手动释放是 MFC 老项目的常见写法,也可以用std::shared_ptr包一层,但跨模块传递时要注意 CRT 堆的一致性。如果 JS 调用频繁,建议做批量合并,避免消息队列被打爆。

4. 避坑与排查:VS2010 链接 CEF3 90.6.0 时最常翻车的几处

4.1 链接报错 LNK2001 找不到__imp_符号

现象:编译通过,链接时大量LNK2001: 无法解析的外部符号 __imp_...,符号名里带cef或base。原因通常是 libcef.lib 的导入库跟当前工具链不匹配,或者 libcef_dll_wrapper.lib 是用更高版本 MSVC 编的,ABI 对不上。VS2010 的_MSC_VER是 1600,而官方 90.6.0 包一般是 1910 以上。解决办法:确认 wrapper 是否用 VS2010 重新编过;如果没有,用 CMake 生成 VS2010 工程重编libcef_dll_wrapper,编译时把CEF_USE_SANDBOX关掉,并确保libcef.lib和 wrapper 的运行时库(/MT 或 /MD)一致。我一般统一用 /MD,跟 CEF 默认一致。

4.2 程序启动即崩,日志显示子进程创建失败

现象:exe 一运行就退出,debug.log里写Failed to create subprocess或browser_subprocess_path相关错误。原因是 CEF 多进程模型需要一个独立的子进程可执行文件,这个文件里必须调CefExecuteProcess,且路径要跟CefSettings.browser_subprocess_path一致。很多人只编了主 exe,忘了子进程。解决办法:单独建一个控制台工程,main里写CefExecuteProcess,输出成cef_subprocess.exe,跟主 exe 放同目录。如果不想多一个 exe,也可以让主 exe 自己兼任子进程,在InitInstance最前面判断命令行参数,但 MFC 工程里这样做容易跟 MFC 初始化冲突,不推荐。

4.3 页面显示空白,但进程和窗口都正常

现象:浏览器窗口出来了,尺寸也对,但内容全白,debug.log里可能有Failed to load resource或locales相关警告。原因通常是locales和resources目录没放对位置,或者locales_dir_path设成了绝对路径但实际目录不存在。CEF 需要locales/zh-CN.pak这类资源文件来渲染界面和本地化。解决办法:把官方包里的locales、resources整个目录拷到 exe 同目录,CefSettings里用相对路径。如果用了--locale命令行参数,确保对应的 pak 文件存在。另外检查chrome_elf.dll是否在 exe 同目录,缺它也会白屏。

4.4 JS 调用 C++ 无反应,控制台报nativeBridge is undefined

现象:页面里写window.nativeBridge.callNative(...),浏览器控制台报nativeBridge is undefined。原因是OnContextCreated没有触发,或者注册的对象挂在了错误的 frame 上。常见情况是CefRenderProcessHandler没有正确设置到CefApp里,或者CefApp没有传给CefInitialize。另一个可能是页面在 iframe 里,OnContextCreated对每个 frame 都会调,但父页面访问不到子 frame 的全局对象。解决办法:确认CefApp子类重写了GetRenderProcessHandler并返回有效对象;在OnContextCreated里加日志确认触发;如果页面用 iframe,把绑定逻辑放到对应 frame 的上下文里,或者用frame->ExecuteJavaScript注入。

4.5 中文参数从 JS 传到 C++ 变成乱码

现象:JS 里传"你好",C++ 侧收到的CefString转CString后显示乱码。原因是字符集不匹配:CEF 内部用 UTF-16,CefString在 Windows 下是CefStringBase<CefStringTraitsUTF16>,转CString时如果工程是多字节字符集,CString是CStringA,直接赋值会按当前代码页截断。解决办法:把 MFC 工程字符集改成 Unicode,或者用CefString的ToWString()再转CStringW。如果必须用多字节,走CefString::ToString()拿 UTF-8,再用MultiByteToWideChar转。我一般直接改 Unicode,省事且跟 CEF 一致。

5. 进阶:用 CefMessageRouter 做双向调用与资源释放的收尾习惯

前面章节的 JS 调 C++ 是单向的,C++ 主动调 JS 或者需要 JS 返回结果时,裸用SendProcessMessage会很别扭。CEF 提供了一个CefMessageRouter,它把请求-响应模式封装好了,JS 侧用cefQuery,C++ 侧在OnQuery里处理并回调,支持异步返回。配置方式是在CefClient里创建CefMessageRouterBrowserSide,在OnProcessMessageReceived和OnRenderProcessThreadCreated里转发消息,渲染进程侧对应创建CefMessageRouterRendererSide。这样 JS 可以写:

// 页面里发起查询,带回调 window.cefQuery({ request: 'getUserName', onSuccess: function(response) { console.log('用户名:' + response); }, onFailure: function(error_code, error_message) { console.error(error_message); } });

C++ 侧在OnQuery里根据request分发,处理完调callback->Success("张三")或callback->Failure(-1, "错误")。CefMessageRouter的好处是自动处理了上下文失效、回调生命周期,比手写消息可靠得多。参数上注意CefMessageRouterConfig里的js_query_function默认是cefQuery,可以改成自己的名字避免跟页面已有变量冲突。

资源释放这块,CEF 的关闭流程容易出问题。CefBrowserHost::CloseBrowser(true)之后,要在CefLifeSpanHandler::OnBeforeClose里把CefRefPtr<CefBrowser>置空,然后调CefShutdown()。如果multi_threaded_message_loop = true,CefShutdown必须在 MFC 主线程退出前调用,且调用后不能再调任何 CEF 接口。我见过有人在OnDestroy里调CefShutdown,结果 CEF 的 UI 线程还在跑,直接死锁。稳妥顺序是:主窗口OnClose里关浏览器,等OnBeforeClose触发后发自定义消息给主线程,主线程收到后调CefShutdown,再PostQuitMessage。从那以后我每次收尾都强制走一遍「关浏览器 → 等 OnBeforeClose → CefShutdown → 退出消息循环」这个顺序,再没遇到过退出卡死。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询