diff --git a/src/lua/module_loader.cpp b/src/lua/module_loader.cpp index ce30d00..9ca91df 100644 --- a/src/lua/module_loader.cpp +++ b/src/lua/module_loader.cpp @@ -6,6 +6,25 @@ #include "bindings.h" #include "lua_app.h" +// Streamed rather than slurped: holding a whole module as one String needs that many bytes +// contiguous, and once WiFi is up the largest free block is around 40KB even though far +// more than that is free. Lua only ever wants the next few bytes. +namespace { +struct ChunkReader { + File file; + char buf[512]; +}; + +const char* readChunk(lua_State*, void* ud, size_t* size) { + ChunkReader* reader = static_cast(ud); + int read = reader->file.read((uint8_t*)reader->buf, sizeof(reader->buf)); + *size = read > 0 ? (size_t)read : 0; + return read > 0 ? reader->buf : nullptr; +} +} // namespace + +// Whole file in one String, for callers that want the bytes themselves. Scripts go through +// loadScript(), which never materializes the file. bool readScript(const char* path, String& out) { // Probe first: a missed SD.open logs a VFS error, and the searcher misses by design. if (!SD.exists(path)) return false; @@ -14,19 +33,31 @@ bool readScript(const char* path, String& out) { if (file) file.close(); return false; } + // readString() reports an allocation failure by silently returning a partial read, so the + // size is verified rather than trusted: a truncated script is a syntax error at a random + // offset, which says nothing about the real cause. + size_t size = file.size(); out = file.readString(); file.close(); + if (out.length() != size) { + Serial.printf("[lua] %s: short read, %u of %u bytes\n", path, out.length(), (unsigned)size); + return false; + } return true; } int loadScript(lua_State* L, const char* path) { - String source; - if (!readScript(path, source)) { + ChunkReader reader; + if (SD.exists(path)) reader.file = SD.open(path); + if (!reader.file || reader.file.isDirectory()) { + if (reader.file) reader.file.close(); lua_pushfstring(L, "cannot open %s", path); return LUA_ERRFILE; } String chunkname = String("@") + path; - return luaL_loadbuffer(L, source.c_str(), source.length(), chunkname.c_str()); + int status = lua_load(L, readChunk, &reader, chunkname.c_str(), "t"); + reader.file.close(); + return status; } static int l_loadfile(lua_State* L) { @@ -64,10 +95,8 @@ static int l_searcher(lua_State* L) { if (candidate.length() == 0) continue; candidate.replace("?", name); - String source; - if (readScript(candidate.c_str(), source)) { - String chunkname = String("@") + candidate; - if (luaL_loadbuffer(L, source.c_str(), source.length(), chunkname.c_str()) != LUA_OK) { + if (SD.exists(candidate.c_str())) { + if (loadScript(L, candidate.c_str()) != LUA_OK) { return luaL_error(L, "error loading module '%s' from '%s':\n\t%s", luaL_checkstring(L, 1), candidate.c_str(), lua_tostring(L, -1)); } diff --git a/src/main.cpp b/src/main.cpp index 06662b8..e9b36fa 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -50,7 +50,7 @@ void startApp(const String& path) { // Full panel until the app's own status bar module reports its height in load(). tft.setRotation(settings.rotationIndex()); // apps may have rotated the frame tft.resetViewport(); // load() re-clips once the new app's bar reports its height - Serial.printf("launching %s\n", path.c_str()); + Serial.printf("launching %s free=%u largest=%u\n", path.c_str(), ESP.getFreeHeap(), ESP.getMaxAllocHeap()); if (app.load(path.c_str(), path == HOME)) return; if (path == HOME) fallbackScreen("home failed to start"); }