web implementation, missing js and wasm artifacts

This commit is contained in:
jusax23 2024-02-25 00:39:40 +01:00
parent bdeee62de8
commit 8f9f220c40
Signed by: jusax23
GPG key ID: 4A6CED31031AE931
12 changed files with 306 additions and 30 deletions

5
.gitignore vendored
View file

@ -45,3 +45,8 @@ src/-lstdc++.res
**/*.nnue
flags.txt
CMakeFiles
cmake_install.cmake
CMakeCache.txt
Makefile

View file

@ -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.

View file

@ -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';
class Stockfish {
final _state = StockfishStateClass();
final StockfishChessEngineAbstractBindings _bindings =
StockfishChessEngineBindings();
class Stockfish {
final _state = StockfishStateClass();
Stockfish._() {
Stockfish._({Completer<Stockfish>? 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<StockfishState> 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<Stockfish> stockfishAsync() {
if (Stockfish._instance != null) {
return Future.error(StateError('Only one instance can be used at a time'));
}
final completer = Completer<Stockfish>();
Stockfish._instance = Stockfish._(completer: completer);
return completer.future;
}

View file

@ -43,6 +43,25 @@ class StockfishChessEngineCBindings {
_lookup<ffi.NativeFunction<ffi.Int Function()>>('stockfish_main');
late final _stockfish_main = _stockfish_mainPtr.asFunction<int Function()>();
void stockfish_start_main() {
return _stockfish_start_main();
}
late final _stockfish_start_mainPtr =
_lookup<ffi.NativeFunction<ffi.Void Function()>>('stockfish_start_main');
late final _stockfish_start_main =
_stockfish_start_mainPtr.asFunction<void Function()>();
int stockfish_last_main_state() {
return _stockfish_last_main_state();
}
late final _stockfish_last_main_statePtr =
_lookup<ffi.NativeFunction<ffi.Int Function()>>(
'stockfish_last_main_state');
late final _stockfish_last_main_state =
_stockfish_last_main_statePtr.asFunction<int Function()>();
int stockfish_stdin_write(
ffi.Pointer<ffi.Char> data,
) {
@ -74,3 +93,4 @@ class StockfishChessEngineCBindings {
typedef ssize_t = __ssize_t;
typedef __ssize_t = ffi.Long;
typedef Dart__ssize_t = int;

View file

@ -64,6 +64,7 @@ class StockfishChessEngineBindings
onError: (error) {
developer.log('The init isolate encountered an error $error',
name: 'Stockfish');
completer.completeError(error);
cleanUp(1);
},
);

View file

@ -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<void>? loadJs;
StockfishChessEngineBindings() {
loadJs = loadJsFileIfNeeded();
}
@override
Future<int> stockfishMain(Function active) {
// TODO: implement stockfishMain
void cleanUp(int exitCode) {
stdoutController.close();
js.context.callMethod("stop_listening", []);
}
@override
Future<int> stockfishMain(Function active) async {
if (loadJs != null) {
await loadJs;
loadJs = null;
}
final completer = Completer<int>();
//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<void> 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<dynamic> _stockfishWaitReady() {
final completer = Completer<dynamic>();
js.context.callMethod('wait_ready', [
completer.complete,
]);
return completer.future;
}

View file

@ -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

View file

@ -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")

View file

@ -14,6 +14,7 @@
#include "fixes.h"
#include "stockfish.h"
#include <thread>
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;

View file

@ -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"

52
web/CMakeLists.txt Normal file
View file

@ -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)

93
web/js_bindings.js Normal file
View file

@ -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;
}