likes
comments
collection
share

Flutter编写win plugin调用第三方exe后台运行

作者站长头像
站长
· 阅读数 17

一、目的

本篇文章用于记录使用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项目,还挺不错。

三、流程

问题:

  1. flutter plugin调用windows中方法如何传参:普通参数与Map参数如何获取。

  2. 如何调用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中文件

Flutter编写win plugin调用第三方exe后台运行

我们需要关注的只有 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++项目的正确打开方式。

Flutter编写win plugin调用第三方exe后台运行

用vs2022打开该文件,现在编辑cpp文件就不会报错,并且可以正常进行代码提示与编辑。

Flutter编写win plugin调用第三方exe后台运行

普通参数获取

实现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中。

Flutter编写win plugin调用第三方exe后台运行

修改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

Flutter编写win plugin调用第三方exe后台运行

kernel.exe会出现在根目录中

运行后结果:

Flutter编写win plugin调用第三方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中他人的项目如何写的。