diff --git a/include/mgba/core/scripting.h b/include/mgba/core/scripting.h index ebf8f88cf..c65897e17 100644 --- a/include/mgba/core/scripting.h +++ b/include/mgba/core/scripting.h @@ -1,4 +1,4 @@ -/* Copyright (c) 2013-2017 Jeffrey Pfau +/* Copyright (c) 2013-2022 Jeffrey Pfau * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this @@ -13,6 +13,10 @@ CXX_GUARD_START #ifdef USE_DEBUGGERS #include #endif +#include + +struct mCore; +mSCRIPT_DECLARE_STRUCT(mCore); struct mScriptBridge; struct VFile; @@ -47,6 +51,11 @@ bool mScriptBridgeLoadScript(struct mScriptBridge*, const char* name); bool mScriptBridgeLookupSymbol(struct mScriptBridge*, const char* name, int32_t* out); +struct mScriptContext; +struct mCore; +void mScriptContextAttachCore(struct mScriptContext*, struct mCore*); +void mScriptContextDetachCore(struct mScriptContext*); + CXX_GUARD_END #endif diff --git a/include/mgba/script/context.h b/include/mgba/script/context.h index 0b83a02fe..d37caa293 100644 --- a/include/mgba/script/context.h +++ b/include/mgba/script/context.h @@ -19,7 +19,7 @@ struct mScriptFunction; struct mScriptEngineContext; struct mScriptContext { - struct mScriptValue rootScope; + struct Table rootScope; struct Table engines; }; @@ -46,9 +46,9 @@ struct mScriptEngineContext { void mScriptContextInit(struct mScriptContext*); void mScriptContextDeinit(struct mScriptContext*); -void mScriptContextRegisterEngine(struct mScriptContext*, struct mScriptEngine2*); +struct mScriptEngineContext* mScriptContextRegisterEngine(struct mScriptContext*, struct mScriptEngine2*); -void mScriptContextAddGlobal(struct mScriptContext*, const char* key, struct mScriptValue* value); +void mScriptContextSetGlobal(struct mScriptContext*, const char* key, struct mScriptValue* value); void mScriptContextRemoveGlobal(struct mScriptContext*, const char* key); bool mScriptInvoke(const struct mScriptValue* fn, struct mScriptFrame* frame); diff --git a/include/mgba/script/types.h b/include/mgba/script/types.h index ad7c679d0..b3dfda5ec 100644 --- a/include/mgba/script/types.h +++ b/include/mgba/script/types.h @@ -580,6 +580,7 @@ void mScriptValueDeref(struct mScriptValue* val); void mScriptValueWrap(struct mScriptValue* val, struct mScriptValue* out); struct mScriptValue* mScriptValueUnwrap(struct mScriptValue* val); +const struct mScriptValue* mScriptValueUnwrapConst(const struct mScriptValue* val); struct mScriptValue* mScriptStringCreateFromUTF8(const char* string); diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 7c1108252..bd1f3ecf8 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -14,7 +14,6 @@ set(SOURCE_FILES map-cache.c mem-search.c rewind.c - scripting.c serialize.c sync.c thread.c @@ -24,6 +23,16 @@ set(SOURCE_FILES set(TEST_FILES test/core.c) +if(ENABLE_SCRIPTING) + list(APPEND SOURCE_FILES + scripting.c) + + if(USE_LUA) + list(APPEND TEST_FILES + test/scripting.c) + endif() +endif() + source_group("mCore" FILES ${SOURCE_FILES}) source_group("mCore tests" FILES ${TEST_FILES}) diff --git a/src/core/scripting.c b/src/core/scripting.c index add6da031..e4a472d79 100644 --- a/src/core/scripting.c +++ b/src/core/scripting.c @@ -1,10 +1,12 @@ -/* Copyright (c) 2013-2017 Jeffrey Pfau +/* Copyright (c) 2013-2022 Jeffrey Pfau * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include +#include +#include #include #include @@ -144,3 +146,33 @@ bool mScriptBridgeLookupSymbol(struct mScriptBridge* sb, const char* name, int32 HashTableEnumerate(&sb->engines, _seLookupSymbol, &info); return info.success; } + +mSCRIPT_DECLARE_STRUCT_CD_METHOD(mCore, U32, frameCounter, 0); +mSCRIPT_DECLARE_STRUCT_CD_METHOD(mCore, S32, frameCycles, 0); +mSCRIPT_DECLARE_STRUCT_CD_METHOD(mCore, S32, frequency, 0); +mSCRIPT_DECLARE_STRUCT_VOID_D_METHOD(mCore, runFrame, 0); +mSCRIPT_DECLARE_STRUCT_VOID_D_METHOD(mCore, step, 0); + +mSCRIPT_DEFINE_STRUCT(mCore) +mSCRIPT_DEFINE_DOCSTRING("Get the number of the current frame") +mSCRIPT_DEFINE_STRUCT_METHOD_NAMED(mCore, currentFrame, frameCounter) +mSCRIPT_DEFINE_DOCSTRING("Get the number of cycles per frame") +mSCRIPT_DEFINE_STRUCT_METHOD(mCore, frameCycles) +mSCRIPT_DEFINE_DOCSTRING("Get the number of cycles per second") +mSCRIPT_DEFINE_STRUCT_METHOD(mCore, frequency) +mSCRIPT_DEFINE_DOCSTRING("Run until the next frame") +mSCRIPT_DEFINE_STRUCT_METHOD(mCore, runFrame) +mSCRIPT_DEFINE_DOCSTRING("Run a single instruction") +mSCRIPT_DEFINE_STRUCT_METHOD(mCore, step) +mSCRIPT_DEFINE_END; + +void mScriptContextAttachCore(struct mScriptContext* context, struct mCore* core) { + struct mScriptValue* coreValue = mScriptValueAlloc(mSCRIPT_TYPE_MS_S(mCore)); + coreValue->value.opaque = core; + mScriptContextSetGlobal(context, "emu", coreValue); + mScriptValueDeref(coreValue); +} + +void mScriptContextDetachCore(struct mScriptContext* context) { + mScriptContextRemoveGlobal(context, "emu"); +} diff --git a/src/core/test/scripting.c b/src/core/test/scripting.c new file mode 100644 index 000000000..cfe009601 --- /dev/null +++ b/src/core/test/scripting.c @@ -0,0 +1,143 @@ +/* Copyright (c) 2013-2022 Jeffrey Pfau + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "util/test/suite.h" + +#include +#include +#include +#include +#include + +#ifdef M_CORE_GBA +#define TEST_PLATFORM mPLATFORM_GBA +#elif defined(M_CORE_GB) +#define TEST_PLATFORM mPLATFORM_GB +#else +#error "Need a valid platform for testing" +#endif + +static const uint8_t _fakeGBROM[0x4000] = { + [0x100] = 0x18, // Loop forever + [0x101] = 0xFE, // jr, $-2 + [0x102] = 0xCE, // Enough of the header to fool the core + [0x103] = 0xED, + [0x104] = 0x66, + [0x105] = 0x66, +}; + +#define SETUP_LUA \ + struct mScriptContext context; \ + mScriptContextInit(&context); \ + struct mScriptEngineContext* lua = mScriptContextRegisterEngine(&context, mSCRIPT_ENGINE_LUA) + +#define CREATE_CORE \ + struct mCore* core = mCoreCreate(TEST_PLATFORM); \ + assert_non_null(core); \ + assert_true(core->init(core)); \ + switch (core->platform(core)) { \ + case mPLATFORM_GBA: \ + core->busWrite32(core, 0x020000C0, 0xEAFFFFFE); \ + break; \ + case mPLATFORM_GB: \ + assert_true(core->loadROM(core, VFileFromConstMemory(_fakeGBROM, sizeof(_fakeGBROM)))); \ + break; \ + case mPLATFORM_NONE: \ + break; \ + } \ + mCoreInitConfig(core, NULL); \ + mScriptContextAttachCore(&context, core) + +#define TEARDOWN_CORE \ + mCoreConfigDeinit(&core->config); \ + core->deinit(core) + +#define LOAD_PROGRAM(PROG) \ + do { \ + struct VFile* vf = VFileFromConstMemory(PROG, strlen(PROG)); \ + const char* error = NULL; \ + assert_true(lua->load(lua, vf, &error)); \ + assert_null(error); \ + vf->close(vf); \ + } while(0) + +#define TEST_VALUE(TYPE, NAME, VALUE) \ + do { \ + struct mScriptValue val = mSCRIPT_MAKE(TYPE, VALUE); \ + struct mScriptValue* global = lua->getGlobal(lua, NAME); \ + assert_non_null(global); \ + assert_true(global->type->equal(global, &val)); \ + mScriptValueDeref(global); \ + } while(0) + +M_TEST_SUITE_SETUP(mScriptCore) { + if (mSCRIPT_ENGINE_LUA->init) { + mSCRIPT_ENGINE_LUA->init(mSCRIPT_ENGINE_LUA); + } + return 0; +} + +M_TEST_SUITE_TEARDOWN(mScriptCore) { + if (mSCRIPT_ENGINE_LUA->deinit) { + mSCRIPT_ENGINE_LUA->deinit(mSCRIPT_ENGINE_LUA); + } + return 0; +} + +M_TEST_DEFINE(globals) { + SETUP_LUA; + CREATE_CORE; + core->reset(core); + + LOAD_PROGRAM("assert(emu)"); + assert_true(lua->run(lua)); + + TEARDOWN_CORE; + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(infoFuncs) { + SETUP_LUA; + CREATE_CORE; + core->reset(core); + + LOAD_PROGRAM( + "frequency = emu:frequency()\n" + "frameCycles = emu:frameCycles()\n" + ); + assert_true(lua->run(lua)); + + TEST_VALUE(S32, "frequency", core->frequency(core)); + TEST_VALUE(S32, "frameCycles", core->frameCycles(core)); + + TEARDOWN_CORE; + mScriptContextDeinit(&context); +} + +M_TEST_DEFINE(runFrame) { + SETUP_LUA; + CREATE_CORE; + core->reset(core); + + LOAD_PROGRAM( + "frame = emu:currentFrame()\n" + "emu:runFrame()\n" + ); + + int i; + for (i = 0; i < 5; ++i) { + assert_true(lua->run(lua)); + TEST_VALUE(S32, "frame", i); + } + + TEARDOWN_CORE; + mScriptContextDeinit(&context); +} + +M_TEST_SUITE_DEFINE_SETUP_TEARDOWN(mScriptCore, + cmocka_unit_test(globals), + cmocka_unit_test(infoFuncs), + cmocka_unit_test(runFrame), +) diff --git a/src/script/context.c b/src/script/context.c index 321d2f222..9cc8b09b0 100644 --- a/src/script/context.c +++ b/src/script/context.c @@ -5,13 +5,64 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include +struct mScriptKVPair { + const char* key; + struct mScriptValue* value; +}; + +static void _engineContextDestroy(void* ctx) { + struct mScriptEngineContext* context = ctx; + context->destroy(context); +} + +static void _contextAddGlobal(const char* key, void* value, void* user) { + UNUSED(key); + struct mScriptEngineContext* context = value; + struct mScriptKVPair* pair = user; + context->setGlobal(context, pair->key, pair->value); +} + +static void _contextRemoveGlobal(const char* key, void* value, void* user) { + UNUSED(key); + struct mScriptEngineContext* context = value; + context->setGlobal(context, user, NULL); +} + void mScriptContextInit(struct mScriptContext* context) { - // TODO: rootScope - HashTableInit(&context->engines, 0, NULL); + HashTableInit(&context->rootScope, 0, (void (*)(void*)) mScriptValueDeref); + HashTableInit(&context->engines, 0, _engineContextDestroy); } void mScriptContextDeinit(struct mScriptContext* context) { HashTableDeinit(&context->engines); + HashTableDeinit(&context->rootScope); +} + +struct mScriptEngineContext* mScriptContextRegisterEngine(struct mScriptContext* context, struct mScriptEngine2* engine) { + struct mScriptEngineContext* ectx = engine->create(engine, context); + if (ectx) { + HashTableInsert(&context->engines, engine->name, ectx); + } + return ectx; +} + +void mScriptContextSetGlobal(struct mScriptContext* context, const char* key, struct mScriptValue* value) { + mScriptValueRef(value); + HashTableInsert(&context->rootScope, key, value); + struct mScriptKVPair pair = { + .key = key, + .value = value + }; + HashTableEnumerate(&context->engines, _contextAddGlobal, &pair); +} + +void mScriptContextRemoveGlobal(struct mScriptContext* context, const char* key) { + if (!HashTableLookup(&context->rootScope, key)) { + return; + } + // Since _contextRemoveGlobal doesn't mutate |key|, this cast should be safe + HashTableEnumerate(&context->engines, _contextRemoveGlobal, (char*) key); + HashTableRemove(&context->rootScope, key); } bool mScriptInvoke(const struct mScriptValue* val, struct mScriptFrame* frame) { diff --git a/src/script/engines/lua.c b/src/script/engines/lua.c index 2d9eb5294..0df0142d0 100644 --- a/src/script/engines/lua.c +++ b/src/script/engines/lua.c @@ -30,6 +30,7 @@ static void _luaDeref(struct mScriptValue*); static int _luaThunk(lua_State* lua); static int _luaGetObject(lua_State* lua); static int _luaSetObject(lua_State* lua); +static int _luaGcObject(lua_State* lua); #if LUA_VERSION_NUM < 503 #define lua_pushinteger lua_pushnumber @@ -83,6 +84,7 @@ struct mScriptEngine2* const mSCRIPT_ENGINE_LUA = &_engineLua.d; static const luaL_Reg _mSTStruct[] = { { "__index", _luaGetObject }, { "__newindex", _luaSetObject }, + { "__gc", _luaGcObject }, { NULL, NULL } }; @@ -483,3 +485,13 @@ int _luaSetObject(lua_State* lua) { mScriptValueDeref(val); return 0; } + +static int _luaGcObject(lua_State* lua) { + struct mScriptValue* val = lua_touserdata(lua, -1); + val = mScriptValueUnwrap(val); + if (!val) { + return 0; + } + mScriptValueDeref(val); + return 0; +} diff --git a/src/script/types.c b/src/script/types.c index 66423761a..0582de186 100644 --- a/src/script/types.c +++ b/src/script/types.c @@ -687,6 +687,13 @@ struct mScriptValue* mScriptValueUnwrap(struct mScriptValue* value) { return NULL; } +const struct mScriptValue* mScriptValueUnwrapConst(const struct mScriptValue* value) { + if (value->type == mSCRIPT_TYPE_MS_WRAPPER) { + return value->value.copaque; + } + return NULL; +} + struct mScriptValue* mScriptStringCreateFromUTF8(const char* string) { struct mScriptValue* val = mScriptValueAlloc(mSCRIPT_TYPE_MS_STR); struct mScriptString* internal = val->value.opaque; @@ -823,6 +830,9 @@ void mScriptClassDeinit(struct mScriptTypeClass* cls) { } bool mScriptObjectGet(struct mScriptValue* obj, const char* member, struct mScriptValue* val) { + if (obj->type->base == mSCRIPT_TYPE_WRAPPER) { + obj = mScriptValueUnwrap(obj); + } if (obj->type->base != mSCRIPT_TYPE_OBJECT) { return false; } @@ -1031,6 +1041,9 @@ bool mScriptPopPointer(struct mScriptList* list, void** out) { } bool mScriptCast(const struct mScriptType* type, const struct mScriptValue* input, struct mScriptValue* output) { + if (input->type->base == mSCRIPT_TYPE_WRAPPER) { + input = mScriptValueUnwrapConst(input); + } if (type->cast && type->cast(input, type, output)) { return true; }