js: Load save files into the C side incrementally

Before this commit, JavaScript Puzzles loaded a save file by pushing the
entire file onto the Emscripten stack and then reading it from there.
This worked tolerably for typical save files, but Emscripten's stack
defaults to only having 64 kiB of space.  That meant that trying to load
something that wasn't a real save file tended to cause a stack overflow.
I expect that at least some real save files would suffer from the same
problem.

The stack overflow would generally cause a JavaScript exception and then
leave the stack pointer outside the stack, so that any future attempt to
call into C would fail as well.

To fix this, arrange that the C function for reading data from the save
file calls out to JavaScript.  The JavaScript can then copy just the
requested data into the caller's buffer.  We can't pass a JavaScript
function pointer to C, but since only one file can be loaded at a time,
we can just have a global variable that's the current loading callback.

There might still be a problem if you try to load a stupendously large
file, since I think FileReader.readAsArrayBuffer() reads the whole file
into the browser's RAM.  It works on my laptop with files up to a few
hundred megabytes, though.
This commit is contained in:
Ben Harris
2023-04-03 15:16:35 +01:00
parent 8d3a93ce47
commit 36c282aaa9
3 changed files with 37 additions and 16 deletions

View File

@ -135,6 +135,12 @@ var dlg_return_funcs = null;
// pass back the final value in each dialog control.
var dlg_return_sval, dlg_return_ival;
// Callback for reading from a savefile. This will be filled in with
// a suitable closure by the JS loading code and called by
// js_savefile_read(). This assumes that only one file can be in the
// process of loading at a time.
var savefile_read_callback;
// The <ul> object implementing the game-type drop-down, and a list of
// the sub-lists inside it. Used by js_add_preset().
var gametypelist = document.getElementById("gametype");
@ -423,7 +429,7 @@ function initPuzzle() {
// 'number' is used for C pointers
var get_save_file = Module.cwrap('get_save_file', 'number', []);
var free_save_file = Module.cwrap('free_save_file', 'void', ['number']);
var load_game = Module.cwrap('load_game', 'void', ['array', 'number']);
var load_game = Module.cwrap('load_game', 'void', []);
if (save_button) save_button.onclick = function(event) {
if (dlg_dimmer === null) {
@ -457,8 +463,17 @@ function initPuzzle() {
var file = input.files.item(0);
var reader = new FileReader();
reader.addEventListener("loadend", function() {
var array = new Uint8Array(reader.result);
load_game(array, array.length);
var pos = 0;
savefile_read_callback = function(buf, len) {
if (pos + len > reader.result.byteLength)
return false;
writeArrayToMemory(
new Int8Array(reader.result, pos, len), buf);
pos += len;
return true;
}
load_game();
savefile_read_callback = null;
});
reader.readAsArrayBuffer(file);
}