// Using code from https://github.com/ArjanAswal/Stockfish/blob/master/lib/src/stockfish.dart import 'dart:async'; import 'dart:ffi'; import 'dart:io'; import 'dart:convert'; import 'dart:isolate'; import 'dart:developer' as developer; import 'package:flutter/foundation.dart'; import 'package:ffi/ffi.dart'; import 'stockfish_bindings_generated.dart'; import 'stockfish_state.dart'; const String _libName = 'stockfish'; const String _releaseType = kDebugMode ? 'Debug' : 'Release'; /// The dynamic library in which the symbols for [StockfishChessEngineBindings] can be found. final DynamicLibrary _dylib = () { if (Platform.isMacOS || Platform.isIOS) { return DynamicLibrary.open('$_libName.framework/$_libName'); } if (Platform.isAndroid) { return DynamicLibrary.open('lib$_libName.so'); } if (Platform.isLinux) { return DynamicLibrary.open( '${File(Platform.resolvedExecutable).parent.parent.path}/plugins/flutter_stockfish_plugin/shared/lib$_libName.so'); } if (Platform.isWindows) { return DynamicLibrary.open( '${File(Platform.resolvedExecutable).parent.parent.parent.path}/plugins/flutter_stockfish_plugin/shared/$_releaseType/$_libName.dll'); } throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}'); }(); /// The bindings to the native functions in [_dylib]. final StockfishChessEngineBindings _bindings = StockfishChessEngineBindings(_dylib); /// A wrapper for C++ engine. class Stockfish { final Completer? completer; final _state = _StockfishState(); final _stdoutController = StreamController.broadcast(); final _mainPort = ReceivePort(); final _stdoutPort = ReceivePort(); late StreamSubscription _mainSubscription; late StreamSubscription _stdoutSubscription; Stockfish._({this.completer}) { _mainSubscription = _mainPort.listen((message) => _cleanUp(message is int ? message : 1)); _stdoutSubscription = _stdoutPort.listen((message) { if (message is String) { _stdoutController.sink.add(message); } else { developer.log('The stdout isolate sent $message', name: 'Stockfish'); } }); compute(_spawnIsolates, [_mainPort.sendPort, _stdoutPort.sendPort]).then( (success) { final state = success ? StockfishState.ready : StockfishState.error; _state._setValue(state); if (state == StockfishState.ready) { completer?.complete(this); } }, onError: (error) { developer.log('The init isolate encountered an error $error', name: 'Stockfish'); _cleanUp(1); }, ); } static Stockfish? _instance; /// Creates a C++ engine. /// /// This may throws a [StateError] if an active instance is being used. /// Owner must [dispose] it before a new instance can be created. factory Stockfish() { if (_instance != null) { throw StateError('Multiple instances are not supported, yet.'); } _instance = Stockfish._(); return _instance!; } /// The current state of the underlying C++ engine. ValueListenable get state => _state; /// The standard output stream. Stream get stdout => _stdoutController.stream; /// The standard input sink. set stdin(String line) { final stateValue = _state.value; if (stateValue != StockfishState.ready) { throw StateError('Stockfish is not ready ($stateValue)'); } final unicodePointer = '$line\n'.toNativeUtf8(); final pointer = unicodePointer.cast(); _bindings.stockfish_stdin_write(pointer); calloc.free(unicodePointer); } /// Stops the C++ engine. void dispose() { final stateValue = _state.value; if (stateValue == StockfishState.ready) { stdin = 'quit'; } } void _cleanUp(int exitCode) { _stdoutController.close(); _mainSubscription.cancel(); _stdoutSubscription.cancel(); _state._setValue( exitCode == 0 ? StockfishState.disposed : StockfishState.error); _instance = null; } } /// Creates a C++ engine asynchronously. /// /// This method is different from the factory method [Stockfish] that /// it will wait for the engine to be ready before returning the instance. Future stockfishAsync() { if (Stockfish._instance != null) { return Future.error(StateError('Only one instance can be used at a time')); } final completer = Completer(); Stockfish._instance = Stockfish._(completer: completer); return completer.future; } class _StockfishState extends ChangeNotifier implements ValueListenable { StockfishState _value = StockfishState.starting; @override StockfishState get value => _value; _setValue(StockfishState v) { if (v == _value) return; _value = v; notifyListeners(); } } void _isolateMain(SendPort mainPort) { final exitCode = _bindings.stockfish_main(); mainPort.send(exitCode); developer.log('nativeMain returns $exitCode', name: 'Stockfish'); } void _isolateStdout(SendPort stdoutPort) { String previous = ''; while (true) { final pointer = _bindings.stockfish_stdout_read(); if (pointer.address == 0) { developer.log('nativeStdoutRead returns NULL', name: 'Stockfish'); return; } Uint8List newContentCharList; final newContentLength = pointer.cast().length; newContentCharList = Uint8List.view( pointer.cast().asTypedList(newContentLength).buffer, 0, newContentLength); final newContent = utf8.decode(newContentCharList); final data = previous + newContent; final lines = data.split('\n'); previous = lines.removeLast(); for (final line in lines) { stdoutPort.send(line); } } } Future _spawnIsolates(List mainAndStdout) async { final initResult = _bindings.stockfish_init(); if (initResult != 0) { developer.log('initResult=$initResult', name: 'Stockfish'); return false; } try { await Isolate.spawn(_isolateStdout, mainAndStdout[1]); } catch (error) { developer.log('Failed to spawn stdout isolate: $error', name: 'Stockfish'); return false; } try { await Isolate.spawn(_isolateMain, mainAndStdout[0]); } catch (error) { developer.log('Failed to spawn main isolate: $error', name: 'Stockfish'); return false; } return true; }