pre work for web support: bindings separation, native preparations

This commit is contained in:
jusax23 2024-02-23 18:58:31 +01:00
parent 68d1e05288
commit 595501ca1d
Signed by: jusax23
GPG key ID: 4A6CED31031AE931
12 changed files with 316 additions and 210 deletions

View file

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

View file

@ -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<Stockfish>? completer;
final _state = StockfishStateClass();
final _state = _StockfishState();
final _stdoutController = StreamController<String>.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<StockfishState> get state => _state;
/// The standard output stream.
Stream<String> get stdout => _stdoutController.stream;
Stream<String> 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<Char>();
_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<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;
}
class _StockfishState extends ChangeNotifier
implements ValueListenable<StockfishState> {
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<Utf8>().length;
newContentCharList = Uint8List.view(
pointer.cast<Uint8>().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<bool> _spawnIsolates(List<SendPort> 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;
}

View file

@ -0,0 +1,9 @@
import 'dart:async';
abstract class StockfishChessEngineAbstractBindings {
final stdoutController = StreamController<String>.broadcast();
Future<int> stockfishMain(Function active);
void cleanUp(int exitCode);
void write(String line);
Stream<String> get read => stdoutController.stream;
}

View file

@ -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<T> Function<T extends ffi.NativeType>(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<T> Function<T extends ffi.NativeType>(String symbolName)
lookup)
: _lookup = lookup;
@ -56,15 +57,19 @@ class StockfishChessEngineBindings {
late final _stockfish_stdin_write = _stockfish_stdin_writePtr
.asFunction<int Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> stockfish_stdout_read() {
return _stockfish_stdout_read();
ffi.Pointer<ffi.Char> stockfish_stdout_read(
int trygetline,
) {
return _stockfish_stdout_read(
trygetline,
);
}
late final _stockfish_stdout_readPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function(ffi.Int)>>(
'stockfish_stdout_read');
late final _stockfish_stdout_read =
_stockfish_stdout_readPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
late final _stockfish_stdout_read = _stockfish_stdout_readPtr
.asFunction<ffi.Pointer<ffi.Char> Function(int)>();
}
typedef ssize_t = __ssize_t;

View file

@ -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<int> stockfishMain(Function active) {
final completer = Completer<int>();
_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<Char>();
_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<Utf8>().length;
newContentCharList = Uint8List.view(
pointer.cast<Uint8>().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<bool> _spawnIsolates(List<SendPort> 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;
}

View file

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

View file

@ -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<int> stockfishMain(Function active) {
// TODO: implement stockfishMain
final completer = Completer<int>();
//completer.complete(0);
active();
return completer.future;
}
@override
void write(String line) {
// TODO: implement write
}
}

View file

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

View file

@ -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() {
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;
}

View file

@ -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();
FFI_PLUGIN_EXPORT const char*
stockfish_stdout_read(int trygetline);

View file

@ -2,7 +2,8 @@
bool FakeStream::try_get_line(std::string& val) {
std::unique_lock<std::mutex> 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;
}

View file

@ -6,18 +6,24 @@
#include <queue>
#include <sstream>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/threading.h>
#endif
template <typename T>
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 <typename T>
FakeStream& operator<<(const T& val) {
if (closed) return *this;
if (closed)
return *this;
std::lock_guard<std::mutex> lock(mutex_guard);
string_queue.push(stringify(val));
mutex_signal.notify_one();
@ -25,29 +31,46 @@ class FakeStream {
};
template <typename T>
FakeStream& operator>>(T& val) {
if (closed) return *this;
if (closed)
return *this;
std::unique_lock<std::mutex> lock(mutex_guard);
#ifdef __EMSCRIPTEN__
if (emscripten_is_main_runtime_thread()) {
lock.unlock();
while (true) {
lock = std::unique_lock<std::mutex>(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<std::string> 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;