Flutter编写win plugin调用第三方exe后台运行
一、目的
本篇文章用于记录使用Flutter编写 win 插件简单调用exe文件并在后台运行。
二、背景
继上篇文章:go打包aar,flutter调用aar 之后,有大佬提出如何调用exe文件,网上调研一番后写下该笔记,用于记录flutter编写插件如何与win打交道,如何调用exe文件。
我手上的资源有一个http exe文件,使用flutter调用该exe并保持后台运行。
本人非C++开发成员,对C++一知半解,如果代码有不正确的地方,请指教。
参考文章/项目:
flutter_barcode_sdk:生成二维码的flutter plugin项目,参考其中的win插件代码,个人觉得写的挺不错的,开发插件很有参考价值。
flutter-desktop-embedding:官方的flutter plugin项目,还挺不错。
三、流程
问题:
-
flutter plugin调用windows中方法如何传参:普通参数与Map参数如何获取。
-
如何调用exe文件并且后台保活。
问题一:flutter plugin调用windows中方法如何传参:普通参数与Map参数如何获取。
介绍plugin项目文件
先创建一个flutter plugin项目:
flutter create --template=plugin --platform=windows win_plugin
dart三文件
在lib目录下有三个文件:
kernel_plugin_platform_interface.dart --抽象类,需要与第三方平台交互的方法都要再次写上
在下面增加三个方法:
// plugin项目自带方法
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
// 普通的加法方法
Future<int?> sum(int num1, int num2) {
throw UnimplementedError('platformVersion() has not been implemented.');
}
// 传递普通参数,调用exe文件
Future<String?> startKernel(String cmd, String args) {
throw UnimplementedError('platformVersion() has not been implemented.');
}
// 传递map参数,调用exe文件
Future<String?> startKernelMap(Map<String, Object> param) {
throw UnimplementedError('platformVersion() has not been implemented.');
}
kernel_plugin.dart -- kernel_plugin_platform_interface 的实现类,用于约定调用第三方平台方法入口。
其中 getPlatformVersion,startKernel,startKernelMap,sum 字符串在第三方代码也必须一模一样,相当于用于约定交互的key
@override
Future<String?> getPlatformVersion() async {
final version =
await methodChannel.invokeMethod<String>('getPlatformVersion');
return version;
}
@override
Future<int?> sum(int num1, int num2) async {
final result = await methodChannel.invokeMethod<int>('sum', [num1, num2]);
return result;
}
@override
Future<String?> startKernel(String cmd, String args) async {
final result =
await methodChannel.invokeMethod<String>('startKernel', [cmd, args]);
return result;
}
@override
Future<String?> startKernelMap(Map<String, Object> param) async {
final result =
await methodChannel.invokeMethod<String>('startKernelMap', param);
return result;
}
kernel_plugin_method_channel.dart -- 用于提供给加载该插件的项目调用的方法,我们调用插件就是调用该类中的方法。
class KernelPlugin {
Future<String?> getPlatformVersion() {
return KernelPluginPlatform.instance.getPlatformVersion();
}
Future<int?> sum(int num1, int num2) {
return KernelPluginPlatform.instance.sum(num1, num2);
}
Future<String?> startKernel(String cmd, String args) {
return KernelPluginPlatform.instance.startKernel(cmd, args);
}
Future<String?> startKernelMap(Map<String, Object> param) async {
return KernelPluginPlatform.instance.startKernelMap(param);
}
}
win中文件
我们需要关注的只有 kernel_plugin.cpp文件,因为该文件是用于实现我们与dart交互的文件
我们主要关注的方法就这一个HandleMethodCall,该方法就是用于我们实现dart方法的入口,该文件用vscode打开后会报错,别理它,后续说明如何正确编辑该c++项目。
void KernelPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
{
if (method_call.method_name().compare("getPlatformVersion") == 0)
{
std::ostringstream version_stream;
version_stream << "Windows ";
if (IsWindows10OrGreater())
{
version_stream << "10+";
}
else if (IsWindows8OrGreater())
{
version_stream << "8";
}
else if (IsWindows7OrGreater())
{
version_stream << "7";
}
result->Success(flutter::EncodableValue(version_stream.str()));
}
else
{
result->NotImplemented();
}
}
普通参数与Map参数如何获取。
重要:创建项目后,先进入example直接build一遍项目,将example/build/windows 文件夹创建出来,目的就是创建出 .sln 文件,然后用 visualstudio2022 打开该文件,这是该c++项目的正确打开方式。
用vs2022打开该文件,现在编辑cpp文件就不会报错,并且可以正常进行代码提示与编辑。
普通参数获取
实现dart中的该方法:
@override
Future<int?> sum(int num1, int num2) async {
final result = await methodChannel.invokeMethod<int>('sum', [num1, num2]);
return result;
}
其中约定的 'sum' 字符串一定不能弄错,其中的步骤已在代码中标注,如果您与我一样对c++一知半解的话,可照猫画虎即可。
else if (method_call.method_name().compare("sum") == 0)
{
// 获取参数数组
const auto* arguments = std::get_if<flutter::EncodableList>(method_call.arguments());
if (!arguments)
{
result->Error("no param");
return;
}
// 根据索引获取对应的参数
auto num1a = arguments->at(0);
auto num2a = arguments->at(1);
// 将变量转化为对应的类型
int num1 = get<int>(num1a);
int num2 = get<int>(num2a);
// 逻辑计算相加,并返回
int num3 = num1 + num2;
result->Success(flutter::EncodableValue(num3));
}
获取map参数
我们将实现该方法,该方法传递的是一个map参数。
@override
Future<String?> startKernelMap(Map<String, Object> param) async {
final result =
await methodChannel.invokeMethod<String>('startKernelMap', param);
return result;
}
其中约定 'startKernelMap' 别写错,其中的步骤已在代码中标注,如果您与我一样对c++一知半解的话,可照猫画虎即可。
else if (method_call.method_name().compare("startKernelMap") == 0) {
// 获取对应的map参数
auto* arguments = get_if<flutter::EncodableMap>(method_call.arguments());
if (!arguments)
{
cout << "error err" << endl;
cout << arguments << endl;
result->Error("arguments error");
return;
}
// 在map中根据对应的key获取对应的指针对象,使用的话 *path,*args 即可获取对应的值
auto* path = std::get_if<string>(&(arguments->find(flutter::EncodableValue("cmd"))->second));
auto* args = std::get_if<string>(&(arguments->find(flutter::EncodableValue("args"))->second));
cout << *path << endl;
cout << *args << endl;
// 此处是创建线程调用exe,暂时别管,后面说明
thread t1(startMyKernel, *path, *args);
t1.detach();
result->Success(flutter::EncodableValue("startKernelMap"));
}
问题二:如何调用exe文件并且后台保活
查询了下资料,暂时选择了两种调用方式
简单的就用 system("C:/kernel.exe"),该方法是调用cmd来执行kernel.exe方法,缺点是打包后会有一个cmd窗口弹出来,试用以下两种参数想让窗口在后台运行不弹出,但是不成功,不清楚为什么。
system("start /b C:/kernel.exe") // 没报错,但是没有实现cmd窗口隐藏
system("hiden C:/kernel.exe") // 报错没有 hiden 命令,我用的是win10,不清楚为什么没有该命令
复杂可控的可使用 CreateProcess() ,该方法用于创建一个进程,通过设置一些参数来进行管理与控制该进程,并且可以做到隐藏cmd运行后台窗口。
void startMyKernel(string exePath, string args) {
// /K 参数用于保持后台运行
std::string cmd = "/K " + exePath + " " + args;
// 窗口
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
// 将string转为wchar_t*
int len = MultiByteToWideChar(CP_UTF8, 0, cmd.c_str(), -1, NULL, 0);
wchar_t* wstr = new wchar_t[len];
MultiByteToWideChar(CP_UTF8, 0, cmd.c_str(), -1, wstr, len);
std::wcout << wstr;
// 可用下面命令直接替换wstr的位置
// TEXT("D:\\project\\go\\event_shop_kernel\\output\\windows\\kernel.exe api --port=6905 --mode=test --dbPath=C:\\Users\\Administrator\\Documents\\event_shop\\databases\\todo_shop.db --logPath=C:\\Users\\Administrator\\Documents\\event_shop\\logs")
// bResult用于判断创建进程是否成功
BOOL bResult;
bResult = CreateProcess(TEXT("C:\\Windows\\System32\\cmd.exe"), wstr, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
// 检测是否成功启动,未启动则弹窗错误信息
if (!bResult)
{
// CreateProcess方法出现错误
LPVOID lpMsgBuf;
DWORD dw = GetLastError();
cout << dw << endl;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf,
0, NULL);
// 弹窗错误信息
MessageBox(NULL, (LPCTSTR)lpMsgBuf, TEXT("Error"), MB_OK | MB_ICONERROR);
LocalFree(lpMsgBuf);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
// 释放内存
delete[] wstr;
wstr = NULL;
}
上述是执行是的方法,如果您需要将exe文件打包进你的项目,需要增加如下配置:
把exe文件放到windows/bin中。
修改CMakeLists.txt文件最后几行,将/bin目录下的文件打包进项目
# List of absolute paths to libraries that should be bundled with the plugin.
# This list could contain prebuilt libraries, or libraries created by an
# external build triggered from this build file.
set(kernel_plugin_bundled_libraries
"${PROJECT_SOURCE_DIR}/bin/"
PARENT_SCOPE
)
打包后的结果:example\build\windows\runner\Release
kernel.exe会出现在根目录中
运行后结果:
总体代码:
因为一些特殊原因,暂时贴代码,如果您需要github地址,可留言下,我后面整理贴上
kernel_plugin.cpp
c++的核心代码
#include "kernel_plugin.h"
// This must be included before many other Windows headers.
#include <windows.h>
// For getPlatformVersion; remove unless needed for your plugin implementation.
#include <VersionHelpers.h>
#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <string>
#include <thread>
#include <iostream>
#include <memory>
#include <sstream>
using namespace std;
const char kMenuSetMethod[] = "startKernel";
const char kMenuSetMethodMap[] = "startKernelMap";
namespace kernel_plugin
{
using flutter::EncodableMap;
using flutter::EncodableValue;
void startMyKernel(string cmd, string args);
// static
void KernelPlugin::RegisterWithRegistrar(
flutter::PluginRegistrarWindows* registrar)
{
auto channel =
std::make_unique<flutter::MethodChannel<flutter::EncodableValue>>(
registrar->messenger(), "kernel_plugin",
&flutter::StandardMethodCodec::GetInstance());
auto plugin = std::make_unique<KernelPlugin>();
channel->SetMethodCallHandler(
[plugin_pointer = plugin.get()](const auto& call, auto result)
{
plugin_pointer->HandleMethodCall(call, std::move(result));
});
registrar->AddPlugin(std::move(plugin));
}
KernelPlugin::KernelPlugin() {}
KernelPlugin::~KernelPlugin() {}
void KernelPlugin::HandleMethodCall(
const flutter::MethodCall<flutter::EncodableValue>& method_call,
std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result)
{
if (method_call.method_name().compare("getPlatformVersion") == 0)
{
std::ostringstream version_stream;
version_stream << "Windows ";
if (IsWindows10OrGreater())
{
version_stream << "10+";
}
else if (IsWindows8OrGreater())
{
version_stream << "8";
}
else if (IsWindows7OrGreater())
{
version_stream << "7";
}
result->Success(flutter::EncodableValue(version_stream.str()));
}
else if (method_call.method_name().compare("sum") == 0)
{
const auto* arguments = std::get_if<flutter::EncodableList>(method_call.arguments());
if (!arguments)
{
result->Error("no param");
return;
}
auto num1a = arguments->at(0);
auto num2a = arguments->at(1);
int num1 = get<int>(num1a);
int num2 = get<int>(num2a);
int num3 = num1 + num2;
result->Success(flutter::EncodableValue(num3));
}
else if (method_call.method_name().compare(kMenuSetMethod) == 0) {
const auto* arguments = std::get_if<flutter::EncodableList>(method_call.arguments());
if (!arguments)
{
result->Error("arguments error");
return;
}
auto pathStr = arguments->at(0);
string path = get<string>(pathStr);
auto argsStr = arguments->at(1);
string args = get<string>(argsStr);
thread t(startMyKernel, path, args);
t.detach();
result->Success(flutter::EncodableValue("startKernel"));
}
else if (method_call.method_name().compare(kMenuSetMethodMap) == 0) {
auto* arguments = get_if<flutter::EncodableMap>(method_call.arguments());
if (!arguments)
{
cout << "error err" << endl;
cout << arguments << endl;
result->Error("arguments error");
return;
}
auto* path = std::get_if<string>(&(arguments->find(flutter::EncodableValue("cmd"))->second));
auto* args = std::get_if<string>(&(arguments->find(flutter::EncodableValue("args"))->second));
cout << *path << endl;
cout << *args << endl;
thread t1(startMyKernel, *path, *args);
t1.detach();
result->Success(flutter::EncodableValue("startKernelMap"));
}
else
{
result->NotImplemented();
}
}
void startMyKernel(string exePath, string args) {
// /K 参数用于保持后台运行
std::string cmd = "/K " + exePath + " " + args;
// 窗口
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
si.dwFlags = STARTF_USESHOWWINDOW;
si.wShowWindow = SW_HIDE;
// 将string转为wchar_t*
int len = MultiByteToWideChar(CP_UTF8, 0, cmd.c_str(), -1, NULL, 0);
wchar_t* wstr = new wchar_t[len];
MultiByteToWideChar(CP_UTF8, 0, cmd.c_str(), -1, wstr, len);
std::wcout << wstr;
// 可用下面命令直接替换wstr的位置
// TEXT("D:\\project\\go\\event_shop_kernel\\output\\windows\\kernel.exe api --port=6905 --mode=test --dbPath=C:\\Users\\Administrator\\Documents\\event_shop\\databases\\todo_shop.db --logPath=C:\\Users\\Administrator\\Documents\\event_shop\\logs")
// bResult用于判断创建进程是否成功
BOOL bResult;
bResult = CreateProcess(TEXT("C:\\Windows\\System32\\cmd.exe"), wstr, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
// 检测是否成功启动,未启动则弹窗错误信息
if (!bResult)
{
// CreateProcess方法出现错误
LPVOID lpMsgBuf;
DWORD dw = GetLastError();
cout << dw << endl;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
dw,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&lpMsgBuf,
0, NULL);
// 弹窗错误信息
MessageBox(NULL, (LPCTSTR)lpMsgBuf, TEXT("Error"), MB_OK | MB_ICONERROR);
LocalFree(lpMsgBuf);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
// 释放内存
delete[] wstr;
wstr = NULL;
}
} // namespace kernel_plugin
main.dart
flutter中调用插件的核心代码
Future<Dir> initPath() async {
var di = await getApplicationDocumentsDirectory();
String dbPath = path.join(di.path, "event_shop", "databases");
String logPath = path.join(di.path, "event_shop", "logs");
var dbDir = Directory(dbPath);
var logDir = Directory(logPath);
dbDir.createSync(recursive: true);
logDir.createSync(recursive: true);
dbPath = path.join(dbPath, "xxxx.db");
Dir dir = Dir(dbPath: dbPath, logPath: logPath);
return dir;
}
void startKernel(RootIsolateToken rootIsolateToken) async {
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
final kernelPlugin = KernelPlugin();
var dir = await initPath();
String args =
"api --port=6905 --mode=test --dbPath=${dir.dbPath} --logPath=${dir.logPath}";
String kernelPath =
"D:\\project\\flutter\\kernel_plugin\\example\\build\\windows\\runner\\Release\\kernel.exe";
await kernelPlugin.startKernel(kernelPath, args);
}
void startKernelMap(RootIsolateToken rootIsolateToken) async {
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
final kernelPlugin = KernelPlugin();
var dir = await initPath();
String args =
"api --port=6905 --mode=test --dbPath=${dir.dbPath} --logPath=${dir.logPath}";
// String kernelPath =
// "D:\\project\\go\\event_shop_kernel\\output\\windows\\kernel.exe";
String kernelPath =
"D:\\project\\flutter\\kernel_plugin\\example\\build\\windows\\runner\\Release\\kernel.exe";
Map<String, Object> param = {"cmd": kernelPath, "args": args};
await kernelPlugin.startKernelMap(param);
}
四、结论
调用exe并不难,感觉有些困难的是获取参数那块,毕竟对c++不熟悉。
plugin的开发资料找起来比较麻烦,没有比较细致的资料,更多的还是看github中他人的项目如何写的。
转载自:https://juejin.cn/post/7215142807861444645