commit dbc707f23fce3c085b389be4bdabb75d27605fc2 Author: jusax23 Date: Sat Oct 28 20:46:47 2023 +0200 code only diff --git a/lib/stockfish.dart b/lib/stockfish.dart new file mode 100644 index 0000000..a9cce74 --- /dev/null +++ b/lib/stockfish.dart @@ -0,0 +1,223 @@ +// 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'); + print("stoped"); +} + +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_generated.dart b/lib/stockfish_bindings_generated.dart new file mode 100644 index 0000000..1f29c15 --- /dev/null +++ b/lib/stockfish_bindings_generated.dart @@ -0,0 +1,71 @@ +// ignore_for_file: always_specify_types +// ignore_for_file: camel_case_types +// ignore_for_file: non_constant_identifier_names + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +import 'dart:ffi' as ffi; + +/// Bindings for `src/stockfish.h`. +/// +/// Regenerate bindings with `dart run ffigen --config ffigen.yaml`. +/// +class StockfishChessEngineBindings { + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + /// The symbols are looked up in [dynamicLibrary]. + StockfishChessEngineBindings(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + StockfishChessEngineBindings.fromLookup( + ffi.Pointer Function(String symbolName) + lookup) + : _lookup = lookup; + + int stockfish_init() { + return _stockfish_init(); + } + + late final _stockfish_initPtr = + _lookup>('stockfish_init'); + late final _stockfish_init = _stockfish_initPtr.asFunction(); + + int stockfish_main() { + return _stockfish_main(); + } + + late final _stockfish_mainPtr = + _lookup>('stockfish_main'); + late final _stockfish_main = _stockfish_mainPtr.asFunction(); + + int stockfish_stdin_write( + ffi.Pointer data, + ) { + return _stockfish_stdin_write( + data, + ); + } + + late final _stockfish_stdin_writePtr = + _lookup)>>( + 'stockfish_stdin_write'); + late final _stockfish_stdin_write = _stockfish_stdin_writePtr + .asFunction)>(); + + ffi.Pointer stockfish_stdout_read() { + return _stockfish_stdout_read(); + } + + late final _stockfish_stdout_readPtr = + _lookup Function()>>( + 'stockfish_stdout_read'); + late final _stockfish_stdout_read = + _stockfish_stdout_readPtr.asFunction Function()>(); +} + +typedef ssize_t = __ssize_t; +typedef __ssize_t = ffi.Long; diff --git a/lib/stockfish_state.dart b/lib/stockfish_state.dart new file mode 100644 index 0000000..c505234 --- /dev/null +++ b/lib/stockfish_state.dart @@ -0,0 +1,16 @@ +// Taken from https://github.com/ArjanAswal/Stockfish/blob/master/lib/src/stockfish_state.dart + +/// C++ engine state. +enum StockfishState { + /// Engine has been stopped. + disposed, + + /// An error occured (engine could not start). + error, + + /// Engine is running. + ready, + + /// Engine is starting. + starting, +} diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..9bcbb05 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,22 @@ +# The Flutter tooling requires that developers have CMake 3.22 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.22) + +# Project-level configuration. +set(PROJECT_NAME "stockfish") +project(${PROJECT_NAME} LANGUAGES CXX) + +# Invoke the build for native code shared with the other target platforms. +# This can be changed to accomodate different builds. +add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../src" "${CMAKE_CURRENT_BINARY_DIR}/shared") + +# 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(stockfish_bundled_libraries + # Defined in ../src/CMakeLists.txt. + # This can be changed to accomodate different builds. + $ + PARENT_SCOPE +) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..cf3aaf0 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,41 @@ +# The Flutter tooling requires that developers have CMake 3.18 or later +# installed. You should not increase this version, as doing so will cause +# the plugin to fail to compile for some customers of the plugin. +cmake_minimum_required(VERSION 3.18) + +project(stockfish VERSION 0.0.1 LANGUAGES CXX) +file(GLOB_RECURSE cppPaths "Stockfish/src/*.cpp") +set(CMAKE_CXX_STANDARD 17) +set(NNUE_NAME nn-0000000000a0.nnue) + +add_library(stockfish SHARED + "stockfish.cpp" + "stream_fix.cpp" + ${cppPaths} +) + +set_target_properties(stockfish PROPERTIES + PUBLIC_HEADER stockfish.h + OUTPUT_NAME "stockfish" +) + +if(MSVC) + add_definitions(/FI"stream_fix.h") +else() + add_definitions(-include stream_fix.h) +endif() + +target_compile_definitions(stockfish PUBLIC DART_SHARED_LIB) + +target_include_directories(stockfish + PUBLIC + "./" +) + + +if (MSVC) + file(DOWNLOAD https://tests.stockfishchess.org/api/nn/${NNUE_NAME} ${CMAKE_BINARY_DIR}/runner/Debug/${NNUE_NAME}) + file(COPY ${CMAKE_BINARY_DIR}/runner/Debug/${NNUE_NAME} DESTINATION ${CMAKE_BINARY_DIR}/runner/Release) +else () + file(DOWNLOAD https://tests.stockfishchess.org/api/nn/${NNUE_NAME} ${CMAKE_BINARY_DIR}/${NNUE_NAME}) +endif () diff --git a/src/stockfish.cpp b/src/stockfish.cpp new file mode 100644 index 0000000..9991bee --- /dev/null +++ b/src/stockfish.cpp @@ -0,0 +1,66 @@ +#include +#include +#ifdef _WIN32 +#include +#include +#define STDIN_FILENO 0 +#define STDOUT_FILENO 1 +#define STDERR_FILENO 2 +#ifdef _WIN64 +#define ssize_t __int64 +#else +#define ssize_t long +#endif +#else +#include +#endif + +#define BUFFER_SIZE 1024 + +#include "stockfish.h" + +const char *QUITOK = "quitok\n"; + +int main(int, char **); + +int stockfish_init() { + std::cout << "Init Stockfish: Nothing todo!"; + return 0; +} + +int stockfish_main() { + int argc = 1; + char *argv[] = {(char *)""}; + int exitCode = main(argc, argv); + + fakeout << QUITOK << "\n"; + + usleep(100000); + + fakeout.close(); + fakein.close(); + + return exitCode; +} + +ssize_t stockfish_stdin_write(char *data) { + std::string val(data); + fakein << val << fakeendl; + return val.length(); +} + +std::string data; +char buffer[BUFFER_SIZE + 1]; + +char *stockfish_stdout_read() { + if (getline(fakeout, data)) { + size_t len = data.length(); + size_t i; + for (i = 0; i < len && i < BUFFER_SIZE; i++) { + buffer[i] = data[i]; + } + buffer[i] = 0; + return buffer; + } + return NULL; +} \ No newline at end of file diff --git a/src/stockfish.h b/src/stockfish.h new file mode 100644 index 0000000..376321b --- /dev/null +++ b/src/stockfish.h @@ -0,0 +1,46 @@ +//#define _ffigen + +#ifdef _WIN32 +#include +#else +#include +#endif +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#endif + +#ifdef _WIN32 +#define FFI_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FFI_PLUGIN_EXPORT __attribute__((visibility("default"))) __attribute__((used)) +#endif + +// Initialisation of Stockfish. +#ifndef _ffigen +extern "C" +#endif +FFI_PLUGIN_EXPORT int stockfish_init(); + +// Stockfish main loop. +#ifndef _ffigen +extern "C" +#endif +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); + +// Reading Stockfish STDOUT +#ifndef _ffigen +extern "C" +#endif +FFI_PLUGIN_EXPORT char * stockfish_stdout_read(); \ No newline at end of file diff --git a/src/stream_fix.cpp b/src/stream_fix.cpp new file mode 100644 index 0000000..d08e29c --- /dev/null +++ b/src/stream_fix.cpp @@ -0,0 +1,31 @@ +#include "stream_fix.h" + +bool FakeStream::try_get_line(std::string& val) { + std::unique_lock lock(mutex_guard); + if (string_queue.empty() || closed) return false; + val = string_queue.front(); + string_queue.pop(); + return true; +} + +void FakeStream::close() { + std::lock_guard lock(mutex_guard); + closed = true; + mutex_signal.notify_one(); +} +bool FakeStream::is_closed() { return closed; } + +std::streambuf* FakeStream::rdbuf() { return nullptr; } + +std::streambuf* FakeStream::rdbuf(std::streambuf* __sb) { return nullptr; } + +bool std::getline(FakeStream& is, std::string& str) { + if (is.is_closed()) return false; + is >> str; + if (is.is_closed()) return false; + return true; +} + +FakeStream fakeout; +FakeStream fakein; +std::string fakeendl("\n"); \ No newline at end of file diff --git a/src/stream_fix.h b/src/stream_fix.h new file mode 100644 index 0000000..7fabef4 --- /dev/null +++ b/src/stream_fix.h @@ -0,0 +1,66 @@ +#ifndef _STREAM_FIX_H_ +#define _STREAM_FIX_H_ +#include +#include +#include +#include +#include + +template +inline std::string stringify(const T& input) { + std::ostringstream output; // from www .ja va 2s . com + output << input; + return output.str(); +} + +class FakeStream { + public: + template + FakeStream& operator<<(const T& val) { + if (closed) return *this; + std::lock_guard lock(mutex_guard); + string_queue.push(stringify(val)); + mutex_signal.notify_one(); + return *this; + }; + template + FakeStream& operator>>(T& val) { + if (closed) return *this; + std::unique_lock lock(mutex_guard); + mutex_signal.wait(lock, + [this] { return !string_queue.empty() || closed; }); + if (closed) return *this; + val = string_queue.front(); + string_queue.pop(); + return *this; + }; + + bool try_get_line(std::string& val); + + void close(); + bool is_closed(); + + std::streambuf* rdbuf(); + std::streambuf* rdbuf(std::streambuf* __sb); + + private: + bool closed = false; + std::queue string_queue; + std::string line; + std::mutex mutex_guard; + std::condition_variable mutex_signal; +}; + +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; + +#endif \ No newline at end of file diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..bcc855d --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,23 @@ +# The Flutter tooling requires that developers have a version of Visual Studio +# installed that includes CMake 3.22 or later. You should not increase this +# version, as doing so will cause the plugin to fail to compile for some +# customers of the plugin. +cmake_minimum_required(VERSION 3.22) + +# Project-level configuration. +set(PROJECT_NAME "stockfish") +project(${PROJECT_NAME} LANGUAGES CXX) + +# Invoke the build for native code shared with the other target platforms. +# This can be changed to accomodate different builds. +add_subdirectory("${CMAKE_CURRENT_SOURCE_DIR}/../src" "${CMAKE_CURRENT_BINARY_DIR}/shared") + +# 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(stockfish_bundled_libraries + # Defined in ../src/CMakeLists.txt. + # This can be changed to accomodate different builds. + $ + PARENT_SCOPE +)