From 8f9f220c4057c82780596d1cb79dab807524cd90 Mon Sep 17 00:00:00 2001 From: jusax23 Date: Sun, 25 Feb 2024 00:39:40 +0100 Subject: [PATCH] web implementation, missing js and wasm artifacts --- .gitignore | 7 +- README.md | 15 ++++ lib/stockfish.dart | 43 +++++++----- lib/stockfish_c_bindings_generated.dart | 20 ++++++ lib/stockfish_native_bindings.dart | 1 + lib/stockfish_web_bindings.dart | 69 +++++++++++++++--- pubspec.yaml | 6 +- src/CMakeLists.txt | 2 +- src/stockfish.cpp | 14 ++++ src/stockfish.h | 14 ++++ web/CMakeLists.txt | 52 ++++++++++++++ web/js_bindings.js | 93 +++++++++++++++++++++++++ 12 files changed, 306 insertions(+), 30 deletions(-) create mode 100644 web/CMakeLists.txt create mode 100644 web/js_bindings.js diff --git a/.gitignore b/.gitignore index 23008ae..5454fab 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,9 @@ src/-lstdc++.res # Neural network for the NNUE evaluation **/*.nnue -flags.txt \ No newline at end of file +flags.txt + +CMakeFiles +cmake_install.cmake +CMakeCache.txt +Makefile \ No newline at end of file diff --git a/README.md b/README.md index 10118c6..0d31f1b 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,21 @@ stockfish.dispose(); A complete Example can be found at [stockfish_chess_engine](https://github.com/loloof64/StockfishChessEngineFlutter). +## Web support +Web support is currently experimental. It uses a version of stockfish compiled with [emscripten](https://emscripten.org/). + +In order to make multithreading available, the site must run in a secure environment. +The following headers must be set for this: + +- `Cross-Origin-Embedder-Policy: require-corp` +- `Cross-Origin-Opener-Policy: same-origin` + +Problems: +- The current version includes the `.js`, `.wasm` and neuralnetwork data as assets. +These files are bundled with every build on every platform, even if they are not needed. +This approach wasts about 41MB, if not striped out by hand. + + ## Goal of this fork of stockfish_chess_engine * Avoid limitation. This version does not redirect stdout and stdin of the app for communication with stockfish. diff --git a/lib/stockfish.dart b/lib/stockfish.dart index 7968d97..1cd4e2e 100644 --- a/lib/stockfish.dart +++ b/lib/stockfish.dart @@ -1,19 +1,21 @@ +import 'dart:async'; + import 'package:flutter/foundation.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'; -final StockfishChessEngineAbstractBindings _bindings = - StockfishChessEngineBindings(); - class Stockfish { final _state = StockfishStateClass(); + final StockfishChessEngineAbstractBindings _bindings = + StockfishChessEngineBindings(); - Stockfish._() { + Stockfish._({Completer? completer}) { _state.setValue(StockfishState.starting); _bindings.stockfishMain(() { _state.setValue(StockfishState.ready); + completer?.complete(this); }).then((exitCode) { _state.setValue( exitCode == 0 ? StockfishState.disposed : StockfishState.error); @@ -21,12 +23,13 @@ class Stockfish { }, onError: (error) { _state.setValue(StockfishState.error); _instance = null; + completer?.completeError(error); }); } static Stockfish? _instance; - /// Creates a C++ engine. + /// Creates the stockfish engine. /// /// This may throws a [StateError] if an active instance is being used. /// Owner must [dispose] it before a new instance can be created. @@ -38,7 +41,7 @@ class Stockfish { return _instance!; } - /// The current state of the underlying C++ engine. + /// The current state of the underlying stockfish engine. ValueListenable get state => _state; /// The standard output stream. @@ -53,23 +56,25 @@ class Stockfish { _bindings.write(line); } - /// Stops the C++ engine. + /// Stops the stockfish 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 the stockfish 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; } diff --git a/lib/stockfish_c_bindings_generated.dart b/lib/stockfish_c_bindings_generated.dart index e2851f3..37d6c5b 100644 --- a/lib/stockfish_c_bindings_generated.dart +++ b/lib/stockfish_c_bindings_generated.dart @@ -43,6 +43,25 @@ class StockfishChessEngineCBindings { _lookup>('stockfish_main'); late final _stockfish_main = _stockfish_mainPtr.asFunction(); + void stockfish_start_main() { + return _stockfish_start_main(); + } + + late final _stockfish_start_mainPtr = + _lookup>('stockfish_start_main'); + late final _stockfish_start_main = + _stockfish_start_mainPtr.asFunction(); + + int stockfish_last_main_state() { + return _stockfish_last_main_state(); + } + + late final _stockfish_last_main_statePtr = + _lookup>( + 'stockfish_last_main_state'); + late final _stockfish_last_main_state = + _stockfish_last_main_statePtr.asFunction(); + int stockfish_stdin_write( ffi.Pointer data, ) { @@ -74,3 +93,4 @@ class StockfishChessEngineCBindings { typedef ssize_t = __ssize_t; typedef __ssize_t = ffi.Long; +typedef Dart__ssize_t = int; diff --git a/lib/stockfish_native_bindings.dart b/lib/stockfish_native_bindings.dart index 54e69d8..5932f8a 100644 --- a/lib/stockfish_native_bindings.dart +++ b/lib/stockfish_native_bindings.dart @@ -64,6 +64,7 @@ class StockfishChessEngineBindings onError: (error) { developer.log('The init isolate encountered an error $error', name: 'Stockfish'); + completer.completeError(error); cleanUp(1); }, ); diff --git a/lib/stockfish_web_bindings.dart b/lib/stockfish_web_bindings.dart index b413d4c..3a9f4aa 100644 --- a/lib/stockfish_web_bindings.dart +++ b/lib/stockfish_web_bindings.dart @@ -1,25 +1,78 @@ -import 'dart:async'; +// ignore_for_file: avoid_web_libraries_in_flutter +import 'dart:async'; +import 'dart:js' as js; +import 'dart:html' as html; + +import 'package:flutter/foundation.dart'; import 'package:flutter_stockfish_plugin/stockfish_bindings.dart'; class StockfishChessEngineBindings extends StockfishChessEngineAbstractBindings { - @override - void cleanUp(int exitCode) { - // TODO: implement cleanUp + Future? loadJs; + StockfishChessEngineBindings() { + loadJs = loadJsFileIfNeeded(); } @override - Future stockfishMain(Function active) { - // TODO: implement stockfishMain + void cleanUp(int exitCode) { + stdoutController.close(); + js.context.callMethod("stop_listening", []); + } + + @override + Future stockfishMain(Function active) async { + if (loadJs != null) { + await loadJs; + loadJs = null; + } final completer = Completer(); - //completer.complete(0); + js.context.callMethod("start_listening", [ + (line) => stdoutController.sink.add(line), + (state) { + cleanUp(state is int ? state : 1); + completer.complete(state is int ? state : 1); + } + ]); active(); return completer.future; } @override void write(String line) { - // TODO: implement write + js.context.callMethod("write", [line]); } } + +bool _jsloaded = false; + +Future loadJsFileIfNeeded() async { + if (kIsWeb && !_jsloaded) { + final stockfishScript = html.document.createElement("script"); + stockfishScript.setAttribute("src", + "assets/packages/flutter_stockfish_plugin/web/flutter_stockfish_plugin.js"); + html.document.head?.append(stockfishScript); + + await stockfishScript.onLoad.first; + + final jsBindingsScript = html.document.createElement("script"); + jsBindingsScript.setAttribute( + "src", "assets/packages/flutter_stockfish_plugin/web/js_bindings.js"); + html.document.head?.append(jsBindingsScript); + + await jsBindingsScript.onLoad.first; + + await _stockfishWaitReady(); + + //js.context.callMethod("t_cb", [test]); + _jsloaded = true; + } +} + +Future _stockfishWaitReady() { + final completer = Completer(); + js.context.callMethod('wait_ready', [ + completer.complete, + ]); + return completer.future; +} diff --git a/pubspec.yaml b/pubspec.yaml index e4c9292..0ac352f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,7 +46,11 @@ flutter: web: # ------------------- assets: - - web/test.js + - web/js_bindings.js + - web/stockfish_data.bin + - web/flutter_stockfish_plugin.js + - web/flutter_stockfish_plugin.wasm + - web/flutter_stockfish_plugin.worker.js # To add assets to your plugin package, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e85fa7d..31eed39 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -27,7 +27,7 @@ if(MSVC) else() set(COMMON_FLAGS "-Wall -Wcast-qual -Wno-main -fno-exceptions -std=c++17 -pedantic -Wextra -Wshadow -Wmissing-declarations -flto -DUSE_PTHREADS") - set(SIMD_FLAGS "-mpopcnt -DUSE_POPCNT -msse -DUSE_SSE2 -msse2 -msse3 -DUSE_SSSE3 -mssse3 -DUSE_SSE41 -msse4.1 -DUSE_SSE42 -msse4.2") + set(SIMD_FLAGS "-mpopcnt -DUSE_POPCNT -mavx -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 fad93cb..618e4a6 100644 --- a/src/stockfish.cpp +++ b/src/stockfish.cpp @@ -14,6 +14,7 @@ #include "fixes.h" #include "stockfish.h" +#include const char* QUITOK = "quitok\n"; @@ -27,7 +28,10 @@ int stockfish_init() { return 0; } +int _last_main_state = -2; + int stockfish_main() { + _last_main_state = -1; int argc = 1; char* empty = (char*)malloc(0); *empty = 0; @@ -46,9 +50,19 @@ int stockfish_main() { fakeout.close(); fakein.close(); + _last_main_state = exitCode; return exitCode; } +void stockfish_start_main(){ + std::thread t(stockfish_main); + t.detach(); +} + +int stockfish_last_main_state(){ + return _last_main_state; +} + ssize_t stockfish_stdin_write(char* data) { std::string val(data); fakein << val << fakeendl; diff --git a/src/stockfish.h b/src/stockfish.h index adba691..539212f 100644 --- a/src/stockfish.h +++ b/src/stockfish.h @@ -38,6 +38,20 @@ extern "C" FFI_PLUGIN_EXPORT int stockfish_main(); +// Stockfish start main loop. +#ifndef _ffigen +extern "C" +#endif + FFI_PLUGIN_EXPORT void + stockfish_start_main(); + +// Stockfish last main loop state. +#ifndef _ffigen +extern "C" +#endif + FFI_PLUGIN_EXPORT int + stockfish_last_main_state(); + // Writing to Stockfish STDIN. #ifndef _ffigen extern "C" diff --git a/web/CMakeLists.txt b/web/CMakeLists.txt new file mode 100644 index 0000000..19e01c3 --- /dev/null +++ b/web/CMakeLists.txt @@ -0,0 +1,52 @@ +# 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(flutter_stockfish_plugin) +file(GLOB_RECURSE cppPaths "../src/Stockfish/src/*.cpp") +set(CMAKE_CXX_STANDARD 17) + +set(NNUE_NAME nn-5af11540bbfe.nnue) + +add_definitions(-DNNUE_EMBEDDING_OFF) # embeding nnue network is currently not supported. + +set(EMSCRIPTEN_PATH "$ENV{EMSDK}/upstream/emscripten" CACHE STRING "Path to Emscripten") +set(CMAKE_TOOLCHAIN_FILE "${EMSCRIPTEN_PATH}/cmake/Modules/Platform/Emscripten.cmake" CACHE STRING "Emscripten toolchain file") +set(CMAKE_CXX_COMPILER "${EMSCRIPTEN_PATH}/em++") + +set(COMMON_FLAGS "-Wall -Wcast-qual -Wno-main -fno-exceptions -std=c++17 -pedantic -Wextra -Wshadow -Wmissing-declarations -flto") +set(SIMD_FLAGS "${CMAKE_CXX_FLAGS} -msimd128 -mavx -msse -DUSE_SSE2 -msse2 -msse3 -DUSE_SSSE3 -mssse3 -DUSE_SSE41 -msse4.1 -DUSE_SSE42 -msse4.2") + +set(EM_FLAGS "${EM_FLAGS} -s WASM=1 -sASYNCIFY") +set(EM_FLAGS "${EM_FLAGS} -s EXPORTED_RUNTIME_METHODS=ccall,cwrap") +set(EM_FLAGS "${EM_FLAGS} -s TOTAL_STACK=8MB -s INITIAL_MEMORY=512MB -s ALLOW_MEMORY_GROWTH") +set(EM_FLAGS "${EM_FLAGS} -s PTHREAD_POOL_SIZE=32") + + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COMMON_FLAGS} ${SIMD_FLAGS} -O3 -DNDEBUG -s USE_PTHREADS=1 -Dmain=runMain") + + +add_executable(${PROJECT_NAME} + "../src/stockfish.cpp" + "../src/stream_fix.cpp" + "../src/small_fixes.cpp" + ${cppPaths} +) + +set_target_properties(${PROJECT_NAME} PROPERTIES LINK_FLAGS "${EM_FLAGS}") + +set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "${PROJECT_NAME}.js") + +add_definitions(-include ../src/fixes.h) + +target_include_directories(${PROJECT_NAME} + PUBLIC + "./" +) + + +file(DOWNLOAD https://tests.stockfishchess.org/api/nn/${NNUE_NAME} ${CMAKE_BINARY_DIR}/stockfish_data.bin) + + + diff --git a/web/js_bindings.js b/web/js_bindings.js new file mode 100644 index 0000000..002d39a --- /dev/null +++ b/web/js_bindings.js @@ -0,0 +1,93 @@ +const nnue_name = "nn-5af11540bbfe.nnue"; + +let s_read, s_write, s_main, s_init, s_state; + +let ready = false; +let ready_cb = null; + +Module.onRuntimeInitialized = async function () { + let data = await fetch("assets/packages/flutter_stockfish_plugin/web/stockfish_data.bin"); + let b = new Uint8Array(await data.arrayBuffer()); + FS.createDataFile("/", nnue_name, b, true, false, true); + s_read = Module.cwrap("stockfish_stdout_read", "char*", ["bool"], { async: false }); + s_write = Module.cwrap("stockfish_stdin_write", "ssize_t", ["char*"], { async: false }); + s_main = Module.cwrap("stockfish_start_main", "void", [], { async: false }); + s_init = Module.cwrap("stockfish_init", "int", [], { async: false }); + s_state = Module.cwrap("stockfish_last_main_state", "int", [], { async: false }); + ready = true; + if (ready_cb) ready_cb(); + +} + +function wait_ready(res) { + if (ready) return void res(); + ready_cb = res; +} + +let _listener_id = -1; +let _listener_line_cb = (_) => { }; +let _listener_state_cb = (_) => { }; +let _last_state = -2; +function _stockfish_listener() { + let state = s_state(); + if(state >= 0 && _last_state != state){ + _listener_state_cb(state); + } + _last_state = state; + let out = readline(); + while (out.length != 0) { + _listener_line_cb(out); + out = readline(); + } + _listener_id = setTimeout(_stockfish_listener, 10); +} + +function start_listening(line_cb = (_) => { }, state_cb = (_) => { }) { + requestAnimationFrame(_stockfish_listener); + _listener_line_cb = line_cb; + _listener_state_cb = state_cb; + s_init(); + s_main(); +} + +function stop_listening() { + if (_listener_id != -1) clearTimeout(_listener_id); + _listener_id = -1; +} + +function _read() { + let ptr = s_read(true); + if (ptr == 0) { + return -1; + } + return UTF8ToString(ptr); +} +var read_buffer = ""; +function readline() { + if (!read_buffer.includes("\n")) + while (true) { + let next = _read(); + if (next === -1) break; + read_buffer += next; + if (next.includes("\n")) break; + } + + let index = read_buffer.indexOf("\n"); + let out = ""; + if (index == -1) { + out = read_buffer; + read_buffer = ""; + } else { + out = read_buffer.substring(0, index); + read_buffer = read_buffer.substring(index + 1); + } + return out; +} + +function write(string) { + let buffer = _malloc(string.length + 1); + stringToUTF8(string, buffer, string.length + 1); + let out = s_write(buffer); + _free(buffer); + return out; +} \ No newline at end of file