/* * Copyright 2023 jacqueline * * SPDX-License-Identifier: GPL-3.0-only */ #include "lua/property.hpp" #include #include #include #include #include #include #include #include "database/track.hpp" #include "drivers/bluetooth_types.hpp" #include "lauxlib.h" #include "lua.h" #include "lua.hpp" #include "lua/lua_database.hpp" #include "lua/lua_thread.hpp" #include "lvgl.h" #include "memory_resource.hpp" #include "system_fsm/service_locator.hpp" #include "types.hpp" namespace lua { static const char kPropertyMetatable[] = "property"; static const char kFunctionMetatable[] = "c_func"; static const char kBindingMetatable[] = "binding"; static const char kBindingsTable[] = "bindings"; static const char kBinderKey[] = "binder"; auto Binding::get(lua_State* L, int idx) -> Binding* { return reinterpret_cast(luaL_testudata(L, idx, kBindingMetatable)); } auto Binding::apply(lua_State* L, int idx) -> bool { Binding* b = get(L, idx); if (b->dirty && b->active) { b->dirty = false; // The binding needs to be reapplied. Push the Lua callback, then its arg. lua_getiuservalue(L, idx, 1); b->property->pushValue(*L); // Invoke the callback. return CallProtected(L, 1, 0) == LUA_OK; } return true; } static auto check_property(lua_State* state) -> Property* { void* data = luaL_checkudata(state, 1, kPropertyMetatable); 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); // Fetch the table of live bindings. lua_pushstring(state, kBindingsTable); lua_gettable(state, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] // Create the userdata holding the new binding's metadata. Binding* binding = reinterpret_cast(lua_newuserdatauv(state, sizeof(Binding), 1)); *binding = Binding{.property = p, .active = true, .dirty = true}; luaL_setmetatable(state, kBindingMetatable); // Associate the callback function with the new binding. lua_pushvalue(state, 2); lua_setiuservalue(state, -2, 1); // Put a reference to the binding into the bindings table, so that we can // look it up later. lua_pushvalue(state, -1); int binding_ref = luaL_ref(state, 3); // Tell the property about the new binding. This was also perform the initial // bind. p->addLuaBinding(state, binding_ref); // Return the only remaining strong reference to the new Binding. return 1; } static auto property_tostring(lua_State* state) -> int { Property* p = check_property(state); p->pushValue(*state); std::stringstream str{}; str << "property { " << luaL_tolstring(state, -1, NULL); if (!p->isTwoWay()) { str << ", read-only"; } str << " }"; lua_settop(state, 0); std::string res = str.str(); lua_pushlstring(state, res.data(), res.size()); return 1; } static const struct luaL_Reg kPropertyBindingFuncs[] = { {"get", property_get}, {"set", property_set}, {"bind", property_bind}, {"__tostring", property_tostring}, {NULL, NULL}}; static auto generic_function_cb(lua_State* state) -> int { lua_pushstring(state, kBinderKey); lua_gettable(state, LUA_REGISTRYINDEX); PropertyBindings* binder = reinterpret_cast(lua_touserdata(state, -1)); size_t* index = reinterpret_cast(luaL_checkudata(state, 1, kFunctionMetatable)); const LuaFunction& fn = binder->GetFunction(*index); // Ensure the C++ function is called with a clean stack; we don't want it to // see the index we just used. lua_remove(state, 1); return std::invoke(fn, state); } PropertyBindings::PropertyBindings() : functions_(&memory::kSpiRamResource) {} auto PropertyBindings::install(lua_State* L) -> void { lua_pushstring(L, kBinderKey); lua_pushlightuserdata(L, this); lua_settable(L, LUA_REGISTRYINDEX); // Create the metatable responsible for the Property API. luaL_newmetatable(L, kPropertyMetatable); lua_pushliteral(L, "__index"); lua_pushvalue(L, -2); lua_settable(L, -3); // metatable.__index = metatable // Add our binding funcs (get, set, bind) to the metatable. luaL_setfuncs(L, kPropertyBindingFuncs, 0); // We've finished setting up the metatable, so pop it. lua_pop(L, 1); // Create the metatable responsible for each Binding. This metatable is empty // as it's only used for identification. luaL_newmetatable(L, kBindingMetatable); lua_pop(L, 1); // Create a weak table in the registry to hold live bindings. lua_pushstring(L, kBindingsTable); lua_newtable(L); // bindings = {} // Metatable for the weak table. Values are weak. lua_newtable(L); // meta = {} lua_pushliteral(L, "__mode"); lua_pushliteral(L, "v"); lua_settable(L, -3); // meta.__mode='v' lua_setmetatable(L, -2); // setmetatable(bindings, meta) lua_settable(L, LUA_REGISTRYINDEX); // REGISTRY[kBindingsTable] = bindings // Create the metatable for C++ functions. luaL_newmetatable(L, kFunctionMetatable); lua_pushliteral(L, "__call"); lua_pushcfunction(L, generic_function_cb); lua_settable(L, -3); // metatable.__call = metatable lua_pop(L, 1); // Clean up the function metatable } auto PropertyBindings::Register(lua_State* s, Property* prop) -> void { Property** data = reinterpret_cast(lua_newuserdata(s, sizeof(uintptr_t))); *data = prop; luaL_setmetatable(s, kPropertyMetatable); } auto PropertyBindings::Register(lua_State* s, LuaFunction fn) -> void { size_t* index = reinterpret_cast(lua_newuserdata(s, sizeof(size_t))); *index = functions_.size(); functions_.push_back(fn); luaL_setmetatable(s, kFunctionMetatable); } auto PropertyBindings::GetFunction(size_t i) -> const LuaFunction& { assert(i < functions_.size()); return functions_[i]; }; template inline constexpr bool always_false_v = false; Property::Property(const LuaValue& val) : value_(memory::SpiRamAllocator().new_object(val)), cb_(), bindings_(&memory::kSpiRamResource) {} Property::Property(const LuaValue& val, std::function cb) : value_(memory::SpiRamAllocator().new_object(val)), cb_(cb), bindings_(&memory::kSpiRamResource) {} auto Property::setDirect(const LuaValue& val) -> void { *value_ = val; reapplyAll(); } auto Property::set(const LuaValue& val) -> bool { if (cb_ && !std::invoke(*cb_, val)) { return false; } setDirect(val); return true; } static void pushTrack(lua_State* L, const audio::TrackInfo& track) { lua_newtable(L); for (const auto& tag : track.tags->allPresent()) { lua_pushstring(L, database::tagName(tag).c_str()); pushTagValue(L, track.tags->get(tag)); lua_settable(L, -3); } if (track.duration) { lua_pushliteral(L, "duration"); lua_pushinteger(L, track.duration.value()); lua_settable(L, -3); } if (track.bitrate_kbps) { lua_pushliteral(L, "bitrate_kbps"); lua_pushinteger(L, track.bitrate_kbps.value()); lua_settable(L, -3); } lua_pushliteral(L, "encoding"); lua_pushstring(L, codecs::StreamTypeToString(track.encoding).c_str()); lua_settable(L, -3); } static void pushDevice(lua_State* L, const drivers::bluetooth::MacAndName& dev) { lua_createtable(L, 0, 4); lua_pushliteral(L, "address"); auto* mac = reinterpret_cast( lua_newuserdata(L, sizeof(drivers::bluetooth::mac_addr_t))); *mac = dev.mac; lua_rawset(L, -3); // What I just did there was perfectly safe. Look, I can prove it: static_assert( std::is_trivially_copy_assignable()); static_assert( std::is_trivially_destructible()); lua_pushliteral(L, "name"); lua_pushlstring(L, dev.name.data(), dev.name.size()); lua_rawset(L, -3); // FIXME: Plumbing through device classes to here could be useful if we ever // want to show cute little icons. } 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_pushboolean(&s, arg); } else if constexpr (std::is_same_v) { lua_pushstring(&s, arg.c_str()); } else if constexpr (std::is_same_v) { pushTrack(&s, arg); } else if constexpr (std::is_same_v) { pushDevice(&s, arg); } else if constexpr (std::is_same_v< T, std::vector>) { lua_createtable(&s, arg.size(), 0); size_t i = 1; for (const auto& dev : arg) { pushDevice(&s, dev); lua_rawseti(&s, -2, i++); } } else { static_assert(always_false_v, "PushValue missing type"); } }, *value_); return 1; } auto popRichType(lua_State* L) -> LuaValue { lua_pushliteral(L, "address"); lua_gettable(L, -2); if (lua_isuserdata(L, -1)) { // This must be a bt device! drivers::bluetooth::mac_addr_t mac = *reinterpret_cast( lua_touserdata(L, -1)); lua_pop(L, 1); lua_pushliteral(L, "name"); lua_gettable(L, -2); std::string name = lua_tostring(L, -1); lua_pop(L, 1); return drivers::bluetooth::MacAndName{.mac = mac, .name = name}; } return std::monostate{}; } auto Property::popValue(lua_State& s) -> bool { LuaValue new_val{std::monostate{}}; if (lua_gettop(&s) >= 2) { switch (lua_type(&s, 2)) { case LUA_TNIL: break; case LUA_TNUMBER: if (lua_isinteger(&s, 2)) { new_val = lua_tointeger(&s, 2); } else { new_val = static_cast(std::round(lua_tonumber(&s, 2))); } break; case LUA_TBOOLEAN: new_val = static_cast(lua_toboolean(&s, 2)); break; case LUA_TSTRING: new_val = lua_tostring(&s, 2); break; default: if (lua_istable(&s, 2)) { new_val = popRichType(&s); if (std::holds_alternative(new_val)) { return false; } } else { return false; } } } return set(new_val); } auto Property::reapplyAll() -> void { for (int i = bindings_.size() - 1; i >= 0; i--) { auto& b = bindings_[i]; if (!applySingle(b.first, b.second, true)) { // Remove the binding if we weren't able to apply it. This is usually due // to the binding getting GC'd. bindings_.erase(bindings_.begin() + i); } } } auto Property::applySingle(lua_State* L, int ref, bool mark_dirty) -> bool { int top = lua_gettop(L); // Push the table of bindings. lua_pushstring(L, kBindingsTable); lua_gettable(L, LUA_REGISTRYINDEX); // Resolve the reference. int type = lua_rawgeti(L, -1, ref); if (type == LUA_TNIL) { lua_settop(L, top); return false; } // Defensively check that the ref was actually for a Binding. Binding* b = Binding::get(L, -1); if (!b) { lua_settop(L, top); return false; } if (mark_dirty) { b->dirty = true; } bool ret = Binding::apply(L, -1); lua_settop(L, top); return ret; } auto Property::addLuaBinding(lua_State* state, int ref) -> void { bindings_.push_back({state, ref}); applySingle(state, ref, true); } } // namespace lua