commit ecb04c6b7fbf5d06c2fdce5128e83575cac2bd21
parent c6a77f30f64be73a5e55e06336975f2ecfbb2324
Author: Sebastiano Tronto <sebastiano@tronto.net>
Date: Fri, 23 May 2025 17:06:02 +0200
Web version (work in progress)
Diffstat:
10 files changed, 247 insertions(+), 32 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -22,6 +22,8 @@ runcpp
runtest.js
runtest.wasm
web/*.wasm
+web/http/*.wasm
+web/http/*.mjs
web/nissy_web_module.*
runtool
tools/.DS_Store
diff --git a/README.md b/README.md
@@ -251,6 +251,19 @@ using [nodejs](https://nodejs.org):
$ node web/examples/[filename]
```
+An example web app running nissy can be found in the `web/http` folder.
+You can run a web server in that folder to check it out, but you need
+to set some extra headers to make it work. For example, if you are using
+[darkhttpd](https://github.com/emikulic/darkhttpd) you can start the server
+with the following command:
+
+```
+$ darkhttpd web/http/ \
+ --header 'Cross-Origin-Opener-Policy: same-origin' \
+ --header 'Cross-Origin-Embedder-Policy: require-corp' \
+ --mimetypes web/http/mime
+```
+
## Cube format
This format is a "base 32" encoding of the cube. It is not meant to be
diff --git a/build b/build
@@ -129,9 +129,12 @@ CPPFLAGS="-std=c++20 -pthread"
# Build flags for emscripten (WASM target)
WASMCFLAGS="-std=c11 -fPIC -D_POSIX_C_SOURCE=199309L -pthread"
WASMMFLAGS="-DTHREADS=$THREADS -DWASMSIMD"
+#WASMLINKFLAGS="--no-entry -sEXPORT_NAME='Nissy' -sMODULARIZE \
+# -sALLOW_MEMORY_GROWTH -sSTACK_SIZE=5MB -sPTHREAD_POOL_SIZE=$THREADS \
+# -sNODERAWFS -sASYNCIFY -sLINKABLE -sEXPORT_ALL"
WASMLINKFLAGS="--no-entry -sEXPORT_NAME='Nissy' -sMODULARIZE \
-sALLOW_MEMORY_GROWTH -sSTACK_SIZE=5MB -sPTHREAD_POOL_SIZE=$THREADS \
- -sLINKABLE -sEXPORT_ALL"
+ -sASYNCIFY -sLINKABLE -sEXPORT_ALL"
if (command -v "python3-config" >/dev/null 2>&1) ; then
PYTHON3_INCLUDES="$(python3-config --includes)"
@@ -207,7 +210,7 @@ odflags() {
build_clean() {
run rm -rf -- *.o *.so *.a run runtest runtool runcpp \
- web/nissy_web_module.*
+ web/nissy_web_module.* web/*.wasm web/http/*.mjs web/http/*.wasm
}
build_nissy() {
@@ -233,7 +236,7 @@ build_sharedlib() {
}
build_shell() {
- build_nissy
+ build_nissy || exit 1
run $CC $CFLAGS $WFLAGS $(odflags) -o run nissy.o shell/shell.c
}
@@ -243,7 +246,7 @@ build_python() {
echo "Cannot build python module"
exit 1
fi
- build_nissy
+ build_nissy || exit 1
run $CC $CFLAGS $WFLAGS $PYTHON3_INCLUDES $(odflags) -shared \
-o nissy_python_module.so nissy.o python/nissy_module.c
}
@@ -255,7 +258,7 @@ build_cpp() {
echo "usage: ./build cpp FILES"
fi
- build_nissy
+ build_nissy || exit 1
run $CXX -std=c++20 -o runcpp cpp/nissy.cpp nissy.o $@
run ./runcpp
}
@@ -265,13 +268,20 @@ build_web() {
validate_threads "$THREADS"
validate_arch "$ARCH"
+ obj="nissy_web_module"
+
run $EMCC $WASMCFLAGS $WFLAGS $WASMMFLAGS $(odflags) -c \
- -o nissy.o src/nissy.c
- run $EMCC -lembind -lidbfs.js -lnodefs.js \
+ -o nissy.o src/nissy.c || exit 1
+ #run $EMCC -lembind -lidbfs.js -lnodefs.js -lnoderawfs.js \
+ # $CPPFLAGS $(odflags) $WASMLINKFLAGS \
+ # --js-library web/callback.js -o web/"$obj".mjs \
+ # cpp/nissy.cpp web/storage.cpp web/adapter.cpp nissy.o
+ run $EMCC -lembind -lidbfs.js \
$CPPFLAGS $(odflags) $WASMLINKFLAGS \
- --js-library web/callback.js \
- -o web/nissy_web_module.mjs \
- cpp/nissy.cpp web/adapter.cpp nissy.o
+ --js-library web/callback.js -o web/"$obj".mjs \
+ cpp/nissy.cpp web/storage.cpp web/adapter.cpp nissy.o || exit 1
+ cp web/"$obj".mjs web/http
+ cp web/"$obj".wasm web/http
}
dotest() {
@@ -310,7 +320,7 @@ build_test_generic() {
shift
fi
debug="yes"
- build_$obj
+ build_$obj || exit 1
for t in test/*; do
dotest || exit 1
done
@@ -323,7 +333,7 @@ build_test() {
testbuild="$CC $CFLAGS $WFLAGS $DFLAGS $MFLAGS -o $testobj"
testrun="./$testobj"
build_test_generic $@
- rm runtest
+ rm -f runtest
}
build_webtest() {
diff --git a/web/adapter.cpp b/web/adapter.cpp
@@ -1,11 +1,15 @@
#include "../cpp/nissy.h"
+#include "storage.h"
+#include <emscripten.h>
#include <emscripten/bind.h>
#include <map>
#include <set>
#include <string>
#include <vector>
+EM_ASYNC_JS(void, fake_async, (), {});
+
extern "C" {
extern int addCallbackFunction(/* args intentionally unspecified */);
extern void callFunction(int, const char *);
@@ -47,26 +51,43 @@ const std::set<std::string> available_solvers
std::map<std::string, nissy::solver> loaded_solvers;
// TODO: this should ask the user if they want to download or generate.
-// TODO: this should also save the data to a file (IDBFS / NODEFS)
bool init_solver(const std::string& name)
{
auto se = nissy::solver::get(name);
nissy::solver solver = std::get<nissy::solver>(se);
- log("Generating data for solver " + solver.name + "\n");
- auto err = solver.generate_data();
- if (!err.ok()) {
- log("Error generating the data!\n");
- return false;
+
+ solver.data.resize(solver.size);
+ if (storage::read(solver.id, solver.size,
+ reinterpret_cast<char *>(solver.data.data()))) {
+ log("Data for solver " + solver.name + " read from storage\n");
+ } else {
+ log("Could not read data for solver " + solver.name +
+ " from storage, generating it\n");
+ auto err = solver.generate_data();
+
+ if (!err.ok()) {
+ log("Error generating the data!\n");
+ return false;
+ }
}
+
log("Checking data integrity "
"(this is done only once per session per solver)...\n");
if (!solver.check_data().ok()) {
- log("Error generating the data!\n");
+ log("Error! Data is corrupted!\n");
return false;
}
loaded_solvers.insert({name, solver});
- log("Data generated successfully, but not saved "
- "(feature not yet available)\n");
+
+ if (storage::write(solver.id, solver.size,
+ reinterpret_cast<const char *>(solver.data.data()))) {
+ log("Data for solver " + solver.name + " stored\n");
+ } else {
+ log("Error storing the data (the solver is usable, "
+ "but the data will have to be re-generated next "
+ "time you want to use it)");
+ }
+
return true;
}
@@ -87,17 +108,31 @@ bool solver_valid(const std::string& name)
int poll_status(void *arg)
{
+ return nissy::status::RUN.value;
+/*
+TODO: reintroduce poll status
int id = *(int *)arg;
if (id == -1)
return nissy::status::RUN.value;
return callFunctionInt(id);
+*/
}
+#if 0
+
nissy::solver::solve_result solve(std::string name,
nissy::cube cube, nissy::nissflag nissflag, unsigned minmoves,
unsigned maxmoves, unsigned maxsols, unsigned optimal, unsigned threads,
int poll_status_id)
{
+ // Here we use a dirty trick to make this function always return the
+ // same kind of JavaScript object. If we did not do this, the returned
+ // object would be a Promise on the first run of the solver for each
+ // session (because when loading the table some async JS code is
+ // called), and a regular object otherwise.
+ // TODO figure out if there is a better way to do this.
+ fake_async();
+
if (!solver_valid(name))
return nissy::solver::solve_result
{.err = nissy::error::INVALID_SOLVER};
@@ -106,6 +141,35 @@ nissy::solver::solve_result solve(std::string name,
maxmoves, maxsols, optimal, threads, NULL, &poll_status_id);
}
+#else
+
+std::string solve(std::string name,
+ nissy::cube cube, nissy::nissflag nissflag, unsigned minmoves,
+ unsigned maxmoves, unsigned maxsols, unsigned optimal, unsigned threads,
+ int poll_status_id)
+{
+ // Here we use a dirty trick to make this function always return the
+ // same kind of JavaScript object. If we did not do this, the returned
+ // object would be a Promise on the first run of the solver for each
+ // session (because when loading the table some async JS code is
+ // called), and a regular object otherwise.
+ // TODO figure out if there is a better way to do this.
+ fake_async();
+
+ if (!solver_valid(name))
+ return "";
+/*
+ return nissy::solver::solve_result
+ {.err = nissy::error::INVALID_SOLVER};
+*/
+
+ return loaded_solvers.at(name).solve(cube, nissflag, minmoves,
+ maxmoves, maxsols, optimal, threads, NULL, &poll_status_id)
+ .solutions[0];
+}
+
+#endif
+
EMSCRIPTEN_BINDINGS(Nissy)
{
emscripten::class_<nissy::nissflag>("NissFlag")
@@ -134,13 +198,11 @@ EMSCRIPTEN_BINDINGS(Nissy)
emscripten::constant("statusRUN", nissy::status::RUN.value);
emscripten::constant("statusSTOP", nissy::status::STOP.value);
emscripten::constant("statusPAUSE", nissy::status::PAUSE.value);
- /*
emscripten::class_<nissy::status>("Status")
.class_property("run", &nissy::status::RUN)
.class_property("stop", &nissy::status::STOP)
.class_property("pause", &nissy::status::PAUSE)
;
- */
emscripten::class_<nissy::cube>("Cube")
.constructor<>()
@@ -150,10 +212,17 @@ EMSCRIPTEN_BINDINGS(Nissy)
.function("toString", &nissy::cube::to_string)
;
+ emscripten::register_vector<std::string>("StringVector");
+ emscripten::value_array<nissy::solver::solve_result>("SolveResult")
+ .element(&nissy::solver::solve_result::err)
+ .element(&nissy::solver::solve_result::solutions)
+ ;
+/*
emscripten::class_<nissy::solver::solve_result>("SolveResult")
.property("err", &nissy::solver::solve_result::err)
.property("solutions", &nissy::solver::solve_result::solutions)
;
+*/
emscripten::function("countMoves", &nissy::count_moves);
emscripten::function("solve", &solve,
diff --git a/web/callback.js b/web/callback.js
@@ -2,20 +2,44 @@ addToLibrary({
cbfl: [],
+validateCallbackId__deps: [ 'cbfl' ],
+validateCallbackId: function(i) {
+ if (i < 0) {
+ console.log("--- WARNING ---");
+ console.log("Trying to access callback function of invalid id " + i);
+ console.log("--- WARNING ---");
+ return false;
+ }
+
+ if (i >= _cbfl.length) {
+ console.log("--- WARNING ---");
+ console.log("Trying to access callback function " + i + ", but only "
+ + _cbfl.length + " have been registered. This may be caused by a "
+ + "call outside of the main thread.");
+ console.log("--- WARNING ---");
+ return false;
+ }
+
+ return true;
+},
+
addCallbackFunction__deps: [ 'cbfl' ],
addCallbackFunction: function(f) {
_cbfl.push(f)
return _cbfl.length - 1
},
-callFunction__deps: [ 'cbfl' ],
+callFunction__deps: [ 'cbfl', 'validateCallbackId' ],
callFunction: function(id, arg) {
- _cbfl[id](UTF8ToString(arg))
+ if (_validateCallbackId(id))
+ _cbfl[id](UTF8ToString(arg));
},
-callFunctionInt__deps: [ 'cbfl' ],
+callFunctionInt__deps: [ 'cbfl', 'validateCallbackId' ],
callFunctionInt: function(id) {
- return _cbfl[id]()
+ if (_validateCallbackId(id))
+ return _cbfl[id]();
+ return 0;
},
});
diff --git a/web/examples/solve.mjs b/web/examples/solve.mjs
@@ -1,10 +1,12 @@
import Nissy from '../nissy_web_module.mjs'
-const nissy = await Nissy()
+const nissy = await Nissy();
-nissy.setLogger(nissy._addCallbackFunction(console.log))
+var log = process.stdout.write.bind(process.stdout);
+//var log = console.log
+nissy.setLogger(nissy._addCallbackFunction(log))
-var cube = new nissy.Cube()
-cube.move('R\' U\' F')
+var cube = new nissy.Cube();
+cube.move('R\' U\' F');
-nissy.solve('h48h0k4', cube, nissy.NissFlag.normal, 0, 8, 2, 99, 4, -1)
+nissy.solve('h48h0k4', cube, nissy.NissFlag.normal, 0, 8, 2, 99, 4, -1);
diff --git a/web/http/index.html b/web/http/index.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html lang="en-US">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width" />
+ <title>Nissy - H48 solver POC</title>
+ <script type="module" src="./solve.mjs"></script>
+ </head>
+ <body>
+ <input id="scrambleText" placeholder="Type the scramble here...">
+ <select id="solverSelector">
+ <option value="h48h0k4" selected="selected">h48 h=0 k=4 (59Mb)</option>
+ <option value="h48h3k2">h48 h=3 k=2 (283Mb)</option>
+ <option value="h48h7k2">h48 h=7 k=2 (3.6Gb)</option>
+ </select>
+ <button id="solveButton">Solve!</button>
+ <p>Solution:</p>
+ <p id="solution"></p>
+ </body>
+</html>
diff --git a/web/http/mime b/web/http/mime
@@ -0,0 +1 @@
+text/javascript mjs
diff --git a/web/storage.cpp b/web/storage.cpp
@@ -0,0 +1,67 @@
+#include "storage.h"
+
+#include "emscripten.h"
+#include <filesystem>
+#include <fstream>
+
+EM_JS(int, inbrowser, (), { return typeof window !== 'undefined'; });
+EM_JS(int, inworker, (), { return typeof WorkerGlobalScope !== 'undefined' &&
+ self instanceof WorkerGlobalScope; });
+
+std::string getprefix() {
+ return inbrowser() || inworker() ? "/tables/" : "./tables/";
+}
+
+EM_ASYNC_JS(int, loadfs, (), {
+ const dir = '/tables';
+ const inBrowser = typeof window !== 'undefined';
+ const inWorker = typeof WorkerGlobalScope !== 'undefined' &&
+ self instanceof WorkerGlobalScope;
+
+ if (!(inBrowser || inWorker)) return;
+
+ if (!FS.analyzePath(dir).exists)
+ FS.mkdir(dir);
+
+ if (FS.analyzePath(dir).object.mount.mountpoint != dir) {
+ FS.mount(IDBFS, { autoPersist: true }, dir);
+
+ await new Promise((resolve, reject) => {
+ FS.syncfs(true, function (err) {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(true);
+ }
+ });
+ });
+ }
+});
+
+bool storage::read(std::string key, size_t data_size, char *data)
+{
+ loadfs();
+
+ std::filesystem::path path(getprefix() + key);
+ if (!std::filesystem::exists(path))
+ return false;
+
+ std::ifstream ifs(path, std::ios::binary);
+ ifs.read(data, data_size);
+ ifs.close();
+
+ return !ifs.fail();
+}
+
+bool storage::write(std::string key, size_t data_size, const char *data)
+{
+ loadfs();
+
+ std::filesystem::path path(getprefix() + key);
+
+ std::ofstream ofs(path, std::ios::binary);
+ ofs.write(data, data_size);
+ ofs.close();
+
+ return !ofs.fail();
+}
diff --git a/web/storage.h b/web/storage.h
@@ -0,0 +1,7 @@
+#include <string>
+#include <fstream>
+
+namespace storage {
+ bool read(std::string, size_t, char *);
+ bool write(std::string, size_t, const char *);
+}