闽公网安备 35020302035485号
Windows应用开发有着较为丰富和多样的技术选型。C#/WPF 这种偏Native的闭源方案,目前开发人员相对比较小众了。C++/QT 的跨平台框架,C++对于GUI开发来说上手会更难。JavaScript/CEF/Electron 基于Chromium 的跨端框架,使用前端技术栈来构建桌面应用,性能会略低一些。总而言之各有所长,有一点可以确定的是,跨端能力成为了选型的重要考量。
在进一步探索和预演之后,通过Flutter的能力,可以很方便地将移动端的业务模块迁移至PC端,尽可能地实现一码多端,降低业务维护成本,以此为出发点,进行了Windows平台的接入。
Windows平台通过Plugin或FFI的方式提供相关能力,需要使用C++编写相关的平台代码。如果Plugin的代码可以自闭环,即所有C++代码都可以在Plugin内编写完成,那这个Plugin可以单独抽成一个Dart库。但是如果Plugin的代码需要复用其他Plugin或者主工程的C++代码,粗暴一点就是拷贝代码,或者通过CMakeLists来控制相互之间的依赖关系,通过find_package来完成头文件和库文件的链接。一旦依赖关系比较复杂,CMakeLists就会变得臃肿,依赖关系发生变化时,也会牵一发而动全身。随着系统复杂度的提升,开发人员的增加,模块之间相互耦合在一起,单一模块的修改都会影响到所有模块。

