rockbox/apps/plugins/lua/rocklib_events.c
William Wilgus 7f1e6b4638 lua rockev cleanup
with the addition of suspending all events on thread start and exit
we don't really need to block on THREAD_QUIT in so many places

Removed suspend clearing on event unregister and updated comments

Change-Id: Id9c6a460def558c5331ee292035691a9f82b2c43
2019-10-07 22:07:22 -05:00

656 lines
20 KiB
C

/***************************************************************************
* __________ __ ___.
* Open \______ \ ____ ____ | | _\_ |__ _______ ___
* Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ /
* Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < <
* Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \
* \/ \/ \/ \/ \/
* $Id$
*
* Copyright (C) 2019 William Wilgus
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
****************************************************************************/
/* lua events from rockbox *****************************************************
* This library allows events to be subscribed / recieved within a lua script
* most events in rb are synchronous so flags are set and later checked by a
* secondary thread to make them (semi?) asynchronous.
*
* There are a few caveats to be aware of:
* FIRST, The main lua state is halted till the lua callback(s) are finished
* Yielding will not return control to your script from within a callback
* Also, subsequent callbacks may be delayed by the code in your lua callback
* SECOND, You must store the value returned from the event_register function
* you might get away with it for a bit but gc will destroy your callback
* eventually if you do not store the event
* THIRD, You only get one cb per event type
* ["action", "button", "custom", "playback", "timer"]
* (Re-registration of an event overwrites the previous one)
*
* Usage:
* possible events =["action", "button", "custom", "playback", "timer"]
*
* local ev = rockev.register("event", cb_function, [timeout / flags])
* cb_function([id] [, data]) ... end
*
*
* rockev.trigger("event", [true/false], [id])
* sets an event to triggered,
* NOTE!, CUSTOM_EVENT must be unset manually
* id is only passed to callback by custom and playback events
*
* rockev.suspend(["event"/nil][true/false]) passing nil suspends all events
* stops event from executing, any event before re-enabling will be lost.
* Passing false will clear the suspend as will
* unregistering or re-registering an event (except suspend all)
*
* rockev.unregister(evX)
* Use unregister(evX) to remove an event
* Unregistering is not necessary before script end, it will be
* cleaned up on script exit
*
*******************************************************************************
* *
*/
#define LUA_LIB
#define _ROCKCONF_H_ /* We don't need strcmp() etc. wrappers */
#include "lua.h"
#include "lauxlib.h"
#include "plugin.h"
#include "rocklib_events.h"
#define EVENT_METATABLE "event metatable"
#define EVENT_THREAD LUA_ROCKEVENTSNAME ".thread"
#define EV_STACKSZ DEFAULT_STACK_SIZE
#define LUA_SUCCESS 0
#define EV_TIMER_FREQ (TIMER_FREQ / HZ)
#define EV_TICKS (HZ / 5)
#define EV_INPUT (HZ / 4)
//#define DEBUG_EV
enum e_thread_state_flags{
THREAD_QUIT = 0x0,
THREAD_YIELD = 0x1,
THREAD_TIMEREVENT = 0x2,
THREAD_PLAYBKEVENT = 0x4,
THREAD_ACTEVENT = 0x8,
THREAD_BUTEVENT = 0x10,
THREAD_CUSTOMEVENT = 0x20,
//THREAD_AVAILEVENT = 0x40,
//THREAD_AVAILEVENT = 0x80,
/* thread state holds 3 status items using masks and bitshifts */
THREAD_STATEMASK = 0x00FF,
THREAD_SUSPENDMASK = 0xFF00,
THREAD_INPUTMASK = 0xFF0000,
};
enum {
ACTEVENT = 0,
BUTEVENT,
CUSTOMEVENT,
PLAYBKEVENT,
TIMEREVENT,
EVENT_CT
};
static const unsigned char thread_ev_states[EVENT_CT] =
{
[ACTEVENT] = THREAD_ACTEVENT,
[BUTEVENT] = THREAD_BUTEVENT,
[CUSTOMEVENT] = THREAD_CUSTOMEVENT,
[PLAYBKEVENT] = THREAD_PLAYBKEVENT,
[TIMEREVENT] = THREAD_TIMEREVENT,
};
static const char *const ev_map[EVENT_CT] =
{
[ACTEVENT] = "action",
[BUTEVENT] = "button",
[CUSTOMEVENT] = "custom",
[PLAYBKEVENT] = "playback",
[TIMEREVENT] = "timer",
};
struct cb_data {
int cb_ref;
unsigned long id;
void *data;
};
struct event_data {
/* lua */
lua_State *L;
lua_State *NEWL;
/* rockbox */
unsigned int thread_id;
int thread_state;
long *event_stack;
long timer_ticks;
short freq_input;
short next_input;
short next_event;
/* callbacks */
struct cb_data *cb[EVENT_CT];
};
static struct event_data ev_data;
static struct mutex rev_mtx SHAREDBSS_ATTR;
#ifdef DEBUG_EV
static int dbg_hook_calls = 0;
#endif
static inline bool has_event(int ev_flag)
{
return ((THREAD_STATEMASK & (ev_data.thread_state & ev_flag)) == ev_flag);
}
static inline bool is_suspend(int ev_flag)
{
ev_flag <<= 8;
return ((THREAD_SUSPENDMASK & (ev_data.thread_state & ev_flag)) == ev_flag);
}
static void init_event_data(lua_State *L, struct event_data *ev_data)
{
/* lua */
ev_data->L = L;
//ev_data->NEWL = NULL;
/* rockbox */
ev_data->thread_id = UINT_MAX;
ev_data->thread_state = THREAD_YIELD | THREAD_SUSPENDMASK;
//ev_data->event_stack = NULL;
//ev_data->timer_ticks = 0;
ev_data->freq_input = EV_INPUT;
ev_data->next_input = EV_INPUT;
ev_data->next_event = EV_TICKS;
/* callbacks */
for (int i= 0; i < EVENT_CT; i++)
ev_data->cb[i] = NULL;
}
/* lock and unlock routines allow us to execute the event thread without
* trashing the lua state on error, yield, or sleep in the callback functions */
static inline void rev_lock_mtx(void)
{
rb->mutex_lock(&rev_mtx);
}
static inline void rev_unlock_mtx(void)
{
rb->mutex_unlock(&rev_mtx);
}
static void lua_interrupt_callback( lua_State *L, lua_Debug *ar)
{
(void) L;
(void) ar;
#ifdef DEBUG_EV
dbg_hook_calls++;
#endif
rb->yield();
rev_lock_mtx();
rev_unlock_mtx(); /* must wait till event thread is done to continue */
#ifdef DEBUG_EV
rb->splashf(0, "spin %d, hooked %d", dbg_hook_calls, (lua_gethookmask(L) != 0));
unsigned char delay = -1;
/* we can't sleep or yield without affecting count so lets spin in a loop */
while(delay > 0) {delay--;}
if (lua_gethookmask(L) == 0)
dbg_hook_calls = 0;
#endif
/* if callback error, pass error to the main lua state */
if (lua_status(ev_data.NEWL) != LUA_SUCCESS)
luaL_error (L, lua_tostring (ev_data.NEWL, -1));
}
static void lua_interrupt_set(lua_State *L, bool is_enabled)
{
const int hookmask = LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT;
if (is_enabled)
lua_sethook(L, lua_interrupt_callback, hookmask, 1 );
else
lua_sethook(L, NULL, 0, 0 );
}
static int lua_rev_callback(lua_State *L, struct cb_data *cbd)
{
int lua_status = LUA_ERRRUN;
if (L != NULL)
{
/* load cb function from lua registry */
lua_rawgeti(L, LUA_REGISTRYINDEX, cbd->cb_ref);
lua_pushinteger(L, cbd->id);
lua_pushlightuserdata (L, cbd->data);
lua_status = lua_resume(L, 2);
if (lua_status == LUA_YIELD) /* coroutine.yield() disallowed */
luaL_where(L, 0); /* push error string on stack */
}
return lua_status;
}
static void event_thread(void)
{
unsigned long action;
int event;
int ev_flag;
while(ev_data.thread_state != THREAD_QUIT && lua_status(ev_data.L) == LUA_SUCCESS)
{
rev_lock_mtx();
lua_interrupt_set(ev_data.L, true);
for (event = 0; event < EVENT_CT; event++)
{
ev_flag = thread_ev_states[event];
if (!has_event(ev_flag) || is_suspend(ev_flag))
continue; /* check next event */
ev_data.thread_state &= ~(ev_flag); /* event handled */
switch (event)
{
case ACTEVENT:
action = get_plugin_action(TIMEOUT_NOBLOCK, true);
if (action == ACTION_UNKNOWN)
continue; /* check next event */
else if (action == ACTION_NONE)
{
/* only send ACTION_NONE once */
if (ev_data.cb[ACTEVENT]->id == ACTION_NONE ||
rb->button_status() != 0)
continue; /* check next event */
}
ev_data.cb[ACTEVENT]->id = action;
break;
case BUTEVENT:
ev_data.cb[BUTEVENT]->id = rb->button_get(false);
if (ev_data.cb[BUTEVENT]->id == BUTTON_NONE)
continue; /* check next event */
break;
case CUSTOMEVENT:
ev_data.thread_state |= thread_ev_states[CUSTOMEVENT]; // don't reset */
break;
case PLAYBKEVENT:
break;
case TIMEREVENT:
ev_data.cb[TIMEREVENT]->id = *rb->current_tick + ev_data.timer_ticks;
break;
}
if (lua_rev_callback(ev_data.NEWL, ev_data.cb[event]) != LUA_SUCCESS)
{
rev_unlock_mtx();
goto event_error;
}
}
rev_unlock_mtx(); /* we are safe to release back to main lua state */
do
{
#ifdef DEBUG_EV
dbg_hook_calls--;
#endif
lua_interrupt_set(ev_data.L, false);
ev_data.next_event = EV_TICKS;
rb->yield();
} while(ev_data.thread_state == THREAD_YIELD || is_suspend(THREAD_SUSPENDMASK >> 8));
}
event_error:
/* thread is exiting -- clean up */
rb->timer_unregister();
//rb->yield();
rb->thread_exit();
return;
}
/* timer interrupt callback */
static void rev_timer_isr(void)
{
if (!is_suspend(THREAD_SUSPENDMASK >> 8)) /* all events suspended? */
{
ev_data.next_event--;
ev_data.next_input--;
if (ev_data.next_input <=0)
{
ev_data.thread_state |= ((ev_data.thread_state & THREAD_INPUTMASK) >> 16);
ev_data.next_input = ev_data.freq_input;
}
if (ev_data.cb[TIMEREVENT] != NULL && !is_suspend(TIMEREVENT))
{
if (TIME_AFTER(*rb->current_tick, ev_data.cb[TIMEREVENT]->id))
{
ev_data.thread_state |= thread_ev_states[TIMEREVENT];
ev_data.next_event = 0;
}
}
if (ev_data.next_event <= 0)
lua_interrupt_set(ev_data.L, true);
}
}
static void create_event_thread_ref(struct event_data *ev_data)
{
lua_State *L = ev_data->L;
lua_createtable(L, 2, 0);
ev_data->event_stack = (long *) lua_newuserdata (L, EV_STACKSZ);
/* attach EVENT_METATABLE to ud so we get notified on garbage collection */
luaL_getmetatable (L, EVENT_METATABLE);
lua_setmetatable (L, -2);
lua_rawseti(L, -2, 1);
ev_data->NEWL = lua_newthread(L);
lua_rawseti(L, -2, 2);
lua_setfield (L, LUA_REGISTRYINDEX, EVENT_THREAD); /* store references */
}
static void destroy_event_thread_ref(struct event_data *ev_data)
{
lua_State *L = ev_data->L;
ev_data->event_stack = NULL;
ev_data->NEWL = NULL;
lua_pushnil(L);
lua_setfield (L, LUA_REGISTRYINDEX, EVENT_THREAD); /* free references */
}
static void exit_event_thread(struct event_data *ev_data)
{
ev_data->thread_state = THREAD_QUIT;
rb->thread_wait(ev_data->thread_id); /* wait for thread to exit */
}
static void init_event_thread(bool init, struct event_data *ev_data)
{
if (ev_data->event_stack != NULL) /* make sure we don't double free */
{
if (!init && ev_data->thread_id != UINT_MAX)
{
ev_data->thread_state |= THREAD_SUSPENDMASK; /* suspend all events */
rb->yield();
exit_event_thread(ev_data);
destroy_event_thread_ref(ev_data);
lua_interrupt_set(ev_data->L, false);
ev_data->thread_state = THREAD_YIELD | THREAD_SUSPENDMASK;
ev_data->thread_id = UINT_MAX;
}
return;
}
else if (!init || ev_data->thread_state == THREAD_QUIT)
return;
create_event_thread_ref(ev_data);
if (ev_data->NEWL == NULL || ev_data->event_stack == NULL)
return;
ev_data->thread_id = rb->create_thread(&event_thread,
ev_data->event_stack,
EV_STACKSZ,
0,
EVENT_THREAD
IF_PRIO(, PRIORITY_SYSTEM)
IF_COP(, COP));
/* Timer is used to poll waiting events */
rb->timer_register(0, NULL, EV_TIMER_FREQ, rev_timer_isr IF_COP(, CPU));
ev_data->thread_state &= ~THREAD_SUSPENDMASK;
}
static void playback_event_callback(unsigned short id, void *data)
{
/* playback events are synchronous we need to return ASAP so set a flag */
if (!is_suspend(THREAD_PLAYBKEVENT)) /* playback events suspended? */
{
ev_data.thread_state |= thread_ev_states[PLAYBKEVENT];
ev_data.cb[PLAYBKEVENT]->id = id;
ev_data.cb[PLAYBKEVENT]->data = data;
lua_interrupt_set(ev_data.L, true);
}
}
static void register_playbk_events(int flag_events,
void (*handler)(unsigned short id, void *data))
{
long unsigned int i = 0;
const unsigned short playback_events[7] =
{ /*flags*/
PLAYBACK_EVENT_START_PLAYBACK, /* 0x1 */
PLAYBACK_EVENT_TRACK_BUFFER, /* 0x2 */
PLAYBACK_EVENT_CUR_TRACK_READY, /* 0x4 */
PLAYBACK_EVENT_TRACK_FINISH, /* 0x8 */
PLAYBACK_EVENT_TRACK_CHANGE, /* 0x10*/
PLAYBACK_EVENT_TRACK_SKIP, /* 0x20*/
PLAYBACK_EVENT_NEXTTRACKID3_AVAILABLE /* 0x40*/
};
for(; i < ARRAYLEN(playback_events); i++, flag_events >>= 1)
{
if (flag_events == 0) /* remove events */
rb->remove_event(playback_events[i], handler);
else /* add events */
if ((flag_events & 0x1) == 0x1)
rb->add_event(playback_events[i], handler);
}
}
static void destroy_event_userdata(lua_State *L, int event)
{
if (ev_data.cb[event] != NULL)
{
int ev_flag = thread_ev_states[event];
int ev_clear = (ev_flag | (ev_flag << 16));
if (!is_suspend(THREAD_SUSPENDMASK >> 8)) /* all events suspended? */
ev_clear |= (ev_flag << 8);
ev_data.thread_state &= ~(ev_clear);
luaL_unref (L, LUA_REGISTRYINDEX, ev_data.cb[event]->cb_ref);
ev_data.cb[event] = NULL;
}
}
static void create_event_userdata(lua_State *L, int event, int index)
{
/* if function is already registered , unregister it */
destroy_event_userdata(L, event);
if (!lua_isfunction (L, index))
{
init_event_thread(false, &ev_data);
luaL_typerror (L, index, "function");
return;
}
lua_pushvalue (L, index); /* copy passed lua function on top of stack */
int ref_lua = luaL_ref(L, LUA_REGISTRYINDEX);
ev_data.cb[event] = (struct cb_data *)lua_newuserdata(L, sizeof(struct cb_data));
ev_data.cb[event]->cb_ref = ref_lua; /* store ref for later call/release */
/* attach EVENT_METATABLE to ud so we get notified on garbage collection */
luaL_getmetatable (L, EVENT_METATABLE);
lua_setmetatable (L, -2);
/* cb_data is on top of stack */
}
static int rockev_gc(lua_State *L) {
bool has_events = false;
void *d = (void *) lua_touserdata (L, 1);
if (d == NULL)
{
return 0;
}
else if (d == ev_data.event_stack) /* thread stack is gc'd kill thread */
{
init_event_thread(false, &ev_data);
}
else if (d == ev_data.cb[PLAYBKEVENT])
{
register_playbk_events(0, &playback_event_callback);
}
for( int i= 0; i < EVENT_CT; i++)
{
if (d == ev_data.cb[i])
destroy_event_userdata(L, i);
else if (ev_data.cb[i] != NULL)
has_events = true;
}
if (!has_events) /* nothing to wait for kill thread */
init_event_thread(false, &ev_data);
return 0;
}
/******************************************************************************
* LUA INTERFACE **************************************************************
*******************************************************************************
*/
static int rockev_register(lua_State *L)
{
int event = luaL_checkoption(L, 1, NULL, ev_map);
int ev_flag = thread_ev_states[event];
int playbk_events;
lua_settop (L, 3); /* we need to lock our optional args before...*/
create_event_userdata(L, event, 2);/* cb_data is on top of stack */
switch (event)
{
case ACTEVENT:
/* fall through */
case BUTEVENT:
ev_data.freq_input = luaL_optinteger(L, 3, EV_INPUT);
if (ev_data.freq_input < HZ / 20) ev_data.freq_input = HZ / 20;
ev_data.thread_state |= (ev_flag | (ev_flag << 16));
break;
case CUSTOMEVENT:
break;
case PLAYBKEVENT:
/* see register_playbk_events() for flags */
playbk_events = luaL_optinteger(L, 3, 0x3F);
register_playbk_events(playbk_events, &playback_event_callback);
break;
case TIMEREVENT:
ev_data.timer_ticks = luaL_checkinteger(L, 3);
ev_data.cb[TIMEREVENT]->id = *rb->current_tick + ev_data.timer_ticks;
break;
}
init_event_thread(true, &ev_data);
return 1; /* returns cb_data */
}
static int rockev_suspend(lua_State *L)
{
if (ev_data.thread_state == THREAD_QUIT)
return 0;
int event; /*Arg 1 is event pass nil to suspend all */
bool suspend = luaL_optboolean(L, 2, true);
int ev_flag = THREAD_SUSPENDMASK;
if (!lua_isnoneornil(L, 1))
{
event = luaL_checkoption(L, 1, NULL, ev_map);
ev_flag = thread_ev_states[event] << 8;
}
if (suspend)
ev_data.thread_state |= ev_flag;
else
ev_data.thread_state &= ~(ev_flag);
return 0;
}
static int rockev_trigger(lua_State *L)
{
int event = luaL_checkoption(L, 1, NULL, ev_map);
bool enable = luaL_optboolean(L, 2, true);
int ev_flag;
/* protect from invalid events */
if (ev_data.cb[event] != NULL)
{
ev_flag = thread_ev_states[event];
/* allow user to pass an id to some of the callback functions */
ev_data.cb[event]->id = luaL_optinteger(L, 3, ev_data.cb[event]->id);
if (enable)
ev_data.thread_state |= ev_flag;
else
ev_data.thread_state &= ~(ev_flag);
}
return 0;
}
static int rockev_unregister(lua_State *L)
{
luaL_checkudata (L, 1, EVENT_METATABLE);
rockev_gc(L);
lua_pushnil(L);
return 1;
}
/*
** Creates events metatable.
*/
static int event_create_meta (lua_State *L) {
luaL_newmetatable (L, EVENT_METATABLE);
/* set __gc field so we can clean-up our objects */
lua_pushcfunction (L, rockev_gc);
lua_setfield (L, -2, "__gc");
return 1;
}
static const struct luaL_reg evlib[] = {
{"register", rockev_register},
{"suspend", rockev_suspend},
{"trigger", rockev_trigger},
{"unregister", rockev_unregister},
{NULL, NULL}
};
int luaopen_rockevents (lua_State *L) {
rb->mutex_init(&rev_mtx);
init_event_data(L, &ev_data);
event_create_meta (L);
luaL_register (L, LUA_ROCKEVENTSNAME, evlib);
return 1;
}