feat: move native communication to FFI (#1457)

This commit is contained in:
Levi authored and GitHub committed 2026-09-05 20:18:42 +08:00
1 parent 177c225f16
commit 39cd9034cd
86 files changed
+4558 -886

No files matched your search

@@ -0,0 +1,64 @@
class ApiServerState {
const ApiServerState({
required this.enabled,
required this.running,
required this.network,
required this.address,
required this.runningPort,
required this.pendingApply,
required this.lastError,
});
final bool enabled;
final bool running;
final String network;
final String address;
final int runningPort;
final bool pendingApply;
final String lastError;
factory ApiServerState.fromJson(Map<String, dynamic> json) {
return ApiServerState(
enabled: json['enabled'] as bool? ?? false,
running: json['running'] as bool? ?? false,
network: json['network'] as String? ?? '',
address: json['address'] as String? ?? '',
runningPort: json['runningPort'] as int? ?? 0,
pendingApply: json['pendingApply'] as bool? ?? false,
lastError: json['lastError'] as String? ?? '',
);
}
String runningAddress() {
if (!running) return '';
if (network == 'unix') return address;
final separator = address.lastIndexOf(':');
final host = separator < 0 ? address : address.substring(0, separator);
return '$host:$runningPort';
}
}
class ApiServerOperationResult {
const ApiServerOperationResult({required this.state, required this.error});
final ApiServerState state;
final String error;
factory ApiServerOperationResult.fromJson(Map<String, dynamic> json) {
final rawState = json['state'] as Map<String, dynamic>?;
return ApiServerOperationResult(
state: rawState == null
? const ApiServerState(
enabled: false,
running: false,
network: '',
address: '',
runningPort: 0,
pendingApply: true,
lastError: '',
)
: ApiServerState.fromJson(rawState),
error: json['error'] as String? ?? '',
);
}
}
@@ -1,12 +1,24 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'api_server_state.dart';
import 'libgopeed_interface.dart';
import 'start_config.dart';
import 'task_event.dart';
class LibgopeedChannel implements LibgopeedInterface {
static const _channel = MethodChannel('gopeed.com/libgopeed');
final _taskEvents = StreamController<TaskEvent>.broadcast();
LibgopeedChannel() {
_channel.setMethodCallHandler((call) async {
if (call.method != 'taskEvent') return;
final payload = jsonDecode(call.arguments as String) as Map<String, dynamic>;
_taskEvents.add(TaskEvent.fromJson(payload));
});
}
@override
Future<int> start(StartConfig cfg) async {
@@ -18,4 +30,40 @@ class LibgopeedChannel implements LibgopeedInterface {
Future<void> stop() async {
return await _channel.invokeMethod('stop');
}
Future<ApiServerOperationResult> _apiServerOperation(String method) async {
final payload = await _channel.invokeMethod<String>(method) ?? '';
return ApiServerOperationResult.fromJson(jsonDecode(payload) as Map<String, dynamic>);
}
@override
Future<ApiServerOperationResult> getApiServerState() => _apiServerOperation('getApiServerState');
@override
Future<ApiServerOperationResult> startApiServer() => _apiServerOperation('startApiServer');
@override
Future<ApiServerOperationResult> stopApiServer() => _apiServerOperation('stopApiServer');
@override
Future<ApiServerOperationResult> restartApiServer() => _apiServerOperation('restartApiServer');
@override
Future<String> invoke(String method, String path, {String query = '', String body = ''}) async {
final result = await _channel.invokeMethod<String>('invoke', {
'method': method,
'path': path,
'query': query,
'body': body,
});
return result ?? '';
}
@override
Stream<TaskEvent> get taskEvents => _taskEvents.stream;
@override
Future<void> subscribeTaskEvents(Set<TaskEventType> events) {
return _channel.invokeMethod<void>('subscribeTaskEvents', {'mask': events.mask});
}
}
+77 -5
View File
@@ -5,24 +5,47 @@ import 'dart:ffi';
import 'package:ffi/ffi.dart';
import '../ffi/libgopeed_bind.dart';
import '../ffi/libgopeed_worker.dart';
import 'api_server_state.dart';
import 'libgopeed_interface.dart';
import 'start_config.dart';
import 'task_event.dart';
class LibgopeedFFi implements LibgopeedInterface {
late LibgopeedBind _libgopeed;
final _taskEvents = StreamController<TaskEvent>.broadcast();
final _worker = LibgopeedWorker();
late final NativeCallable<Void Function(Pointer<Char>)> _taskEventCallback;
LibgopeedFFi(LibgopeedBind libgopeed) {
_libgopeed = libgopeed;
_taskEventCallback = NativeCallable<Void Function(Pointer<Char>)>.listener(_onTaskEvent);
}
void _onTaskEvent(Pointer<Char> payload) {
try {
final decoded = jsonDecode(payload.cast<Utf8>().toDartString()) as Map<String, dynamic>;
_taskEvents.add(TaskEvent.fromJson(decoded));
} finally {
_libgopeed.FreeCString(payload);
}
}
@override
Future<int> start(StartConfig cfg) {
var completer = Completer<int>();
var result = _libgopeed.Start(jsonEncode(cfg).toNativeUtf8().cast());
if (result.r1 != nullptr) {
completer.completeError(Exception(result.r1.cast<Utf8>().toDartString()));
} else {
completer.complete(result.r0);
final cfgPtr = jsonEncode(cfg).toNativeUtf8();
try {
final result = _libgopeed.Start(cfgPtr.cast());
if (result.r1 != nullptr) {
final message = result.r1.cast<Utf8>().toDartString();
_libgopeed.FreeCString(result.r1);
completer.completeError(Exception(message));
} else {
completer.complete(result.r0);
}
} finally {
malloc.free(cfgPtr);
}
return completer.future;
}
@@ -34,4 +57,53 @@ class LibgopeedFFi implements LibgopeedInterface {
completer.complete();
return completer.future;
}
Future<ApiServerOperationResult> _apiServerOperation(String operation) async {
final payload = await _worker.apiServer(operation);
return ApiServerOperationResult.fromJson(jsonDecode(payload) as Map<String, dynamic>);
}
@override
Future<ApiServerOperationResult> getApiServerState() => _apiServerOperation('get');
@override
Future<ApiServerOperationResult> startApiServer() => _apiServerOperation('start');
@override
Future<ApiServerOperationResult> stopApiServer() => _apiServerOperation('stop');
@override
Future<ApiServerOperationResult> restartApiServer() => _apiServerOperation('restart');
@override
Future<String> invoke(String method, String path, {String query = '', String body = ''}) async {
final methodPtr = method.toNativeUtf8();
final pathPtr = path.toNativeUtf8();
final queryPtr = query.toNativeUtf8();
final bodyPtr = body.toNativeUtf8();
Pointer<Char>? resultPtr;
try {
resultPtr = _libgopeed.Invoke(methodPtr.cast(), pathPtr.cast(), queryPtr.cast(), bodyPtr.cast());
if (resultPtr == nullptr) {
throw StateError('Gopeed Invoke returned a null response');
}
return resultPtr.cast<Utf8>().toDartString();
} finally {
malloc.free(methodPtr);
malloc.free(pathPtr);
malloc.free(queryPtr);
malloc.free(bodyPtr);
if (resultPtr != null && resultPtr != nullptr) {
_libgopeed.FreeCString(resultPtr);
}
}
}
@override
Stream<TaskEvent> get taskEvents => _taskEvents.stream;
@override
Future<void> subscribeTaskEvents(Set<TaskEventType> events) async {
_libgopeed.SubscribeTaskEvents(events.mask, events.isEmpty ? 0 : _taskEventCallback.nativeFunction.address);
}
}
@@ -1,7 +1,23 @@
import 'api_server_state.dart';
import 'start_config.dart';
import 'task_event.dart';
abstract class LibgopeedInterface {
Future<int> start(StartConfig cfg);
Future<void> stop();
Future<ApiServerOperationResult> getApiServerState();
Future<ApiServerOperationResult> startApiServer();
Future<ApiServerOperationResult> stopApiServer();
Future<ApiServerOperationResult> restartApiServer();
Future<String> invoke(String method, String path, {String query = '', String body = ''});
Stream<TaskEvent> get taskEvents;
Future<void> subscribeTaskEvents(Set<TaskEventType> events);
}
@@ -19,6 +19,7 @@ class WebViewRpcConfig {
class StartConfig {
late String network;
late String address;
bool apiEnable = false;
late String storage;
late String storageDir;
late int refreshInterval;
@@ -22,6 +22,7 @@ Map<String, dynamic> _$WebViewRpcConfigToJson(WebViewRpcConfig instance) =>
StartConfig _$StartConfigFromJson(Map<String, dynamic> json) => StartConfig()
..network = json['network'] as String
..address = json['address'] as String
..apiEnable = json['apiEnable'] as bool
..storage = json['storage'] as String
..storageDir = json['storageDir'] as String
..refreshInterval = (json['refreshInterval'] as num).toInt()
@@ -36,6 +37,7 @@ Map<String, dynamic> _$StartConfigToJson(StartConfig instance) =>
<String, dynamic>{
'network': instance.network,
'address': instance.address,
'apiEnable': instance.apiEnable,
'storage': instance.storage,
'storageDir': instance.storageDir,
'refreshInterval': instance.refreshInterval,
@@ -0,0 +1,38 @@
enum TaskEventType {
done(1, 'task.done'),
error(2, 'task.error');
const TaskEventType(this.mask, this.wireName);
final int mask;
final String wireName;
static TaskEventType fromWireName(String value) {
return values.firstWhere(
(type) => type.wireName == value,
orElse: () => throw FormatException('Unknown task event type: $value'),
);
}
}
extension TaskEventTypeSet on Set<TaskEventType> {
int get mask => fold(0, (value, type) => value | type.mask);
}
class TaskEvent {
const TaskEvent({required this.type, required this.taskId, required this.name, this.error});
final TaskEventType type;
final String taskId;
final String name;
final String? error;
factory TaskEvent.fromJson(Map<String, dynamic> json) {
return TaskEvent(
type: TaskEventType.fromWireName(json['type'] as String),
taskId: json['taskId'] as String,
name: json['name'] as String? ?? '',
error: json['error'] as String?,
);
}
}