From 71ed09a6f70901c9097973a44b24d6a6ced2834f Mon Sep 17 00:00:00 2001 From: jacqueline Date: Tue, 14 Nov 2023 13:20:04 +1100 Subject: Add two-way databinding for lua, and flesh out the lua statusbar --- src/lua/CMakeLists.txt | 2 +- src/lua/bridge.cpp | 45 ++++++++-- src/lua/include/bridge.hpp | 9 ++ src/lua/include/lua_thread.hpp | 2 + src/lua/include/property.hpp | 47 ++++++++++ src/lua/lua_thread.cpp | 2 +- src/lua/property.cpp | 196 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 295 insertions(+), 8 deletions(-) create mode 100644 src/lua/include/property.hpp create mode 100644 src/lua/property.cpp (limited to 'src/lua') diff --git a/src/lua/CMakeLists.txt b/src/lua/CMakeLists.txt index a2dd8739..f179a881 100644 --- a/src/lua/CMakeLists.txt +++ b/src/lua/CMakeLists.txt @@ -3,7 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-only idf_component_register( - SRCS "lua_thread.cpp" "bridge.cpp" + SRCS "lua_thread.cpp" "bridge.cpp" "property.cpp" INCLUDE_DIRS "include" REQUIRES "drivers" "lvgl" "tinyfsm" "events" "system_fsm" "database" "esp_timer" "battery" "esp-idf-lua" "luavgl") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/lua/bridge.cpp b/src/lua/bridge.cpp index acc64c31..ba6f50b4 100644 --- a/src/lua/bridge.cpp +++ b/src/lua/bridge.cpp @@ -10,10 +10,12 @@ #include #include "esp_log.h" -#include "event_queue.hpp" -#include "lua.h" +#include "lauxlib.h" #include "lua.hpp" #include "lvgl.h" + +#include "event_queue.hpp" +#include "property.hpp" #include "service_locator.hpp" #include "ui_events.hpp" @@ -53,9 +55,7 @@ static auto lua_legacy_ui(lua_State* state) -> int { } static auto get_indexes(lua_State* state) -> int { - lua_pushstring(state, kBridgeKey); - lua_gettable(state, LUA_REGISTRYINDEX); - Bridge* instance = reinterpret_cast(lua_touserdata(state, -1)); + Bridge* instance = Bridge::Get(state); lua_newtable(state); @@ -80,8 +80,14 @@ static auto lua_database(lua_State* state) -> int { return 1; } +auto Bridge::Get(lua_State* state) -> Bridge* { + lua_pushstring(state, kBridgeKey); + lua_gettable(state, LUA_REGISTRYINDEX); + return reinterpret_cast(lua_touserdata(state, -1)); +} + Bridge::Bridge(system_fsm::ServiceLocator& services, lua_State& s) - : services_(services), state_(s) { + : services_(services), state_(s), bindings_(s) { lua_pushstring(&s, kBridgeKey); lua_pushlightuserdata(&s, this); lua_settable(&s, LUA_REGISTRYINDEX); @@ -93,4 +99,31 @@ Bridge::Bridge(system_fsm::ServiceLocator& services, lua_State& s) lua_pop(&s, 1); } +static auto new_property_module(lua_State* state) -> int { + const char* name = luaL_checkstring(state, 1); + luaL_newmetatable(state, name); + + lua_pushstring(state, "__index"); + lua_pushvalue(state, -2); + lua_settable(state, -3); // metatable.__index = metatable + + return 1; +} + +auto Bridge::AddPropertyModule( + const std::string& name, + std::vector>> props) + -> void { + // Create the module (or retrieve it if one with this name already exists) + luaL_requiref(&state_, name.c_str(), new_property_module, true); + + for (const auto& prop : props) { + lua_pushstring(&state_, prop.first.c_str()); + bindings_.Register(&state_, prop.second.get()); + lua_settable(&state_, -3); // metatable.propname = property + } + + lua_pop(&state_, 1); // pop the module off the stack +} + } // namespace lua diff --git a/src/lua/include/bridge.hpp b/src/lua/include/bridge.hpp index 059d0604..26401d14 100644 --- a/src/lua/include/bridge.hpp +++ b/src/lua/include/bridge.hpp @@ -11,19 +11,28 @@ #include "lua.hpp" #include "lvgl.h" +#include "property.hpp" #include "service_locator.hpp" namespace lua { class Bridge { public: + static auto Get(lua_State* state) -> Bridge*; + Bridge(system_fsm::ServiceLocator&, lua_State& s); + auto AddPropertyModule( + const std::string&, + std::vector>>) -> void; + system_fsm::ServiceLocator& services() { return services_; } + PropertyBindings& bindings() { return bindings_; } private: system_fsm::ServiceLocator& services_; lua_State& state_; + PropertyBindings bindings_; }; } // namespace lua diff --git a/src/lua/include/lua_thread.hpp b/src/lua/include/lua_thread.hpp index 381b1bdb..939d0cda 100644 --- a/src/lua/include/lua_thread.hpp +++ b/src/lua/include/lua_thread.hpp @@ -27,6 +27,8 @@ class LuaThread { auto RunScript(const std::string& path) -> bool; + auto bridge() -> Bridge& { return *bridge_; } + private: LuaThread(std::unique_ptr&, std::unique_ptr&, lua_State*); diff --git a/src/lua/include/property.hpp b/src/lua/include/property.hpp new file mode 100644 index 00000000..b6b4718f --- /dev/null +++ b/src/lua/include/property.hpp @@ -0,0 +1,47 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include +#include + +#include "lua.hpp" +#include "lvgl.h" +#include "service_locator.hpp" + +namespace lua { + +using LuaValue = std::variant; + +class Property { + public: + Property() : Property(std::monostate{}) {} + Property(const LuaValue&); + Property(const LuaValue&, std::function); + + auto IsTwoWay() -> bool { return cb_.has_value(); } + + auto PushValue(lua_State& s) -> int; + auto PopValue(lua_State& s) -> bool; + auto Update(const LuaValue& new_val) -> void; + + auto AddLuaBinding(lua_State*, int ref) -> void; + + private: + LuaValue value_; + std::optional> cb_; + std::vector> bindings_; +}; + +class PropertyBindings { + public: + PropertyBindings(lua_State&); + + auto Register(lua_State*, Property*) -> void; +}; + +} // namespace lua diff --git a/src/lua/lua_thread.cpp b/src/lua/lua_thread.cpp index cb7066a5..eb2f5107 100644 --- a/src/lua/lua_thread.cpp +++ b/src/lua/lua_thread.cpp @@ -5,11 +5,11 @@ */ #include "lua_thread.hpp" + #include #include "esp_heap_caps.h" #include "esp_log.h" -#include "lua.h" #include "lua.hpp" #include "luavgl.h" #include "service_locator.hpp" diff --git a/src/lua/property.cpp b/src/lua/property.cpp new file mode 100644 index 00000000..3130077b --- /dev/null +++ b/src/lua/property.cpp @@ -0,0 +1,196 @@ +/* + * Copyright 2023 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "property.hpp" + +#include +#include + +#include "lua.h" +#include "lua.hpp" +#include "lvgl.h" +#include "service_locator.hpp" + +namespace lua { + +static const char kMetatableName[] = "property"; +static const char kBindingsTable[] = "bindings"; + +static auto check_property(lua_State* state) -> Property* { + void* data = luaL_checkudata(state, 1, kMetatableName); + luaL_argcheck(state, data != NULL, 1, "`property` expected"); + return *reinterpret_cast(data); +} + +static auto property_get(lua_State* state) -> int { + Property* p = check_property(state); + p->PushValue(*state); + return 1; +} + +static auto property_set(lua_State* state) -> int { + Property* p = check_property(state); + luaL_argcheck(state, p->IsTwoWay(), 1, "property is read-only"); + bool valid = p->PopValue(*state); + lua_pushboolean(state, valid); + return 1; +} + +static auto property_bind(lua_State* state) -> int { + Property* p = check_property(state); + luaL_checktype(state, 2, LUA_TFUNCTION); + + // Copy the function, as we need to invoke it then store our reference. + lua_pushvalue(state, 2); + // ...and another copy, since we return the original closure. + lua_pushvalue(state, 2); + + // FIXME: This should ideally be lua_pcall, for safety. + p->PushValue(*state); + lua_call(state, 1, 0); // Invoke the initial binding. + + lua_pushstring(state, kBindingsTable); + lua_gettable(state, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] + lua_insert(state, -2); // Move bindings to the bottom, with fn above. + int ref = luaL_ref(state, -2); // bindings[ref] = fn + + p->AddLuaBinding(state, ref); + + // Pop the bindings table, leaving one of the copiesw of the callback fn at + // the top of the stack. + lua_pop(state, 1); + + return 1; +} + +static const struct luaL_Reg kPropertyBindingFuncs[] = {{"get", property_get}, + {"set", property_set}, + {"bind", property_bind}, + {NULL, NULL}}; + +PropertyBindings::PropertyBindings(lua_State& s) { + // Create the metatable responsible for the Property API. + luaL_newmetatable(&s, kMetatableName); + + lua_pushliteral(&s, "__index"); + lua_pushvalue(&s, -2); + lua_settable(&s, -3); // metatable.__index = metatable + + // Add our binding funcs (get, set, bind) to the metatable. + luaL_setfuncs(&s, kPropertyBindingFuncs, 0); + + // Create a weak table in the registry to hold live bindings. + lua_pushstring(&s, kBindingsTable); + lua_newtable(&s); // bindings = {} + + // Metatable for the weak table. Values are weak. + lua_newtable(&s); // meta = {} + lua_pushliteral(&s, "__mode"); + lua_pushliteral(&s, "v"); + lua_settable(&s, -3); // meta.__mode='v' + lua_setmetatable(&s, -2); // setmetatable(bindings, meta) + + lua_settable(&s, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] = bindings +} + +auto PropertyBindings::Register(lua_State* s, Property* prop) -> void { + Property** data = + reinterpret_cast(lua_newuserdata(s, sizeof(Property*))); + *data = prop; + + luaL_setmetatable(s, kMetatableName); +} + +template +inline constexpr bool always_false_v = false; + +Property::Property(const LuaValue& val) : value_(val), cb_() {} + +Property::Property(const LuaValue& val, + std::function cb) + : value_(val), cb_(cb) {} + +auto Property::PushValue(lua_State& s) -> int { + std::visit( + [&](auto&& arg) { + using T = std::decay_t; + if constexpr (std::is_same_v) { + lua_pushnil(&s); + } else if constexpr (std::is_same_v) { + lua_pushinteger(&s, arg); + } else if constexpr (std::is_same_v) { + lua_pushnumber(&s, arg); + } else if constexpr (std::is_same_v) { + lua_pushboolean(&s, arg); + } else if constexpr (std::is_same_v) { + lua_pushstring(&s, arg.c_str()); + } else { + static_assert(always_false_v, "PushValue missing type"); + } + }, + value_); + return 1; +} + +auto Property::PopValue(lua_State& s) -> bool { + LuaValue new_val; + switch (lua_type(&s, 2)) { + case LUA_TNIL: + new_val = std::monostate{}; + break; + case LUA_TNUMBER: + if (lua_isinteger(&s, 2)) { + new_val = lua_tointeger(&s, 2); + } else { + new_val = lua_tonumber(&s, 2); + } + break; + case LUA_TBOOLEAN: + new_val = lua_toboolean(&s, 2); + break; + case LUA_TSTRING: + new_val = lua_tostring(&s, 2); + break; + default: + return false; + } + + if (cb_ && std::invoke(*cb_, new_val)) { + Update(new_val); + return true; + } + return false; +} + +auto Property::Update(const LuaValue& v) -> void { + value_ = v; + + for (int i = bindings_.size() - 1; i >= 0; i--) { + auto& b = bindings_[i]; + + lua_pushstring(b.first, kBindingsTable); + lua_gettable(b.first, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] + int type = lua_rawgeti(b.first, -1, b.second); // push bindings[i] + + // Has closure has been GCed? + if (type == LUA_TNIL) { + // Clean up after ourselves. + lua_pop(b.first, 1); + // Remove the binding. + bindings_.erase(bindings_.begin() + i); + continue; + } + + PushValue(*b.first); // push the argument + lua_call(b.first, 1, 0); // invoke the closure + } +} + +auto Property::AddLuaBinding(lua_State* state, int ref) -> void { + bindings_.push_back({state, ref}); +} + +} // namespace lua -- cgit v1.2.3