diff --git a/ffigen.yaml b/ffigen.yaml index 2463ced..8763d8e 100644 --- a/ffigen.yaml +++ b/ffigen.yaml @@ -1,10 +1,10 @@ # Run with `flutter pub run ffigen --config ffigen_linux.yaml`. -name: StockfishChessEngineBindings +name: StockfishChessEngineCBindings description: | Bindings for `src/stockfish.h`. Regenerate bindings with `dart run ffigen --config ffigen.yaml`. -output: "lib/stockfish_bindings_generated.dart" +output: "lib/stockfish_c_bindings_generated.dart" headers: entry-points: - "src/stockfish.h" diff --git a/lib/stockfish.dart b/lib/stockfish.dart index 9346fba..7968d97 100644 --- a/lib/stockfish.dart +++ b/lib/stockfish.dart @@ -1,75 +1,27 @@ -// 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 'package:flutter_stockfish_plugin/stockfish_bindings.dart'; +import 'package:flutter_stockfish_plugin/stockfish_native_bindings.dart' + if (dart.library.html) 'package:flutter_stockfish_plugin/stockfish_web_bindings.dart'; +import 'package:flutter_stockfish_plugin/stockfish_state.dart'; -import 'stockfish_bindings_generated.dart'; -import 'stockfish_state.dart'; +final StockfishChessEngineAbstractBindings _bindings = + StockfishChessEngineBindings(); -const String _libName = 'flutter_stockfish_plugin'; -//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 || Platform.isLinux) { - return DynamicLibrary.open('lib$_libName.so'); - } - if (Platform.isWindows) { - return DynamicLibrary.open('$_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 = StockfishStateClass(); - 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'); - } + Stockfish._() { + _state.setValue(StockfishState.starting); + _bindings.stockfishMain(() { + _state.setValue(StockfishState.ready); + }).then((exitCode) { + _state.setValue( + exitCode == 0 ? StockfishState.disposed : StockfishState.error); + _instance = null; + }, onError: (error) { + _state.setValue(StockfishState.error); + _instance = null; }); - 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; @@ -82,7 +34,6 @@ class Stockfish { if (_instance != null) { throw StateError('Multiple instances are not supported, yet.'); } - _instance = Stockfish._(); return _instance!; } @@ -91,31 +42,27 @@ class Stockfish { ValueListenable get state => _state; /// The standard output stream. - Stream get stdout => _stdoutController.stream; + Stream get stdout => _bindings.read; /// The standard input sink. set stdin(String line) { - final stateValue = _state.value; + 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); + _bindings.write(line); } /// Stops the C++ engine. void dispose() { - final stateValue = _state.value; + final stateValue = state.value; if (stateValue == StockfishState.ready) { stdin = 'quit'; } } void _cleanUp(int exitCode) { - _stdoutController.close(); + /*_stdoutController.close(); _mainSubscription.cancel(); _stdoutSubscription.cancel(); @@ -123,95 +70,6 @@ class Stockfish { _state._setValue( exitCode == 0 ? StockfishState.disposed : StockfishState.error); - _instance = null; + _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; -} diff --git a/lib/stockfish_bindings.dart b/lib/stockfish_bindings.dart new file mode 100644 index 0000000..5fd230a --- /dev/null +++ b/lib/stockfish_bindings.dart @@ -0,0 +1,9 @@ +import 'dart:async'; + +abstract class StockfishChessEngineAbstractBindings { + final stdoutController = StreamController.broadcast(); + Future stockfishMain(Function active); + void cleanUp(int exitCode); + void write(String line); + Stream get read => stdoutController.stream; +} diff --git a/lib/stockfish_bindings_generated.dart b/lib/stockfish_c_bindings_generated.dart similarity index 80% rename from lib/stockfish_bindings_generated.dart rename to lib/stockfish_c_bindings_generated.dart index 1f29c15..e2851f3 100644 --- a/lib/stockfish_bindings_generated.dart +++ b/lib/stockfish_c_bindings_generated.dart @@ -5,23 +5,24 @@ // AUTO GENERATED FILE, DO NOT EDIT. // // Generated by `package:ffigen`. +// ignore_for_file: type=lint import 'dart:ffi' as ffi; /// Bindings for `src/stockfish.h`. /// /// Regenerate bindings with `dart run ffigen --config ffigen.yaml`. /// -class StockfishChessEngineBindings { +class StockfishChessEngineCBindings { /// Holds the symbol lookup function. final ffi.Pointer Function(String symbolName) _lookup; /// The symbols are looked up in [dynamicLibrary]. - StockfishChessEngineBindings(ffi.DynamicLibrary dynamicLibrary) + StockfishChessEngineCBindings(ffi.DynamicLibrary dynamicLibrary) : _lookup = dynamicLibrary.lookup; /// The symbols are looked up with [lookup]. - StockfishChessEngineBindings.fromLookup( + StockfishChessEngineCBindings.fromLookup( ffi.Pointer Function(String symbolName) lookup) : _lookup = lookup; @@ -56,15 +57,19 @@ class StockfishChessEngineBindings { late final _stockfish_stdin_write = _stockfish_stdin_writePtr .asFunction)>(); - ffi.Pointer stockfish_stdout_read() { - return _stockfish_stdout_read(); + ffi.Pointer stockfish_stdout_read( + int trygetline, + ) { + return _stockfish_stdout_read( + trygetline, + ); } late final _stockfish_stdout_readPtr = - _lookup Function()>>( + _lookup Function(ffi.Int)>>( 'stockfish_stdout_read'); - late final _stockfish_stdout_read = - _stockfish_stdout_readPtr.asFunction Function()>(); + late final _stockfish_stdout_read = _stockfish_stdout_readPtr + .asFunction Function(int)>(); } typedef ssize_t = __ssize_t; diff --git a/lib/stockfish_native_bindings.dart b/lib/stockfish_native_bindings.dart new file mode 100644 index 0000000..54e69d8 --- /dev/null +++ b/lib/stockfish_native_bindings.dart @@ -0,0 +1,148 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:developer' as developer; + +import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart'; + +import 'package:flutter_stockfish_plugin/stockfish_bindings.dart'; +import 'package:flutter_stockfish_plugin/stockfish_c_bindings_generated.dart'; + +const String _libName = 'flutter_stockfish_plugin'; + +/// The dynamic library in which the symbols for [StockfishChessEngineCBindings] can be found. +final DynamicLibrary _dylib = () { + if (Platform.isMacOS || Platform.isIOS) { + return DynamicLibrary.open('$_libName.framework/$_libName'); + } + if (Platform.isAndroid || Platform.isLinux) { + return DynamicLibrary.open('lib$_libName.so'); + } + if (Platform.isWindows) { + return DynamicLibrary.open('$_libName.dll'); + } + throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}'); +}(); + +/// The bindings to the native functions in [_dylib]. +final StockfishChessEngineCBindings _bindings = + StockfishChessEngineCBindings(_dylib); + +class StockfishChessEngineBindings + extends StockfishChessEngineAbstractBindings { + final _mainPort = ReceivePort(); + final _stdoutPort = ReceivePort(); + late StreamSubscription _mainSubscription; + late StreamSubscription _stdoutSubscription; + + @override + Future stockfishMain(Function active) { + final completer = Completer(); + _mainSubscription = _mainPort.listen((message) { + cleanUp(message is int ? message : 1); + completer.complete(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) { + if (success) { + active(); + } else { + completer.completeError('Unable to create Isolates'); + cleanUp(1); + } + }, + onError: (error) { + developer.log('The init isolate encountered an error $error', + name: 'Stockfish'); + cleanUp(1); + }, + ); + return completer.future; + } + + @override + void write(String line) { + final unicodePointer = '$line\n'.toNativeUtf8(); + final pointer = unicodePointer.cast(); + _bindings.stockfish_stdin_write(pointer); + calloc.free(unicodePointer); + } + + @override + void cleanUp(int exitCode) { + stdoutController.close(); + _mainSubscription.cancel(); + _stdoutSubscription.cancel(); + } +} + +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(0); + + 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; +} diff --git a/lib/stockfish_state.dart b/lib/stockfish_state.dart index c505234..05b3a85 100644 --- a/lib/stockfish_state.dart +++ b/lib/stockfish_state.dart @@ -1,4 +1,20 @@ -// Taken from https://github.com/ArjanAswal/Stockfish/blob/master/lib/src/stockfish_state.dart +import 'package:flutter/foundation.dart'; + +class StockfishStateClass extends ChangeNotifier + implements ValueListenable { + StockfishState _value = StockfishState.starting; + + @override + StockfishState get value => _value; + + setValue(StockfishState v) { + if (v == _value) return; + _value = v; + notifyListeners(); + } +} + +// The following is taken from https://github.com/ArjanAswal/Stockfish/blob/master/lib/src/stockfish_state.dart /// C++ engine state. enum StockfishState { diff --git a/lib/stockfish_web_bindings.dart b/lib/stockfish_web_bindings.dart new file mode 100644 index 0000000..b413d4c --- /dev/null +++ b/lib/stockfish_web_bindings.dart @@ -0,0 +1,25 @@ +import 'dart:async'; + +import 'package:flutter_stockfish_plugin/stockfish_bindings.dart'; + +class StockfishChessEngineBindings + extends StockfishChessEngineAbstractBindings { + @override + void cleanUp(int exitCode) { + // TODO: implement cleanUp + } + + @override + Future stockfishMain(Function active) { + // TODO: implement stockfishMain + final completer = Completer(); + //completer.complete(0); + active(); + return completer.future; + } + + @override + void write(String line) { + // TODO: implement write + } +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8e0fc1a..e85fa7d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -10,7 +10,7 @@ set(CMAKE_CXX_STANDARD 17) if(MSVC) set(COMMON_FLAGS "/std:c++17 /LTCG") - set(SIMD_FLAGS "/arch:AVX2 /arch:SSE /DUSE_POPCNT /DUSE_SSE41 /DUSE_SSSE3 /DUSE_SSE2") + set(SIMD_FLAGS "/arch:AVX2 /DUSE_POPCNT /arch:SSE /DUSE_SSE2 /DUSE_SSSE3 /DUSE_SSE41") if (CMAKE_SIZEOF_VOID_P EQUAL 8) message(STATUS "Adding x86_64 specific flags") @@ -25,9 +25,9 @@ if(MSVC) set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} /Ox /DNDEBUG") else() - set(COMMON_FLAGS "-Wall -Wcast-qual -fno-exceptions -std=c++17 -pedantic -Wextra -Wshadow -Wmissing-declarations -flto -DUSE_PTHREADS") + set(COMMON_FLAGS "-Wall -Wcast-qual -Wno-main -fno-exceptions -std=c++17 -pedantic -Wextra -Wshadow -Wmissing-declarations -flto -DUSE_PTHREADS") - set(SIMD_FLAGS "-msse -msse3 -mpopcnt -DUSE_POPCNT -DUSE_SSE41 -msse4.1 -DUSE_SSSE3 -mssse3 -DUSE_SSE2 -msse2") + set(SIMD_FLAGS "-mpopcnt -DUSE_POPCNT -msse -DUSE_SSE2 -msse2 -msse3 -DUSE_SSSE3 -mssse3 -DUSE_SSE41 -msse4.1 -DUSE_SSE42 -msse4.2") if (CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64") message(STATUS "Adding x86_64 specific flags") diff --git a/src/stockfish.cpp b/src/stockfish.cpp index 88a38f0..fad93cb 100644 --- a/src/stockfish.cpp +++ b/src/stockfish.cpp @@ -15,9 +15,11 @@ #include "fixes.h" #include "stockfish.h" -const char *QUITOK = "quitok\n"; +const char* QUITOK = "quitok\n"; -int main(int, char **); +void runMain() {} + +int main(int, char**); int stockfish_init() { fakein.open(); @@ -27,8 +29,11 @@ int stockfish_init() { int stockfish_main() { int argc = 1; - char *argv[] = {(char *)""}; + char* empty = (char*)malloc(0); + *empty = 0; + char* argv[] = {empty}; int exitCode = main(argc, argv); + free(empty); fakeout << QUITOK << "\n"; @@ -44,7 +49,7 @@ int stockfish_main() { return exitCode; } -ssize_t stockfish_stdin_write(char *data) { +ssize_t stockfish_stdin_write(char* data) { std::string val(data); fakein << val << fakeendl; return val.length(); @@ -52,9 +57,15 @@ ssize_t stockfish_stdin_write(char *data) { std::string data; -const char *stockfish_stdout_read() { - if (getline(fakeout, data)) { - return data.c_str(); +const char* stockfish_stdout_read(int trygetline) { + if (trygetline) { + if (fakeout.try_get_line(data)) { + return data.c_str(); + } + } else { + if (getline(fakeout, data)) { + return data.c_str(); + } } return nullptr; } \ No newline at end of file diff --git a/src/stockfish.h b/src/stockfish.h index b7fa36f..adba691 100644 --- a/src/stockfish.h +++ b/src/stockfish.h @@ -20,29 +20,34 @@ #ifdef _WIN32 #define FFI_PLUGIN_EXPORT __declspec(dllexport) #else -#define FFI_PLUGIN_EXPORT __attribute__((visibility("default"))) __attribute__((used)) +#define FFI_PLUGIN_EXPORT \ + __attribute__((visibility("default"))) __attribute__((used)) #endif // Initialisation of Stockfish. #ifndef _ffigen extern "C" #endif -FFI_PLUGIN_EXPORT int stockfish_init(); + FFI_PLUGIN_EXPORT int + stockfish_init(); // Stockfish main loop. #ifndef _ffigen extern "C" #endif -FFI_PLUGIN_EXPORT int stockfish_main(); + FFI_PLUGIN_EXPORT int + stockfish_main(); // Writing to Stockfish STDIN. #ifndef _ffigen extern "C" #endif -FFI_PLUGIN_EXPORT ssize_t stockfish_stdin_write(char *data); + FFI_PLUGIN_EXPORT ssize_t + stockfish_stdin_write(char* data); // Reading Stockfish STDOUT #ifndef _ffigen extern "C" #endif -FFI_PLUGIN_EXPORT const char * stockfish_stdout_read(); \ No newline at end of file + FFI_PLUGIN_EXPORT const char* + stockfish_stdout_read(int trygetline); diff --git a/src/stream_fix.cpp b/src/stream_fix.cpp index 4271c6b..9cae184 100644 --- a/src/stream_fix.cpp +++ b/src/stream_fix.cpp @@ -2,7 +2,8 @@ bool FakeStream::try_get_line(std::string& val) { std::unique_lock lock(mutex_guard); - if (string_queue.empty() || closed) return false; + if (string_queue.empty() || closed) + return false; val = string_queue.front(); string_queue.pop(); return true; @@ -20,18 +21,27 @@ void FakeStream::close() { while (!string_queue.empty()) { string_queue.pop(); } - mutex_signal.notify_one(); + mutex_signal.notify_all(); +} +bool FakeStream::is_closed() { + return closed; } -bool FakeStream::is_closed() { return closed; } -std::streambuf* FakeStream::rdbuf() { return nullptr; } +std::streambuf* FakeStream::rdbuf() { + return nullptr; +} -std::streambuf* FakeStream::rdbuf(std::streambuf* buf) { return nullptr; } +std::streambuf* FakeStream::rdbuf(std::streambuf* buf) { + (void)buf; + return nullptr; +} bool std::getline(FakeStream& is, std::string& str) { - if (is.is_closed()) return false; + if (is.is_closed()) + return false; is >> str; - if (is.is_closed()) return false; + if (is.is_closed()) + return false; return true; } diff --git a/src/stream_fix.h b/src/stream_fix.h index cfc94d1..16981d7 100644 --- a/src/stream_fix.h +++ b/src/stream_fix.h @@ -6,18 +6,24 @@ #include #include +#ifdef __EMSCRIPTEN__ +#include +#include +#endif + template inline std::string stringify(const T& input) { std::ostringstream output; // from www .ja va 2s . com output << input; - return output.str(); + return std::string(output.str()); } class FakeStream { public: template FakeStream& operator<<(const T& val) { - if (closed) return *this; + if (closed) + return *this; std::lock_guard lock(mutex_guard); string_queue.push(stringify(val)); mutex_signal.notify_one(); @@ -25,29 +31,46 @@ class FakeStream { }; template FakeStream& operator>>(T& val) { - if (closed) return *this; + if (closed) + return *this; std::unique_lock lock(mutex_guard); +#ifdef __EMSCRIPTEN__ + if (emscripten_is_main_runtime_thread()) { + lock.unlock(); + while (true) { + lock = std::unique_lock(mutex_guard); + if (!string_queue.empty() || closed) + break; + lock.unlock(); + emscripten_sleep(10); + } + } else { + mutex_signal.wait( + lock, [this] { return !string_queue.empty() || closed; }); + } +#else mutex_signal.wait(lock, [this] { return !string_queue.empty() || closed; }); - if (closed) return *this; +#endif + if (closed) + return *this; val = string_queue.front(); string_queue.pop(); return *this; }; - bool try_get_line(std::string& val); + bool try_get_line(std::string&); void open(); void close(); bool is_closed(); std::streambuf* rdbuf(); - std::streambuf* rdbuf(std::streambuf* __sb); + std::streambuf* rdbuf(std::streambuf*); private: bool closed = false; std::queue string_queue; - //std::string line; std::mutex mutex_guard; std::condition_variable mutex_signal; }; @@ -56,10 +79,6 @@ namespace std { bool getline(FakeStream& is, std::string& str); } // namespace std -// #define endl fakeendl -// #define cout fakeout -// #define cin fakein - extern FakeStream fakeout; extern FakeStream fakein; extern std::string fakeendl;