上述是一个登录模块的例子,Module 作为基类,定义了模块的一些生命周期方法。LoginModule是对外公开的业务接口,里面仅包含外部会用到的和登录业务相关的方法。LoginModuleImplV1类是登录逻辑的具体实现,不对外公开,里面的私有成员变量和方法对外部是隐藏的,同时实现了Module和LoginModule的接口。Provider用于创建和管理Module实例。
// LoginModuleProvider 通过宏自动生成 X_MODULE_PROVIDER_DEFINE_SINGLE(LoginModule, MIN_VERSION, MAX_VERSION); // LoginModuleImplV1Provider 通过宏自动生成 X_MODULE_DEFINE_SECONDARY_PROVIDER(LoginModuleImplV1, LoginModule);XModule的模版开发方式,会增加很多类文件,为了方便,通过宏来控制Provider类的自动生成。其中MIN_VERSION和MAX_VERSION是该Module接口能支持的最小和最大的版本范围,可以限制后期dll插件化加载时,不加载在版本之外的dll,避免产生冲突和错误,目前Provider的GetVersion使用的是MAX_VERSION。
// 由 X_MODULE_DEFINE_SECONDARY_PROVIDER 宏自动生成
class DLLEXPORT LoginModuleImplV1Provider : public LoginModuleProvider {
public:
LoginModule* Create() const {
LoginModuleImplV1* p = new LoginModuleImplV1();
((Module*)p)->OnCreate();
return p;
}
};
LoginModuleImplV1Provider可以通过调用Create方法拿到对应的LoginModuleImplV1实例。x_module::ModuleCenter* module_center = x_module::ModuleCenter::GetInstance(); module_center->AcceptProviderType<LoginModuleProvider>();ModuleCenter是所有Module的管理类,先通过x_module::ModuleCenter::GetInstance()拿到ModuleCenter的实例,它是一个跨dll的单例。然后要用之前的LoginModuleProvider去注册一个Module类型到ModuleCenter中。LoginModuleProvider中定义了支持的Module类型,以及最小版本和最大版本,如果后续扫描到的dll中提供的对应类型的Provider中GetVersion返回的值不在最大版本和最小版本之间,那么就不会被允许加载进来。
module_center->AddProvider(new LoginModuleImplV1Provider());通过这种方式,可以将LoginModuleImplV1Provider注册到ModuleCenter中,然后创建并管理LoginModuleImplV1的实例。但是这样就显式地依赖了LoginModuleImplV1Provider,违反了前面说过的依赖倒置原则,对开闭原则也不友好,因为这样就只能通过修改代码来实现扩展了。
#include <x_module/connector.h>
#include "login_module/login_module_impl.h"
X_MODULE_CONNECTOR
bool XModuleConnect(x_module::Owner& owner) {
owner.add(new LoginModuleImplV1Provider());
return true;
}
为了在加载dll时,来注册Provider,增加了一个connector.cc,添加一个XModuleConnect方法,让dll被加载之后,能够找到XModuleConnect这个符号方法,并进行调用,在XModuleConnect被调用的时候,会调用AddProvider将Provider进行注册。std::string path = GetProgramDir(); module_center->Install(path, "login_module");由于目前login_module.dll是直接放在exe同目录的,所以这里直接获取了一下exe绝对路径,然后调用Install方法,将路径和dll名login_module传入进去,这样就完成了注册。
auto* p_login_module = module_center->ModuleFromProtocol<LoginModule, LoginModuleProvider>();
if (p_login_module == nullptr) {
(*move_result)->Error("-100", "login module 为空");
return;
}
bool islogin = p_login_module->IsLogin();
在使用时,只需要LoginModule和LoginModuleProvider这两个抽象,就能获取真实的LoginModuleImplV1这个实例,调用方仅需关心LoginModule所公开的API,完全屏蔽了对实现的依赖。后续底层扩展成了LoginModuleImplV2,只要LoginModule的公开API不变,对上层是无感知的。这种方式完全遵循了前面提到的设计原则,对团队内的多人维护以及后续的更新迭代都带来了稳定的保障。├── CMakeLists.txt ├── LICENSE ├── cmake │ └── config.cmake.in ├── include │ └── fish_ffi_module.h ├── src │ ├── connector.cc │ ├── fish_ffi_module_impl_v1.cc │ └── fish_ffi_module_impl_v1.h ├── vcpkg-configuration.json └── vcpkg.jsonvcpkg-configuration.json配置了私有源,后面会讲到。vcpkg.json文件,声明了当前库所依赖的其他库,即vcpkg的依赖清单,其中"dependencies"字段声明了所使用的依赖名称。
{
"name": "fish-ffi-module",
"version": "1.0.0",
"description": "A fish-ffi module based on fish-ffi-sdk.",
"homepage": "",
"dependencies": [
"fish-ffi-sdk",
"x-module",
"flutter-sdk"
]
}
CMake工程最重要的就是CMakeLists文件了,里面配置了编译相关的设置,添加了相关的注释来帮助理解。cmake_minimum_required(VERSION 3.15)
# 仓库版本常量,升级时修改
set(FISH_FFI_MODULE_VERSION "1.0.0")
project(fish-ffi-module
VERSION ${FISH_FFI_MODULE_VERSION}
DESCRIPTION "A fish-ffi module based on fish-ffi-sdk."
HOMEPAGE_URL ""
LANGUAGES CXX)
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
# vcpkg清单中添加依赖之后,通过find_package就能找到
find_package(fish-ffi-sdk CONFIG REQUIRED)
find_package(flutter-sdk CONFIG REQUIRED)
find_package(x-module CONFIG REQUIRED)
# configure_package_config_file 生成config要用到
include(CMakePackageConfigHelpers)
# install 安装要用到
include(GNUInstallDirs)
# 当前库的头文件和源文件
aux_source_directory(include HEADER_LIST)
aux_source_directory(src SRC_LIST)
add_library(fish-ffi-module SHARED
${HEADER_LIST}
${SRC_LIST}
)
# 设置别名
add_library(fish-ffi-module::fish-ffi-module ALIAS fish-ffi-module)
# 设置动态库导出宏,PRIVATE为编译时,INTERFACE为运行时
if (BUILD_SHARED_LIBS AND WIN32)
target_compile_definitions(fish-ffi-module
PRIVATE "FISH_FFI_MODULE_EXPORT=__declspec(dllexport)"
INTERFACE "FISH_FFI_MODULE_EXPORT=__declspec(dllimport)")
endif ()
target_compile_features(fish-ffi-module PUBLIC cxx_std_17)
# 添加头文件
target_include_directories(fish-ffi-module PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include/>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
# 链接库文件
target_link_libraries(fish-ffi-module PRIVATE fish-ffi-sdk::fish-ffi-sdk)
target_link_libraries(fish-ffi-module PRIVATE flutter-sdk::flutter-sdk)
target_link_libraries(fish-ffi-module PRIVATE x-module::x-module)
# 基于config.cmake.in的模板生成xxx-config.cmake的文件
configure_package_config_file(
cmake/config.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/fish-ffi-module-config.cmake
INSTALL_DESTINATION ${CMAKE_INSTALL_DATADIR}/fish-ffi-module
NO_SET_AND_CHECK_MACRO)
# 生成xx-config-version.cmake文件
write_basic_package_version_file(
${CMAKE_CURRENT_BINARY_DIR}/fish-ffi-module-config-version.cmake
VERSION ${FISH_FFI_MODULE_VERSION}
COMPATIBILITY SameMajorVersion)
# 将上面生成的两个config文件,安装到share/fish-ffi-module下
install(
FILES
${CMAKE_CURRENT_BINARY_DIR}/fish-ffi-module-config.cmake
${CMAKE_CURRENT_BINARY_DIR}/fish-ffi-module-config-version.cmake
DESTINATION
${CMAKE_INSTALL_DATADIR}/fish-ffi-module)
# 安装头文件
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
# install target
install(TARGETS fish-ffi-module
EXPORT fish-ffi-module-targets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
# 导出
install(EXPORT fish-ffi-module-targets
NAMESPACE fish-ffi-module::
DESTINATION ${CMAKE_INSTALL_DATADIR}/fish-ffi-module)
这里面最重要的一点是配置xx-config.cmake和xx-config-version.cmake的生成,vcpkg会在源码首次拉下来的时候进行编译,编译完在相应库的share目录生成上述两个文件,并且在CMake配置阶段执行,这样在使用find_package的时候就能获取到这个库以及对应版本号。总结一下就是,vcpkg帮助完成了代码的下载、编译和配置,然后就可以方便的链接三方库了。. ├── ports │ ├── fish-ffi-module │ │ ├── portfile.cmake │ │ └── vcpkg.json │ └── x-module │ ├── portfile.cmake │ └── vcpkg.json ├── versions │ ├── f- │ │ └── fish-ffi-module.json │ └── x- │ │ └── x-module.json │ └──baseline.json └── LICENSEvcpkg里面对依赖库的定义叫port,这里定义了两个port,分别是fish-ffi-module和x-module。其中的文件说明如下:
{
"default-registry": {
"kind": "git",
"repository": "https://github.com/microsoft/vcpkg",
"baseline": "f4b262b259145adb2ab0116a390b08642489d32b"
},
"registries": [
{
"kind": "git",
"repository": "xxx.git",
"baseline": "1ad54586a5a2fadb8c44d3f8f47754e849fc5a38",
"packages": [ "x-module", "fish-ffi-sdk", "fish-ffi-module"]
}
]
}
在versions文件夹下还有一个baseline.json的文件,这个文件主要是设置基线用的,不像其他的依赖管理工具,vcpkg主要是通过这个基线来设置当前所使用的版本号的。vcpkg可以胜任依赖管理的相关工作,综上所述只是一个简单使用,相比其他平台的依赖管理工具略显繁琐,除此之外还有很多其他能力,需要到vcpkg.io的官方文档里面探索了。