在 Arm 上的 Windows 平台上使用 Chromium Embedded Framework 构建原生 Webview 应用程序





5.00/5 (3投票s)
此演示展示了如何通过相对较少的代码行,我们能够构建一个可以轻松交叉编译的 GUI 应用程序。
编写 WoA GUI 应用程序的选项
在更详细地讨论 CEF 之前,我们首先讨论编写 WoA GUI 应用程序的其他选项的优势和挑战。
Microsoft 框架
如果您已经熟悉微软现有的用于构建 GUI 应用程序的框架和 API 之一,您可能可以使用您选择的框架。原生 WoA 应用程序可以使用以下方式构建:
- Win32 API
- UWP
- Windows Forms - 从 .NET 5.0 开始支持
- WPF - 从 .NET 5.0.8 开始支持
- Xamarin Forms
使用这些框架之一可能允许您重用现有知识和代码。但是,除了 Xamarin Forms,它们都不是跨平台的。
Electron
另一个应用程序构建选项是流行的 Electron 框架。Electron 是一个用于使用 JavaScript、HTML 和 CSS 构建桌面应用程序的框架。Electron 将 Chromium 和 Node.js 嵌入到其二进制文件中。这允许您维护一个 Javascript 代码库并创建可以与操作系统 (OS) 直接交互的跨平台应用程序。在典型的 Electron 应用程序中,您使用 Web 技术(HTML 和 CSS)结合 JavaScript (JS) 或 TypeScript 来编写用户界面 (UI) 和前端逻辑。您可以使用 node-addon-api 从 JS 调用原生代码。
Electron 应用程序至少有一个主进程和一个或多个渲染器进程。任何进程都可以链接包含原生代码的共享库。要调用原生函数,您必须将数据封送到应用程序二进制接口 (ABI) 边界,这可能会带来一些开销。
Electron 的主要优点是 Web UI 极具可移植性。此外,Electron 框架提供了一个可移植的 API,用于与各种操作系统功能(如任务栏和剪贴板)进行接口,跨多个平台,包括 iOS、macOS、Android、Windows 和 WoA。
当大部分业务逻辑不需要原生性能时,Electron 是一个很好的选择。它还提供了为应用程序中性能关键的部分使用原生扩展的能力。打包 Electron 应用程序时,您必须注意确保原生代码已针对当前目标架构进行编译。
Electron 的主要缺点是大小:一个最小的应用程序重达 30 兆字节。应用程序也固定到它们使用的 Electron 构建中嵌入的 Chromium 版本。这可能是有益的,因为它确保您的应用程序不会因 Chromium 的更改而中断。由于 Chromium 中偶尔出现的安全漏洞,这也存在风险。当出现零日漏洞时,您必须等待问题在 Chromium 中打补丁,并且新的 Electron 构建包含 Chromium 更新后,才能更新您的应用程序以确保其安全。
Chromium Embedded Framework
另一个应用程序创建选项是 Chromium Embedded Framework (CEF),它在 2021 年初获得了对 Arm 上的 Windows 的官方支持。与 Electron 一样,CEF 构建在 Chromium 项目之上,提供跨平台 GUI 功能。
Electron 通常作为“宿主”应用程序运行,而您可以将 CEF 嵌入到现有的原生应用程序中。CEF 的 C 和 C++ API 管理其运行时,而不是 JavaScript API。
作为一个框架,CEF 可能比 Electron 更难上手。它的生态系统也更小。然而,当应用程序的很大一部分需要或已经是原生代码时,CEF 是一个不错的选择。它也比 Electron 小,只有 4MB 而不是 30MB。与 Electron 一样,由于 Chromium 中潜在的零日漏洞,CEF 带来了一些风险。当这些漏洞出现时,您将不得不等待包含修复程序的 CEF 构建,然后创建您应用程序的新构建。
在 WoA 上开始使用 CEF
虽然 Visual Studio 可以在 WoA 设备上运行,但它尚未得到官方支持。因此,我们将在 x86_64 主机上构建和编译我们的示例项目。我们将使用 Visual Studio 远程工具将应用程序部署并测试到运行 Windows 10 的 Arm 驱动的 Surface Pro X 上。对于此演示,我们使用 C++17。
在开始之前,请确保您已安装 Visual Studio 的 ARM64 构建工具。为此,请打开 Visual Studio Installer,单击 VS2019 的“修改”按钮,选择使用 C++ 进行桌面开发,勾选MSVC v142 – VS 2019 C++ ARM64 构建工具复选框,然后单击窗口右下角的“修改”按钮。
当您需要在目标 Arm 设备上调试应用程序时,您可以使用 Windows 的 ARM 调试工具,或者使用 Visual Studio 远程调试工具从 x86 主机远程调试您的应用程序。
将 CEF 集成到 CMake 项目中是最简单的,因此首先为 CMake 创建一个空目录。
另外,下载并解压 CEF 的“Minimal Distribution”二进制发行版。将解压后的文件夹重命名为“cef_arm64”以便快速识别,并将其放在您的项目根目录下。
我们还希望在 x86_64 主机上测试应用程序,因此请下载 Windows 64 位 最小分发二进制文件。将其解压到同一文件夹中,并将解压后的内容重命名为“cef_win64”。
接下来,让我们设置根 CMake 项目文件。将以下内容粘贴到空的 CMakeLists.txt 文件中
cmake_minimum_required(VERSION 3.19)
project(woa_cef LANGUAGES C CXX)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
if(${CMAKE_GENERATOR_PLATFORM} MATCHES "arm64")
set(CEF_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/cef_arm64 CACHE INTERNAL "")
else()
set(CEF_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/cef_win64 CACHE INTERNAL "")
endif()
add_subdirectory(${CEF_ROOT})
add_executable(
woa_cef
# NOTE: This property is needed to use WinMain as the entry point
WIN32
app.cpp
main.cpp
)
target_compile_features(
woa_cef
PRIVATE
cxx_std_17
)
target_link_directories(
woa_cef
PRIVATE
${CEF_ROOT}/Release
)
target_link_libraries(
woa_cef
PRIVATE
libcef
cef_sandbox
libcef_dll_wrapper
)
target_include_directories(
woa_cef
PRIVATE
${CEF_ROOT}
)
target_compile_definitions(
woa_cef
PRIVATE
_ITERATOR_DEBUG_LEVEL=0
)
set_property(
TARGET
woa_cef
PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>"
)
function(file_copy FILES BASE_DIR)
message(STATUS "FILES: ${FILES}")
foreach(FILE ${FILES})
get_filename_component(FILE_DIR ${FILE} DIRECTORY)
file(RELATIVE_PATH REL ${BASE_DIR} ${FILE_DIR})
get_filename_component(FILE_NAME ${FILE} NAME)
add_custom_target(
copy_${REL}_${FILE_NAME}
COMMAND ${CMAKE_COMMAND} -E echo ${CMAKE_CURRENT_BINARY_DIR}/$<IF:$<CONFIG:Debug>,Debug,Release>/${REL}
COMMAND ${CMAKE_COMMAND} -E copy ${FILE} ${CMAKE_CURRENT_BINARY_DIR}/$<IF:$<CONFIG:Debug>,Debug,Release>/${REL}/${FILE_NAME}
)
add_dependencies(woa_cef copy_${REL}_${FILE_NAME})
endforeach()
endfunction()
file(
GLOB_RECURSE
RESOURCE_FILES
"${CEF_ROOT}/Resources/*"
)
message(STATUS "CEF resources: ${RESOURCE_FILES}")
file_copy("${RESOURCE_FILES}" ${CEF_ROOT}/Resources)
file(
GLOB_RECURSE
DLLS
"${CEF_ROOT}/Release/*.dll"
)
message(STATUS "CEF dlls: ${DLLS}")
file_copy("${DLLS}" ${CEF_ROOT}/Release)
file(
GLOB_RECURSE
BINS
"${CEF_ROOT}/Release/*.bin"
)
message(STATUS "CEF bins: ${BINS}")
file_copy("${BINS}" ${CEF_ROOT}/Release)
项目文件创建了一个名为“woa_cef
”的新可执行文件,其中包含我们尚未创建的两个 C++ 源文件:main.cpp 和 app.cpp。
为了正确包含和链接 CEF,我们首先将 CEF_ROOT
变量设置为相应的目录。我们可以使用此变量在 ARM64、x86 和 x86_64 等目标架构之间切换。然后,我们将相应的 Release 文件夹添加为链接目录。
接下来,我们将链接所需的库,libcef、cef_sandbox
和 libcef_dll_wrapper
。我们将 DLL 包装器作为项目的一部分进行编译,前两个库是发行版中的预编译库。
所有 CEF 头文件都要求根发布路径在头文件包含路径中,因此我们将其添加为包含目录。我们针对 Microsoft Visual C++ (MSVC) 的最后两个编译设置更改是 _ITERATOR_DEBUG_LEVEL
和 MSVC_RUNTIME_LIBRARY
。这必须与 CEF 链接的设置匹配。
最后,我们将使用一个小的“file_copy
”函数来确保各种运行时工件在构建时正确复制到我们的二进制目录。我们正在复制几个 DLL(用于 WebGL 和其他功能)、区域设置数据、V8 运行时快照二进制文件和各种打包资源。
有了这个,我们就可以编写一些最少的代码来启动和运行。我们只需创建一个可执行文件,在 CEF 窗口中生成 Arm 开发人员主页。它在关闭时终止程序。
然后我们编写我们的应用程序接口。在“app.hpp”中,插入以下内容:
#pragma once
#include <include/cef_app.h>
class App : public CefApp, public CefBrowserProcessHandler
{
public:
App() = default;
CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler() override
{
return this;
}
void OnContextInitialized() override;
private:
IMPLEMENT_REFCOUNTING(App);
};
我们的 App
类是一个 CefApp
,它是 CEF 的 C API 的顶级 C++ 包装器。一个典型的 CEF 应用程序有一个浏览器进程和一个或多个渲染器进程,而 CefApp
可以处理这两个进程的回调。这反映了 Chromium 底层的 每个站点实例一个进程的模型:一个浏览器进程处理窗口创建和网络访问等一般任务,以及一个独立的沙盒渲染器进程,用于您访问的每个站点。渲染器进程负责渲染站点的 HTML/CSS 并运行 JavaScript。
为了方便起见,我们的 App
类还继承自 CefBrowserProcessHandler。CefBrowserProcessHandler
包含您可以在浏览器进程创建过程中的特定点需要执行工作时覆盖的方法。如果您需要在浏览器进程初始化之前或之后执行任何复杂的工作,您还可以创建一个单独的浏览器进程处理程序类,该类继承自 CefBrowserProcessHandler
。
在这里,我们只处理从 CefBrowserProcessHandler
继承的浏览器进程事件。在我们的类声明主体中,我们覆盖 GetBrowserProcessHandler
方法以将此类别声明为浏览器进程处理程序。我们还覆盖 OnContextInitialized
浏览器事件。在我们的 App
类声明的私有部分中,使用 IMPLEMENT_REFCOUNTING
便利宏来添加 CEF 在所有使用引用计数管理生命周期的类上所需的样板代码。
在“app.cpp”文件中,我们开始充实我们的应用程序。
#include "app.hpp"
// A CefClient is an event handler that can optionally handle events corresponding to the various CEF handlers
// CefDisplayHandler handles events related to the browser display state
// CefLifeSpanHandler handles events related to the browser window lifetime
// CefLoadHandler handles events related to the browser loading status
class Handler : public CefClient, public CefLifeSpanHandler
{
public:
Handler() = default;
// Overrides on CefClient indicate that this class implements the following handlers
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
void OnBeforeClose(CefRefPtr<CefBrowser> browser) override
{
CefQuitMessageLoop();
}
private:
IMPLEMENT_REFCOUNTING(Handler);
};
static CefRefPtr<Handler> handler{new Handler};
void App::OnContextInitialized()
{
CefBrowserSettings browser_settings;
CefWindowInfo window_info;
window_info.SetAsPopup(nullptr, "CEF Demo");
CefBrowserHost::CreateBrowser(window_info, handler, "https://developer.arm.com/", browser_settings, nullptr, nullptr);
}
虽然 App
类处理浏览器进程创建期间的事件,但我们必须定义一个内部 Handler
类来处理浏览器进程运行后触发的浏览器事件。
在这个最小的应用程序中,我们只处理单个 OnBeforeClose
事件以关闭 CEF 事件循环。否则,在用户关闭窗口后,我们的应用程序仍将在后台运行。
我们的演示只创建一个窗口,因此我们将创建一个单独的静态处理程序。在更复杂的场景中,您可以为每个窗口使用不同的处理程序,并协调它们以确定应用程序何时应该关闭。
我们将在 App
类的 OnContextInitialized
实现中生成窗口本身。此事件在 CEF 上下文初始化后立即在浏览器进程 UI 线程上发生。
保留默认的 CefBrowserSettings
,我们只更改 Windows 分类为 Win32 弹出窗口样式,标题为“CEF Demo”。然后,我们使用 CreateBrowser 生成一个带有我们指定设置的窗口来加载 Google 主页。
我们演示中最后一个缺失的元素是“main.cpp”文件,它需要初始化并启动 CEF 上下文和事件循环。“main.cpp”源文件的内容如下:
#include "app.hpp"
#include <Windows.h>
int APIENTRY WinMain(HINSTANCE instance, HINSTANCE previous_instance, LPTSTR args, int command_show)
{
CefMainArgs main_args{instance};
int exit_code = CefExecuteProcess(main_args, nullptr, nullptr);
if (exit_code >= 0)
{
return exit_code;
}
CefSettings settings;
CefRefPtr<App> app(new App);
CefInitialize(main_args, settings, app.get(), nullptr);
CefRunMessageLoop();
CefShutdown();
return 0;
}
我们使用 典型的 WinMain 入口点 用于不期望生成控制台的 GUI 应用程序。CefExecuteProcess
运行 CEF,CefInitialize
生成浏览器进程。通过将我们的 App
类注册为浏览器的 CefApp,我们接收到 OnContextInitialized
事件,以生成我们之前实现的浏览器窗口。最后,我们运行消息循环,并在消息循环完成时关闭所有内容。
我们将通过运行以下命令来验证我们编译的演示是否使用 ARM64 MSVC CMake 生成器:
<a>cmake -G "Visual Studio 16" -A arm64 -B build</a>
项目生成后,在 Visual Studio 2019 中打开 build\woa_cef.sln 并选择“生成”>“生成解决方案”。
由于此版本是针对 arm64 从 x86 机器交叉编译的,我们需要将其复制到 WoA 设备上进行测试。运行“woa_cef.exe”可执行文件应该会弹出一个加载了 Arm 开发人员主页的窗口。
<a>cmake -G "Visual Studio 16" -A x64 -B build</a>
如上所述打开解决方案文件,构建它,然后在 build/x64 目录中,您将找到一个可以运行的 woa_cef.exe 文件。
请注意,虽然浏览器导航会起作用,但典型的浏览器 UI(例如菜单栏和 URL 字段)是缺失的。通过右上角的 X 按钮关闭此窗口,以正确终止我们的应用程序。
后续步骤
要了解有关 CEF 通用架构和使用的更多信息,请参阅官方的 General Usage wiki。涉及命令行参数解析和多窗口场景的稍微更复杂的演示的源代码是 CEF 主仓库的一部分。
最后,请参阅 Microsoft Windows 10 on ARM 文档门户,获取一般信息、指南和参考资料。
Arm 上的 Windows 催生了新一代快速、轻量级的计算解决方案。现在您知道如何操作了,开始构建您自己的 Arm 上的原生 Windows 应用程序吧。