From 6d390150473c4f31093a2dd68b46d12089673c2c Mon Sep 17 00:00:00 2001 From: falsycat Date: Tue, 1 Aug 2023 21:29:50 +0900 Subject: [PATCH 01/18] improve luajit::TaskContext::Query --- core/luajit/context.cc | 7 +++---- core/luajit/context.hh | 2 +- core/luajit/context_test.cc | 6 +++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/core/luajit/context.cc b/core/luajit/context.cc index ff46931..bfcef24 100644 --- a/core/luajit/context.cc +++ b/core/luajit/context.cc @@ -19,10 +19,9 @@ std::shared_ptr TaskContext::Register() noexcept { return std::make_shared(ctx_, index); } -void TaskContext::Query(const std::shared_ptr& value) noexcept { - assert(nullptr != value); - assert(value->context() == ctx_); - lua_rawgeti(state_, LUA_REGISTRYINDEX, value->index()); +void TaskContext::Query(const Value& value) noexcept { + assert(value.context() == ctx_); + lua_rawgeti(state_, LUA_REGISTRYINDEX, value.index()); } diff --git a/core/luajit/context.hh b/core/luajit/context.hh index 24cbd4f..5e735d9 100644 --- a/core/luajit/context.hh +++ b/core/luajit/context.hh @@ -62,7 +62,7 @@ class TaskContext final { lua_State* operator*() const noexcept { return state_; } std::shared_ptr Register() noexcept; - void Query(const std::shared_ptr&) noexcept; + void Query(const Value&) noexcept; const std::shared_ptr& context() const noexcept { return ctx_; } lua_State* state() const noexcept { return state_; } diff --git a/core/luajit/context_test.cc b/core/luajit/context_test.cc index 38ba72b..0e59748 100644 --- a/core/luajit/context_test.cc +++ b/core/luajit/context_test.cc @@ -34,10 +34,10 @@ TEST_P(LuaJIT_Context, Query) { sut->Exec([&](auto& ctx) { EXPECT_TRUE(nullptr != value); - ctx.Query(value); - const char* value = lua_tostring(*ctx, -1); + ctx.Query(*value); - EXPECT_STREQ(value, "helloworld"); + const char* str = lua_tostring(*ctx, -1); + EXPECT_STREQ(str, "helloworld"); }); ConsumeTasks(); -- 2.45.2 From 6f6aa557fd8cef74e813642f72b67cfcedaac6c8 Mon Sep 17 00:00:00 2001 From: falsycat Date: Sat, 5 Aug 2023 10:17:54 +0900 Subject: [PATCH 02/18] add luajit::TaskContext::Push() methods --- core/luajit/context.hh | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/core/luajit/context.hh b/core/luajit/context.hh index 5e735d9..faddaef 100644 --- a/core/luajit/context.hh +++ b/core/luajit/context.hh @@ -47,9 +47,10 @@ class TaskContext final { public: friend class Context; + class Nil {}; + TaskContext() = delete; - explicit TaskContext( - std::shared_ptr&& ctx, lua_State* state) noexcept + TaskContext(const std::shared_ptr& ctx, lua_State* state) noexcept : ctx_(std::move(ctx)), state_(state) { assert(nullptr != state_); } @@ -64,6 +65,39 @@ class TaskContext final { std::shared_ptr Register() noexcept; void Query(const Value&) noexcept; + template + uint32_t PushAll(T&& v, Args&&... args) noexcept { + Push(v); + return 1 + PushAll(std::forward(args)...); + } + uint32_t PushAll() noexcept { return 0; } + + void Push(Nil) noexcept { + lua_pushnil(state_); + } + void Push(bool v) noexcept { + lua_pushboolean(state_, v); + } + void Push(lua_Integer v) noexcept { + lua_pushinteger(state_, v); + } + void Push(lua_Number v) noexcept { + lua_pushnumber(state_, v); + } + void Push(std::string_view str) noexcept { + lua_pushlstring(state_, str.data(), str.size()); + } + void Push(std::span ptr) noexcept { + lua_pushlstring( + state_, reinterpret_cast(ptr.data()), ptr.size()); + } + void Push(const std::shared_ptr& v) noexcept { + Query(*v); + } + void Push(const luajit::Value& v) noexcept { + Query(v); + } + const std::shared_ptr& context() const noexcept { return ctx_; } lua_State* state() const noexcept { return state_; } -- 2.45.2 From cc51952268724d896bfbcc858c73fac51fa5af14 Mon Sep 17 00:00:00 2001 From: falsycat Date: Sat, 5 Aug 2023 10:18:37 +0900 Subject: [PATCH 03/18] add luajit::Thread --- core/CMakeLists.txt | 3 ++ core/luajit/thread.hh | 93 ++++++++++++++++++++++++++++++++++++++ core/luajit/thread_test.cc | 79 ++++++++++++++++++++++++++++++++ core/luajit/thread_test.hh | 21 +++++++++ 4 files changed, 196 insertions(+) create mode 100644 core/luajit/thread.hh create mode 100644 core/luajit/thread_test.cc create mode 100644 core/luajit/thread_test.hh diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index ce13b6c..cfa3707 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -12,6 +12,7 @@ target_sources(nf7_core version.cc PUBLIC luajit/context.hh + luajit/thread.hh version.hh ) @@ -20,6 +21,8 @@ target_sources(nf7_core_test PRIVATE luajit/context_test.cc luajit/context_test.hh + luajit/thread_test.cc + luajit/thread_test.hh ) target_link_libraries(nf7_core_test PRIVATE diff --git a/core/luajit/thread.hh b/core/luajit/thread.hh new file mode 100644 index 0000000..b326bf9 --- /dev/null +++ b/core/luajit/thread.hh @@ -0,0 +1,93 @@ +// No copyright +#pragma once + +#include +#include +#include +#include + +#include + +#include "core/luajit/context.hh" + +namespace nf7::core::luajit { + +class Thread : public std::enable_shared_from_this { + public: + struct DoNotCallConstructorDirectly { }; + + enum State : uint8_t { + kPaused, + kRunning, + kFinished, + }; + + public: + template + static std::shared_ptr Make( + TaskContext& lua, const std::shared_ptr& func) { + DoNotCallConstructorDirectly key; + auto th = std::make_shared(lua, key); + th->taskContext(lua).Query(*func); + return th; + } + + public: + Thread(TaskContext& t, DoNotCallConstructorDirectly&) noexcept + : context_(t.context()), th_(lua_newthread(*t)) { + assert(th_); + } + + public: + // if this finished with state_ kPaused, + // a responsibility to resume is on one who yielded + template + void Resume(TaskContext& lua, Args&&... args) noexcept { + assert(lua.context() == context_); + + if (kFinished == state_) { + return; + } + assert(kPaused == state_); + + auto thlua = taskContext(lua); + const auto narg = thlua.PushAll(std::forward(args)...); + + state_ = kRunning; + const auto ret = lua_resume(*thlua, narg); + switch (ret) { + case 0: + state_ = kFinished; + onExited(thlua); + return; + case LUA_YIELD: + state_ = kPaused; + return; + default: + state_ = kFinished; + onAborted(thlua); + return; + } + } + + const std::shared_ptr& context() const noexcept { return context_; } + State state() const noexcept { return state_; } + + protected: + virtual void onExited(TaskContext&) noexcept { } + virtual void onAborted(TaskContext&) noexcept { } + + private: + TaskContext taskContext(const TaskContext& t) const noexcept { + assert(t.context() == context_); + return TaskContext {context_, th_}; + } + + private: + const std::shared_ptr context_; + lua_State* const th_; + + State state_ = kPaused; +}; + +} // namespace nf7::core::luajit diff --git a/core/luajit/thread_test.cc b/core/luajit/thread_test.cc new file mode 100644 index 0000000..3f604e0 --- /dev/null +++ b/core/luajit/thread_test.cc @@ -0,0 +1,79 @@ +// No copyright +#include "core/luajit/thread.hh" +#include "core/luajit/thread_test.hh" + +#include +#include + +#include "core/luajit/context_test.hh" + + +class LuaJIT_Thread : public nf7::core::luajit::test::ContextFixture { + public: + using ContextFixture::ContextFixture; + + template + void TestThread( + const auto& setup, const char* script, Args&&... args) { + auto lua = nf7::core::luajit::Context::Create(*env_, GetParam()); + auto called = uint32_t {0}; + lua->Exec([&](auto& lua) { + const auto compile = luaL_loadstring(*lua, script); + ASSERT_EQ(compile, LUA_OK); + + auto sut = nf7::core::luajit::Thread::Make< + nf7::core::luajit::test::ThreadMock>(lua, lua.Register()); + setup(*sut); + + sut->Resume(lua, std::forward(args)...); + ++called; + }); + ConsumeTasks(); + EXPECT_EQ(called, 1); + } +}; + + +TEST_P(LuaJIT_Thread, ResumeWithSingleReturn) { + TestThread([](auto& sut) { + EXPECT_CALL(sut, onExited) + .WillOnce([](auto& lua) { EXPECT_EQ(lua_tointeger(*lua, 1), 6); }); + }, + "return 1+2+3"); +} + +TEST_P(LuaJIT_Thread, ResumeWithArgs) { + TestThread([](auto& sut) { + EXPECT_CALL(sut, onExited) + .WillOnce([](auto& lua) { EXPECT_EQ(lua_tointeger(*lua, 1), 60); }); + }, + "local x,y,z = ...\nreturn x+y+z", + lua_Integer {10}, lua_Integer {20}, lua_Integer {30}); +} + +TEST_P(LuaJIT_Thread, RunWithMultipleReturn) { + TestThread([](auto& sut) { + EXPECT_CALL(sut, onExited) + .WillOnce([](auto& lua) { + EXPECT_EQ(lua_gettop(*lua), 3); + EXPECT_EQ(lua_tointeger(*lua, 1), 1); + EXPECT_EQ(lua_tointeger(*lua, 2), 2); + EXPECT_EQ(lua_tointeger(*lua, 3), 3); + }); + }, + "return 1, 2, 3"); +} + +TEST_P(LuaJIT_Thread, RunAndError) { + TestThread([](auto& sut) { + EXPECT_CALL(sut, onAborted); + }, + "return foo()"); +} + + +INSTANTIATE_TEST_SUITE_P( + SyncOrAsync, LuaJIT_Thread, + testing::Values( + nf7::core::luajit::Context::kSync, + nf7::core::luajit::Context::kAsync)); diff --git a/core/luajit/thread_test.hh b/core/luajit/thread_test.hh new file mode 100644 index 0000000..bc39687 --- /dev/null +++ b/core/luajit/thread_test.hh @@ -0,0 +1,21 @@ +// No copyright +#pragma once + +#include "core/luajit/thread.hh" + +#include + +#include "core/luajit/context.hh" + + +namespace nf7::core::luajit::test { + +class ThreadMock : public Thread { + public: + using Thread::Thread; + + MOCK_METHOD(void, onExited, (TaskContext&), (noexcept, override)); + MOCK_METHOD(void, onAborted, (TaskContext&), (noexcept, override)); +}; + +} // namespace nf7::core::luajit::test -- 2.45.2 From 2aef087d02f60c9d86d581f73ea3c3b4573c1477 Mon Sep 17 00:00:00 2001 From: falsycat Date: Sat, 5 Aug 2023 11:05:56 +0900 Subject: [PATCH 04/18] add SimpleTaskQueue::WaitForEmpty() --- iface/common/task.hh | 7 +++++++ iface/common/task_test.cc | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/iface/common/task.hh b/iface/common/task.hh index e9cb12e..4ddd746 100644 --- a/iface/common/task.hh +++ b/iface/common/task.hh @@ -233,6 +233,12 @@ class SimpleTaskQueue : public TaskQueue { cv_.notify_all(); } + // THREAD-SAFE + bool WaitForEmpty(auto dur) noexcept { + std::unique_lock k {mtx_}; + return cv_.wait_for(k, dur, [this]() { return tasks_.empty(); }); + } + template Driver> void Drive(Driver& driver) { while (!driver.nextIdleInterruption()) { @@ -257,6 +263,7 @@ class SimpleTaskQueue : public TaskQueue { try { std::unique_lock k{mtx_}; + cv_.notify_all(); const auto until = nextAwake(); const auto pred = [&]() { diff --git a/iface/common/task_test.cc b/iface/common/task_test.cc index ce010b2..e376bb2 100644 --- a/iface/common/task_test.cc +++ b/iface/common/task_test.cc @@ -281,3 +281,36 @@ TEST(SimpleTaskQueue, ChaoticPushAndDrive) { EXPECT_EQ(execCount, kPushPerThread); } } + +TEST(SimpleTaskQueue, WaitForEmpty) { + nf7::test::SimpleTaskQueueMock> sut; + EXPECT_CALL(sut, onErrorWhilePush).Times(0); + + // use NiceMock to suppress annoying warnings that slowed unittests + ::testing::NiceMock< + nf7::test::SimpleTaskQueueDriverMock>> driver; + + for (uint32_t i = 0; i < 1000; ++i) { + sut.Exec([](auto&){}); + } + + auto ctx = int32_t {0}; + ON_CALL(driver, Drive) + .WillByDefault([&](auto&& task) { task(ctx); }); + + std::atomic exit = false; + ON_CALL(driver, nextIdleInterruption) + .WillByDefault([&]() -> bool { return exit; }); + + std::thread th {[&]() { sut.Drive(driver); }}; + EXPECT_TRUE(sut.WaitForEmpty(1s)); + + exit = true; + sut.Wake(); + th.join(); +} + +TEST(SimpleTaskQueue, WaitForEmptyWhenEmpty) { + nf7::test::SimpleTaskQueueMock> sut; + EXPECT_TRUE(sut.WaitForEmpty(1s)); +} -- 2.45.2 From ca2ad884e939fe0fb5a7580fd1e35e7fe4bcbf37 Mon Sep 17 00:00:00 2001 From: falsycat Date: Sat, 5 Aug 2023 11:18:02 +0900 Subject: [PATCH 05/18] improve luajit::test::ContextFixture to consume async tasks on worker thread instead of main --- core/luajit/context_test.hh | 88 ++++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/core/luajit/context_test.hh b/core/luajit/context_test.hh index a34af75..3c15c32 100644 --- a/core/luajit/context_test.hh +++ b/core/luajit/context_test.hh @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "iface/common/exception.hh" @@ -21,43 +22,63 @@ namespace nf7::core::luajit::test { class ContextFixture : public ::testing::TestWithParam { private: - template - class Driver final { + class AsyncDriver final { public: - using Param = typename T::Param; - using Time = typename T::Time; - - explicit Driver(Param p) : param_(std::forward(p)) { } - - Driver(const Driver&) = delete; - Driver(Driver&&) = delete; - Driver& operator=(const Driver&) = delete; - Driver& operator=(Driver&&) = delete; + explicit AsyncDriver(ContextFixture& parent) noexcept : parent_(parent) { } void BeginBusy() noexcept { } - void EndBusy() noexcept { interrupt_ = true; } - void Drive(T&& task) noexcept { + void EndBusy() noexcept { } + void Drive(AsyncTask&& task) noexcept { try { task(param_); } catch (const Exception& e) { - std::cout - << "unexpected exception while task execution: " << e.what() + std::cerr + << "unexpected exception while async task execution: " << e.what() << std::endl; std::abort(); } } - Time tick() const noexcept { + AsyncTask::Time tick() const noexcept { const auto now = std::chrono::system_clock::now(); - return std::chrono::time_point_cast(now); + return std::chrono::time_point_cast(now); + } + bool nextIdleInterruption() const noexcept { return !parent_.alive_; } + bool nextTaskInterruption() const noexcept { return false; } + + private: + ContextFixture& parent_; + AsyncTaskContext param_; + }; + + class SyncDriver final { + public: + void BeginBusy() noexcept { } + void EndBusy() noexcept { interrupt_ = true; } + void Drive(SyncTask&& task) noexcept { + try { + task(param_); + } catch (const Exception& e) { + std::cerr + << "unexpected exception while sync task execution: " << e.what() + << std::endl; + std::abort(); + } + } + SyncTask::Time tick() const noexcept { + const auto now = std::chrono::system_clock::now(); + return std::chrono::time_point_cast(now); } bool nextIdleInterruption() const noexcept { return interrupt_; } bool nextTaskInterruption() const noexcept { return false; } private: bool interrupt_ = false; - Param param_; + SyncTaskContext param_; }; + public: + ContextFixture() noexcept : async_driver_(*this) { } + protected: void SetUp() override { syncq_ = std::make_shared>(); @@ -76,28 +97,45 @@ class ContextFixture : public ::testing::TestWithParam { }, }, }); + thread_ = std::thread {[this]() { asyncq_->Drive(async_driver_); }}; } void TearDown() override { ConsumeTasks(); - env_ = std::nullopt; + env_ = std::nullopt; + + WaitAsyncTasks(std::chrono::seconds(3)); + alive_ = false; + asyncq_->Wake(); + thread_.join(); + asyncq_ = nullptr; syncq_ = nullptr; } void ConsumeTasks() noexcept { - AsyncTaskContext async_ctx; - Driver async_driver {async_ctx}; - asyncq_->Drive(async_driver); - - SyncTaskContext sync_ctx; - Driver sync_driver {sync_ctx}; + SyncDriver sync_driver; syncq_->Drive(sync_driver); + + WaitAsyncTasks(std::chrono::seconds(1)); + } + void WaitAsyncTasks(auto dur) noexcept { + if (!asyncq_->WaitForEmpty(dur)) { + std::cerr << "timeout while waiting for task execution" << std::endl; + std::abort(); + } } protected: std::shared_ptr> syncq_; std::shared_ptr> asyncq_; std::optional env_; + + private: + std::atomic alive_ = true; + uint32_t async_cycle_ = 0; + + std::thread thread_; + AsyncDriver async_driver_; }; } // namespace nf7::core::luajit::test -- 2.45.2 From 73cb7e741269d456f0f960b99ebd9bcaf5a20155 Mon Sep 17 00:00:00 2001 From: falsycat Date: Sat, 5 Aug 2023 11:25:26 +0900 Subject: [PATCH 06/18] fix an issue that ConsumeTasks() may end before end of the last task execution --- core/luajit/context_test.hh | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/core/luajit/context_test.hh b/core/luajit/context_test.hh index 3c15c32..ac0869f 100644 --- a/core/luajit/context_test.hh +++ b/core/luajit/context_test.hh @@ -26,8 +26,11 @@ class ContextFixture : public ::testing::TestWithParam { public: explicit AsyncDriver(ContextFixture& parent) noexcept : parent_(parent) { } - void BeginBusy() noexcept { } - void EndBusy() noexcept { } + void BeginBusy() noexcept { async_busy_ = true; } + void EndBusy() noexcept { + async_busy_ = false; + async_busy_.notify_all(); + } void Drive(AsyncTask&& task) noexcept { try { task(param_); @@ -45,9 +48,13 @@ class ContextFixture : public ::testing::TestWithParam { bool nextIdleInterruption() const noexcept { return !parent_.alive_; } bool nextTaskInterruption() const noexcept { return false; } + void Wait() { async_busy_.wait(true); } + private: ContextFixture& parent_; AsyncTaskContext param_; + + std::atomic async_busy_ = false; }; class SyncDriver final { @@ -123,6 +130,7 @@ class ContextFixture : public ::testing::TestWithParam { std::cerr << "timeout while waiting for task execution" << std::endl; std::abort(); } + async_driver_.Wait(); } protected: -- 2.45.2 From 4fd6b179363786d6373ad76e9212594a8a24c7cf Mon Sep 17 00:00:00 2001 From: falsycat Date: Sat, 5 Aug 2023 20:40:04 +0900 Subject: [PATCH 07/18] enhance Exception to print exception stack on stream --- core/luajit/context_test.hh | 8 ++++---- iface/CMakeLists.txt | 2 ++ iface/common/exception.cc | 28 ++++++++++++++++++++++++++++ iface/common/exception.hh | 9 +++++++++ 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 iface/common/exception.cc diff --git a/core/luajit/context_test.hh b/core/luajit/context_test.hh index ac0869f..29d1d31 100644 --- a/core/luajit/context_test.hh +++ b/core/luajit/context_test.hh @@ -36,8 +36,8 @@ class ContextFixture : public ::testing::TestWithParam { task(param_); } catch (const Exception& e) { std::cerr - << "unexpected exception while async task execution: " << e.what() - << std::endl; + << "unexpected exception while async task execution:\n" + << e << std::endl; std::abort(); } } @@ -66,8 +66,8 @@ class ContextFixture : public ::testing::TestWithParam { task(param_); } catch (const Exception& e) { std::cerr - << "unexpected exception while sync task execution: " << e.what() - << std::endl; + << "unexpected exception while sync task execution:\n" + << e << std::endl; std::abort(); } } diff --git a/iface/CMakeLists.txt b/iface/CMakeLists.txt index d8bccd9..17afe0f 100644 --- a/iface/CMakeLists.txt +++ b/iface/CMakeLists.txt @@ -6,10 +6,12 @@ target_link_libraries(nf7_iface ) target_sources(nf7_iface PRIVATE + common/exception.cc version.cc PUBLIC common/dealer.hh common/container.hh + common/exception.hh common/future.hh common/observer.hh common/task.hh diff --git a/iface/common/exception.cc b/iface/common/exception.cc new file mode 100644 index 0000000..efc609e --- /dev/null +++ b/iface/common/exception.cc @@ -0,0 +1,28 @@ +// No copyright +#include "iface/common/exception.hh" + +namespace nf7 { + +std::ostream& operator<<(std::ostream& st, const Exception& e) { + auto idx = uint32_t {0}; + auto ptr = &e; + while (true) { + const auto& loc = ptr->location(); + st << idx << ": " << ptr->what() << "\n"; + st << " " << loc.file_name() << ":" << loc.line() << "\n"; + st << " " << loc.function_name() << "\n"; + try { + ptr->RethrowNestedIf(); + break; + } catch (const Exception& e2) { + ptr = &e2; + ++idx; + } catch (const std::exception& e2) { + st << idx << ": " << e2.what() << "\n"; + break; + } + } + return st; +} + +} // namespace nf7 diff --git a/iface/common/exception.hh b/iface/common/exception.hh index 8e1b656..43cc949 100644 --- a/iface/common/exception.hh +++ b/iface/common/exception.hh @@ -2,6 +2,7 @@ #pragma once #include +#include #include namespace nf7 { @@ -14,6 +15,12 @@ class Exception : public std::exception, std::nested_exception { std::source_location location = std::source_location::current()) : what_(what), location_(location) { } + void RethrowNestedIf() const { + if (auto ptr = nested_ptr()) { + std::rethrow_exception(ptr); + } + } + const char* what() const noexcept override { return what_; } const std::source_location& location() const noexcept { return location_; } @@ -22,4 +29,6 @@ class Exception : public std::exception, std::nested_exception { std::source_location location_; }; +std::ostream& operator<<(std::ostream&, const Exception& e); + } // namespace nf7 -- 2.45.2 From 94025017081b48d4c2d7a4a6900ec1ecb26d00ff Mon Sep 17 00:00:00 2001 From: falsycat Date: Sat, 5 Aug 2023 20:42:09 +0900 Subject: [PATCH 08/18] separate kFinished state of Thread into kExited and kAborted --- core/luajit/thread.hh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/luajit/thread.hh b/core/luajit/thread.hh index b326bf9..c36df30 100644 --- a/core/luajit/thread.hh +++ b/core/luajit/thread.hh @@ -19,7 +19,8 @@ class Thread : public std::enable_shared_from_this { enum State : uint8_t { kPaused, kRunning, - kFinished, + kExited, + kAborted, }; public: @@ -45,7 +46,7 @@ class Thread : public std::enable_shared_from_this { void Resume(TaskContext& lua, Args&&... args) noexcept { assert(lua.context() == context_); - if (kFinished == state_) { + if (kExited == state_ || kAborted == state_) { return; } assert(kPaused == state_); @@ -57,14 +58,14 @@ class Thread : public std::enable_shared_from_this { const auto ret = lua_resume(*thlua, narg); switch (ret) { case 0: - state_ = kFinished; + state_ = kExited; onExited(thlua); return; case LUA_YIELD: state_ = kPaused; return; default: - state_ = kFinished; + state_ = kAborted; onAborted(thlua); return; } -- 2.45.2 From 8ebb5871e6c26a72fcf36cac0e5354fa5f2643da Mon Sep 17 00:00:00 2001 From: falsycat Date: Sat, 5 Aug 2023 20:46:09 +0900 Subject: [PATCH 09/18] add luajit::Context to Env in ContextFixture --- core/luajit/context_test.cc | 6 +++--- core/luajit/context_test.hh | 5 +++++ core/luajit/thread_test.cc | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/luajit/context_test.cc b/core/luajit/context_test.cc index 0e59748..9d807a6 100644 --- a/core/luajit/context_test.cc +++ b/core/luajit/context_test.cc @@ -7,11 +7,11 @@ using LuaJIT_Context = nf7::core::luajit::test::ContextFixture; using LuaJIT_Value = nf7::core::luajit::test::ContextFixture; TEST_P(LuaJIT_Context, CreateAndDestroy) { - auto sut = nf7::core::luajit::Context::Create(*env_, GetParam()); + auto sut = env_->Get(); EXPECT_EQ(sut->kind(), GetParam()); } TEST_P(LuaJIT_Context, Register) { - auto sut = nf7::core::luajit::Context::Create(*env_, GetParam()); + auto sut = env_->Get(); sut->Exec([](auto& ctx) { lua_createtable(*ctx, 0, 0); auto value = ctx.Register(); @@ -23,7 +23,7 @@ TEST_P(LuaJIT_Context, Register) { ConsumeTasks(); } TEST_P(LuaJIT_Context, Query) { - auto sut = nf7::core::luajit::Context::Create(*env_, GetParam()); + auto sut = env_->Get(); std::shared_ptr value; diff --git a/core/luajit/context_test.hh b/core/luajit/context_test.hh index 29d1d31..c952e62 100644 --- a/core/luajit/context_test.hh +++ b/core/luajit/context_test.hh @@ -103,6 +103,11 @@ class ContextFixture : public ::testing::TestWithParam { WrappedTaskQueue>(asyncq_); }, }, + { + typeid(Context), [this](auto& env) { + return Context::Create(env, GetParam()); + }, + } }); thread_ = std::thread {[this]() { asyncq_->Drive(async_driver_); }}; } diff --git a/core/luajit/thread_test.cc b/core/luajit/thread_test.cc index 3f604e0..945e7fc 100644 --- a/core/luajit/thread_test.cc +++ b/core/luajit/thread_test.cc @@ -15,7 +15,7 @@ class LuaJIT_Thread : public nf7::core::luajit::test::ContextFixture { template void TestThread( const auto& setup, const char* script, Args&&... args) { - auto lua = nf7::core::luajit::Context::Create(*env_, GetParam()); + auto lua = env_->Get(); auto called = uint32_t {0}; lua->Exec([&](auto& lua) { const auto compile = luaL_loadstring(*lua, script); -- 2.45.2 From 528d181cbf548aac4f53566ce8f892d03629aebe Mon Sep 17 00:00:00 2001 From: falsycat Date: Sat, 5 Aug 2023 21:44:20 +0900 Subject: [PATCH 10/18] set immutable environment for lua threads --- core/CMakeLists.txt | 1 + core/luajit/context.cc | 21 ++++++++++++++++++++- core/luajit/context.hh | 33 +++++++++++++++++++++++++++++++++ core/luajit/thread.cc | 30 ++++++++++++++++++++++++++++++ core/luajit/thread.hh | 3 +++ core/luajit/thread_test.cc | 21 +++++++++++++++++++++ 6 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 core/luajit/thread.cc diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index cfa3707..303ce7e 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -9,6 +9,7 @@ target_link_libraries(nf7_core target_sources(nf7_core PRIVATE luajit/context.cc + luajit/thread.cc version.cc PUBLIC luajit/context.hh diff --git a/core/luajit/context.cc b/core/luajit/context.cc index bfcef24..986e354 100644 --- a/core/luajit/context.cc +++ b/core/luajit/context.cc @@ -29,7 +29,26 @@ template class ContextImpl final : public Context { public: ContextImpl(const char* name, Kind kind, Env& env) - : Context(name, kind), tasq_(env.Get()) { } + : Context(name, kind), tasq_(env.Get()) { + auto L = state(); + + lua_pushthread(L); + if (luaL_newmetatable(L, "nf7::Context::ImmutableEnv")) { + lua_createtable(L, 0, 0); + { + luaL_newmetatable(L, kGlobalTableName); + lua_setfield(L, -2, "__index"); + + lua_pushcfunction(L, [](auto L) { + return luaL_error(L, "global is immutable"); + }); + lua_setfield(L, -2, "__newindex"); + } + lua_setmetatable(L, -2); + } + lua_setfenv(L, -2); + lua_pop(L, 1); + } void Push(Task&& task) noexcept override { auto self = std::dynamic_pointer_cast>(shared_from_this()); diff --git a/core/luajit/context.hh b/core/luajit/context.hh index faddaef..b3c1c67 100644 --- a/core/luajit/context.hh +++ b/core/luajit/context.hh @@ -2,12 +2,15 @@ #pragma once #include +#include #include +#include #include #include #include "iface/common/task.hh" +#include "iface/common/value.hh" #include "iface/subsys/interface.hh" #include "iface/env.hh" @@ -98,6 +101,34 @@ class TaskContext final { Query(v); } + template + T& NewUserData(T&& v) { + return *(new (lua_newuserdata(state_, sizeof(T))) T {std::move(v)}); + } + template + T& NewUserData(T&& v) { + return *(new (lua_newuserdata(state_, sizeof(T))) T {v}); + } + + template + T& CheckUserData(int index, const char* name) { + return CheckUserData(state_, index, name); + } + template + static T& CheckUserData(lua_State* L, int index, const char* name) { + return *reinterpret_cast(luaL_checkudata(L, index, name)); + } + + void Push(const nf7::Value&) noexcept { + lua_pushstring(state_, "hello"); + } + const nf7::Value& CheckValue(int index) noexcept { + return CheckValue(state_, index); + } + static const nf7::Value& CheckValue(lua_State* L, int index) { + return CheckUserData(L, index, "nf7::Value"); + } + const std::shared_ptr& context() const noexcept { return ctx_; } lua_State* state() const noexcept { return state_; } @@ -110,6 +141,8 @@ class Context : public subsys::Interface, public TaskQueue { public: + static constexpr auto kGlobalTableName = "nf7::Context::GlobalTable"; + using Item = Task; enum Kind { diff --git a/core/luajit/thread.cc b/core/luajit/thread.cc new file mode 100644 index 0000000..67b33ee --- /dev/null +++ b/core/luajit/thread.cc @@ -0,0 +1,30 @@ +// No copyright +#include "core/luajit/thread.hh" + +#include "core/luajit/context.hh" + + +namespace nf7::core::luajit { + +void Thread::SetUpThread() noexcept { + luaL_newmetatable(th_, Context::kGlobalTableName); + { + new (lua_newuserdata(th_, sizeof(this))) Thread* {this}; + if (luaL_newmetatable(th_, "nf7::Thread")) { + lua_createtable(th_, 0, 0); + { + lua_pushcfunction(th_, [](auto L) { + luaL_checkudata(L, 1, "nf7::Thread"); + return luaL_error(L, lua_tostring(L, 2)); + }); + lua_setfield(th_, -2, "throw"); + } + lua_setfield(th_, -2, "__index"); + } + lua_setmetatable(th_, -2); + lua_setfield(th_, -2, "nf7"); + } + lua_pop(th_, 1); +} + +} // namespace nf7::core::luajit diff --git a/core/luajit/thread.hh b/core/luajit/thread.hh index c36df30..4543266 100644 --- a/core/luajit/thread.hh +++ b/core/luajit/thread.hh @@ -50,6 +50,7 @@ class Thread : public std::enable_shared_from_this { return; } assert(kPaused == state_); + SetUpThread(); auto thlua = taskContext(lua); const auto narg = thlua.PushAll(std::forward(args)...); @@ -79,6 +80,8 @@ class Thread : public std::enable_shared_from_this { virtual void onAborted(TaskContext&) noexcept { } private: + void SetUpThread() noexcept; + TaskContext taskContext(const TaskContext& t) const noexcept { assert(t.context() == context_); return TaskContext {context_, th_}; diff --git a/core/luajit/thread_test.cc b/core/luajit/thread_test.cc index 945e7fc..04737ff 100644 --- a/core/luajit/thread_test.cc +++ b/core/luajit/thread_test.cc @@ -71,6 +71,27 @@ TEST_P(LuaJIT_Thread, RunAndError) { "return foo()"); } +TEST_P(LuaJIT_Thread, ForbidGlobalVariable) { + TestThread([](auto& sut) { + EXPECT_CALL(sut, onAborted) + .WillOnce([](auto& lua) { + EXPECT_THAT(lua_tostring(*lua, -1), ::testing::HasSubstr("immutable")); + }); + }, + "x = 1"); +} + +TEST_P(LuaJIT_Thread, StdThrow) { + TestThread([](auto& sut) { + EXPECT_CALL(sut, onAborted) + .WillOnce([](auto& lua) { + EXPECT_THAT(lua_tostring(*lua, -1), + ::testing::HasSubstr("hello world")); + }); + }, + "nf7:throw(\"hello world\")"); +} + INSTANTIATE_TEST_SUITE_P( SyncOrAsync, LuaJIT_Thread, -- 2.45.2 From ed73ab63fed1c85c258713f9b8313b6092e5002c Mon Sep 17 00:00:00 2001 From: falsycat Date: Sat, 5 Aug 2023 22:06:36 +0900 Subject: [PATCH 11/18] implement Push for nf7::Value --- core/luajit/context.cc | 25 +++++++++++++++++++++++++ core/luajit/context.hh | 6 ++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/core/luajit/context.cc b/core/luajit/context.cc index 986e354..e5b907b 100644 --- a/core/luajit/context.cc +++ b/core/luajit/context.cc @@ -24,6 +24,31 @@ void TaskContext::Query(const Value& value) noexcept { lua_rawgeti(state_, LUA_REGISTRYINDEX, value.index()); } +void TaskContext::Push(const nf7::Value& v) noexcept { + NewUserData(v); + if (luaL_newmetatable(state_, "nf7::Value")) { + lua_createtable(state_, 0, 0); + { + lua_pushcfunction(state_, [](auto L) { + const nf7::Value& v = CheckUserData(L, 1, "nf7::Value"); + lua_pushstring(L, + v.is() ? "null": + v.is()? "integer": + v.is() ? "real": + v.is() ? "buffer": + v.is() ? "object": + "unknown"); + return 1; + }); + lua_setfield(state_, -2, "type"); + + // TODO(falsycat) + } + lua_setfield(state_, -2, "__index"); + } + lua_setmetatable(state_, -2); +} + template class ContextImpl final : public Context { diff --git a/core/luajit/context.hh b/core/luajit/context.hh index b3c1c67..86a9707 100644 --- a/core/luajit/context.hh +++ b/core/luajit/context.hh @@ -106,7 +106,7 @@ class TaskContext final { return *(new (lua_newuserdata(state_, sizeof(T))) T {std::move(v)}); } template - T& NewUserData(T&& v) { + T& NewUserData(const T& v) { return *(new (lua_newuserdata(state_, sizeof(T))) T {v}); } @@ -119,9 +119,7 @@ class TaskContext final { return *reinterpret_cast(luaL_checkudata(L, index, name)); } - void Push(const nf7::Value&) noexcept { - lua_pushstring(state_, "hello"); - } + void Push(const nf7::Value&) noexcept; const nf7::Value& CheckValue(int index) noexcept { return CheckValue(state_, index); } -- 2.45.2 From cf3898d7bf28baa0ca03e07504928b601194c572 Mon Sep 17 00:00:00 2001 From: falsycat Date: Sat, 5 Aug 2023 22:07:16 +0900 Subject: [PATCH 12/18] add nf7:assert to luajit std table --- core/luajit/thread.cc | 10 ++++++++++ core/luajit/thread_test.cc | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/core/luajit/thread.cc b/core/luajit/thread.cc index 67b33ee..ea22c01 100644 --- a/core/luajit/thread.cc +++ b/core/luajit/thread.cc @@ -18,6 +18,16 @@ void Thread::SetUpThread() noexcept { return luaL_error(L, lua_tostring(L, 2)); }); lua_setfield(th_, -2, "throw"); + + lua_pushcfunction(th_, [](auto L) { + luaL_checkudata(L, 1, "nf7::Thread"); + if (lua_toboolean(L, 2)) { + return 0; + } else { + return luaL_error(L, "assertion failure"); + } + }); + lua_setfield(th_, -2, "assert"); } lua_setfield(th_, -2, "__index"); } diff --git a/core/luajit/thread_test.cc b/core/luajit/thread_test.cc index 04737ff..904daee 100644 --- a/core/luajit/thread_test.cc +++ b/core/luajit/thread_test.cc @@ -92,6 +92,19 @@ TEST_P(LuaJIT_Thread, StdThrow) { "nf7:throw(\"hello world\")"); } +TEST_P(LuaJIT_Thread, StdAssertWithTrue) { + TestThread([](auto& sut) { + EXPECT_CALL(sut, onExited); + }, + "nf7:assert(true)"); +} +TEST_P(LuaJIT_Thread, StdAssertWithFalse) { + TestThread([](auto& sut) { + EXPECT_CALL(sut, onAborted); + }, + "nf7:assert(false)"); +} + INSTANTIATE_TEST_SUITE_P( SyncOrAsync, LuaJIT_Thread, -- 2.45.2 From 1319a81ce284863e693413c33ddac3f1d1800766 Mon Sep 17 00:00:00 2001 From: falsycat Date: Sat, 5 Aug 2023 23:22:35 +0900 Subject: [PATCH 13/18] add luajit::TaskContext::Push(const nf7::Value&) --- core/luajit/thread.cc | 58 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/core/luajit/thread.cc b/core/luajit/thread.cc index ea22c01..7806c07 100644 --- a/core/luajit/thread.cc +++ b/core/luajit/thread.cc @@ -1,6 +1,8 @@ // No copyright #include "core/luajit/thread.hh" +#include + #include "core/luajit/context.hh" @@ -9,18 +11,20 @@ namespace nf7::core::luajit { void Thread::SetUpThread() noexcept { luaL_newmetatable(th_, Context::kGlobalTableName); { + static const char* kName = "nf7::core::luajit::Thread"; + new (lua_newuserdata(th_, sizeof(this))) Thread* {this}; - if (luaL_newmetatable(th_, "nf7::Thread")) { + if (luaL_newmetatable(th_, kName)) { lua_createtable(th_, 0, 0); { lua_pushcfunction(th_, [](auto L) { - luaL_checkudata(L, 1, "nf7::Thread"); + luaL_checkudata(L, 1, kName); return luaL_error(L, lua_tostring(L, 2)); }); lua_setfield(th_, -2, "throw"); lua_pushcfunction(th_, [](auto L) { - luaL_checkudata(L, 1, "nf7::Thread"); + luaL_checkudata(L, 1, kName); if (lua_toboolean(L, 2)) { return 0; } else { @@ -28,6 +32,54 @@ void Thread::SetUpThread() noexcept { } }); lua_setfield(th_, -2, "assert"); + + lua_pushcfunction(th_, ([](auto L) { + auto th = TaskContext::CheckUserData(L, 1, kName); + + TaskContext lua {th->context_, L}; + + const auto type = std::string {lua_tostring(L, 3)}; + if (type.empty()) { + const auto type = lua_type(L, 2); + switch (type) { + case LUA_TNIL: + lua.Push(nf7::Value {}); + break; + case LUA_TNUMBER: + lua.Push(nf7::Value { + static_cast(lua_tonumber(L, 2))}); + break; + case LUA_TSTRING: { + size_t len; + const auto ptr = lua_tolstring(L, 2, &len); + lua.Push(nf7::Value::MakeBuffer(ptr, ptr+len)); + } break; + case LUA_TUSERDATA: + lua.Push(lua.CheckValue(2)); + break; + default: + return luaL_error(L, "invalid type in #1 param"); + } + } else { + if ("null" == type) { + lua.Push(nf7::Value {}); + } else if ("integer" == type) { + lua.Push(nf7::Value { + static_cast(lua_tointeger(L, 2))}); + } else if ("real" == type) { + lua.Push(nf7::Value { + static_cast(lua_tonumber(L, 2))}); + } else if ("string" == type) { + size_t len; + const auto ptr = lua_tolstring(L, 2, &len); + lua.Push(nf7::Value::MakeBuffer(ptr, ptr+len)); + } else { + return luaL_error(L, "unknown type specifier: %s", type.c_str()); + } + } + return 1; + })); + lua_setfield(th_, -2, "value"); } lua_setfield(th_, -2, "__index"); } -- 2.45.2 From ca16c6948a6c30ed5ce16e1e6bf5e9085d19dcc9 Mon Sep 17 00:00:00 2001 From: falsycat Date: Sat, 5 Aug 2023 23:23:47 +0900 Subject: [PATCH 14/18] fix an issue that ConsumeTasks() ends before all tasks consumed --- core/luajit/context_test.hh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/core/luajit/context_test.hh b/core/luajit/context_test.hh index c952e62..8bf2e59 100644 --- a/core/luajit/context_test.hh +++ b/core/luajit/context_test.hh @@ -125,10 +125,11 @@ class ContextFixture : public ::testing::TestWithParam { } void ConsumeTasks() noexcept { - SyncDriver sync_driver; - syncq_->Drive(sync_driver); - - WaitAsyncTasks(std::chrono::seconds(1)); + for (uint32_t i = 0; i < 16; ++i) { + SyncDriver sync_driver; + syncq_->Drive(sync_driver); + WaitAsyncTasks(std::chrono::seconds(1)); + } } void WaitAsyncTasks(auto dur) noexcept { if (!asyncq_->WaitForEmpty(dur)) { -- 2.45.2 From 1bcbd786f530aec678dbe88ee7a7bdffee2f0ade Mon Sep 17 00:00:00 2001 From: falsycat Date: Sun, 6 Aug 2023 07:38:10 +0900 Subject: [PATCH 15/18] add nf7::Value::operator== --- iface/common/value.hh | 19 +++++++++++++- iface/common/value_test.cc | 52 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/iface/common/value.hh b/iface/common/value.hh index 70eb048..d3bf310 100644 --- a/iface/common/value.hh +++ b/iface/common/value.hh @@ -23,10 +23,14 @@ namespace nf7 { class Value final { public: - struct Null { }; using Integer = int64_t; using Real = double; + class Null { + public: + bool operator==(const Null&) const noexcept { return true; } + }; + class Buffer final { public: Buffer() = default; @@ -48,6 +52,10 @@ class Value final { return *this; } + bool operator==(const Buffer& other) const noexcept { + return size_ == other.size_ && buf_ == other.buf_; + } + template std::span span() const noexcept { return {begin(), end()}; @@ -111,6 +119,10 @@ class Value final { return itr->second; } + bool operator==(const Object& other) const noexcept { + return size_ == other.size_ && pairs_ == other.pairs_; + } + const Value& at( uint64_t index, const Value& def = {}) const noexcept { return index < size_? pairs_[index].second: def; @@ -215,11 +227,16 @@ class Value final { Value(Object&& v) noexcept : var_(std::move(v)) { } Value(const Object& v) noexcept : var_(v) { } + public: Value(const Value&) = default; Value(Value&&) = default; Value& operator=(const Value&) = default; Value& operator=(Value&&) = default; + bool operator==(const Value& other) const noexcept { + return var_ == other.var_; + } + public: template const T& as( diff --git a/iface/common/value_test.cc b/iface/common/value_test.cc index c947a24..d5f91e0 100644 --- a/iface/common/value_test.cc +++ b/iface/common/value_test.cc @@ -19,6 +19,15 @@ TEST(Value, NullAsInvalid) { EXPECT_THROW(v.as(), nf7::Exception); EXPECT_THROW(v.as(), nf7::Exception); } +TEST(Value, NullEqual) { + EXPECT_EQ(nf7::Value::MakeNull(), nf7::Value::MakeNull()); +} +TEST(Value, NullNotEqual) { + EXPECT_NE(nf7::Value::MakeNull(), nf7::Value::MakeInteger(0)); + EXPECT_NE(nf7::Value::MakeNull(), nf7::Value::MakeReal(0)); + EXPECT_NE(nf7::Value::MakeNull(), nf7::Value::MakeBuffer({})); + EXPECT_NE(nf7::Value::MakeNull(), nf7::Value::MakeObject({})); +} TEST(Value, IntegerAsInteger) { const auto v = nf7::Value::MakeInteger(777); @@ -48,6 +57,16 @@ TEST(Value, IntegerAsInvalidNum) { EXPECT_THROW(v.num(), nf7::Exception); EXPECT_THROW(v.num(int8_t {77}), nf7::Exception); } +TEST(Value, IntegerEqual) { + EXPECT_EQ(nf7::Value::MakeInteger(666), nf7::Value::MakeInteger(666)); +} +TEST(Value, IntegerNotEqual) { + EXPECT_NE(nf7::Value::MakeInteger(666), nf7::Value::MakeInteger(777)); + EXPECT_NE(nf7::Value::MakeInteger(666), nf7::Value::MakeNull()); + EXPECT_NE(nf7::Value::MakeInteger(666), nf7::Value::MakeReal(0)); + EXPECT_NE(nf7::Value::MakeInteger(666), nf7::Value::MakeBuffer({})); + EXPECT_NE(nf7::Value::MakeInteger(666), nf7::Value::MakeObject({})); +} TEST(Value, RealAsReal) { const auto v = nf7::Value::MakeReal(777); @@ -77,6 +96,16 @@ TEST(Value, RealAsInvalidNum) { EXPECT_THROW(v.num(), nf7::Exception); EXPECT_THROW(v.num(int8_t {77}), nf7::Exception); } +TEST(Value, RealEqual) { + EXPECT_EQ(nf7::Value::MakeReal(0.5), nf7::Value::MakeReal(0.5)); +} +TEST(Value, RealNotEqual) { + EXPECT_NE(nf7::Value::MakeReal(1), nf7::Value::MakeReal(0.5)); + EXPECT_NE(nf7::Value::MakeReal(1), nf7::Value::MakeNull()); + EXPECT_NE(nf7::Value::MakeReal(1), nf7::Value::MakeInteger(1)); + EXPECT_NE(nf7::Value::MakeReal(1), nf7::Value::MakeBuffer({})); + EXPECT_NE(nf7::Value::MakeReal(1), nf7::Value::MakeObject({})); +} TEST(Value, BufferAsBuffer) { const auto v = nf7::Value::MakeBuffer({}); @@ -93,6 +122,18 @@ TEST(Value, BufferAsInvalid) { EXPECT_THROW(v.as(), nf7::Exception); EXPECT_THROW(v.as(), nf7::Exception); } +TEST(Value, BufferEqual) { + const auto v = nf7::Value::MakeBuffer({}); + EXPECT_EQ(v, v); +} +TEST(Value, BufferNotEqual) { + EXPECT_NE(nf7::Value::MakeBuffer({}), nf7::Value::MakeNull()); + EXPECT_NE(nf7::Value::MakeBuffer({}), nf7::Value::MakeInteger(0)); + EXPECT_NE(nf7::Value::MakeBuffer({}), nf7::Value::MakeReal(0)); + EXPECT_NE(nf7::Value::MakeBuffer({}), + nf7::Value::MakeBuffer({})); + EXPECT_NE(nf7::Value::MakeBuffer({}), nf7::Value::MakeObject({})); +} TEST(Value, ObjectAsObject) { const auto v = nf7::Value::MakeObject({}); @@ -109,6 +150,17 @@ TEST(Value, ObjectAsInvalid) { EXPECT_THROW(v.as(), nf7::Exception); EXPECT_THROW(v.as(), nf7::Exception); } +TEST(Value, ObjectEqual) { + const auto v = nf7::Value::MakeObject({}); + EXPECT_EQ(v, v); +} +TEST(Value, ObjectNotEqual) { + EXPECT_NE(nf7::Value::MakeObject({}), nf7::Value::MakeNull()); + EXPECT_NE(nf7::Value::MakeObject({}), nf7::Value::MakeInteger(0)); + EXPECT_NE(nf7::Value::MakeObject({}), nf7::Value::MakeReal(0)); + EXPECT_NE(nf7::Value::MakeObject({}), nf7::Value::MakeBuffer({})); + EXPECT_NE(nf7::Value::MakeObject({}), nf7::Value::MakeObject({})); +} TEST(Value_Buffer, Make) { const auto value = nf7::Value::MakeBuffer({1, 2, 3, 4}); -- 2.45.2 From 90a543cd759a4cd83bc677850789b6ccd7c7482f Mon Sep 17 00:00:00 2001 From: falsycat Date: Sun, 6 Aug 2023 07:38:37 +0900 Subject: [PATCH 16/18] add luajit::Lambda --- core/CMakeLists.txt | 3 + core/luajit/lambda.cc | 140 +++++++++++++++++++++++++++++++++++++ core/luajit/lambda.hh | 58 +++++++++++++++ core/luajit/lambda_test.cc | 135 +++++++++++++++++++++++++++++++++++ 4 files changed, 336 insertions(+) create mode 100644 core/luajit/lambda.cc create mode 100644 core/luajit/lambda.hh create mode 100644 core/luajit/lambda_test.cc diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index 303ce7e..210de03 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -9,10 +9,12 @@ target_link_libraries(nf7_core target_sources(nf7_core PRIVATE luajit/context.cc + luajit/lambda.cc luajit/thread.cc version.cc PUBLIC luajit/context.hh + luajit/lambda.hh luajit/thread.hh version.hh ) @@ -22,6 +24,7 @@ target_sources(nf7_core_test PRIVATE luajit/context_test.cc luajit/context_test.hh + luajit/lambda_test.cc luajit/thread_test.cc luajit/thread_test.hh ) diff --git a/core/luajit/lambda.cc b/core/luajit/lambda.cc new file mode 100644 index 0000000..e0c3e9b --- /dev/null +++ b/core/luajit/lambda.cc @@ -0,0 +1,140 @@ +// No copyright +#include "core/luajit/lambda.hh" + +#include + +#include "core/luajit/context.hh" + +namespace nf7::core::luajit { + +class Lambda::Thread : public luajit::Thread { + public: + using luajit::Thread::Thread; + + void Attach(const std::shared_ptr& la) noexcept { + recvq_size_before_run_ = la->recvq_.size(); + recv_count_before_run_ = la->recv_count_; + la_ = la; + } + + private: + void onExited(TaskContext& lua) noexcept override { + if (auto la = la_.lock()) { + ++la->exit_count_; + TryResume(lua, la); + } + } + void onAborted(TaskContext& lua) noexcept override { + if (auto la = la_.lock()) { + ++la->abort_count_; + TryResume(lua, la); + } + } + void TryResume(TaskContext& lua, const std::shared_ptr& la) noexcept { + auto self = std::move(la->thread_); + + const bool no_pop = recvq_size_before_run_ == la->recvq_.size(); + const bool no_push = recv_count_before_run_ == la->recv_count_; + if ((no_pop && no_push) || la->recvq_.empty()) { + return; + } + lua.context()->Exec([wla = la_](auto& lua) { + if (auto la = wla.lock()) { + la->Resume(lua); + } + }); + } + + private: + std::weak_ptr la_; + size_t recvq_size_before_run_ = 0; + size_t recv_count_before_run_ = 0; +}; + + +void Lambda::Main(const nf7::Value& v) noexcept { + lua_->Exec([this, self = shared_from_this(), v](auto& lua) { + recvq_.push_back(v); + ++recv_count_; + Resume(lua); + }); +} + +void Lambda::Resume(TaskContext& lua) noexcept { + if (recvq_.empty()) { + // skip resuming until this lambda takes next value if the queue is empty + return; + } + + if (!ctx_) { + // create context if it's a first time + PushLuaContextObject(lua); + ctx_ = lua.Register(); + } + if (awaiting_value_ && nullptr != thread_) { + // thread is paused by recv() so resume it with a value + const auto v = recvq_.front(); + recvq_.pop_front(); + thread_->Resume(lua, v); + } else { + if (nullptr != thread_) { + // the active thread is paused by a reason except recv() + // in this case, thread_->Resume() is done by one who yielded + return; + } + // start the thread + thread_ = luajit::Thread::Make(lua, func_); + thread_->Attach(shared_from_this()); + thread_->Resume(lua, ctx_); + } +} + +void Lambda::PushLuaContextObject(TaskContext& lua) noexcept { + static const auto kName = "nf7::core::luajit::Lambda"; + static const auto self = [](auto L) { + auto la = TaskContext:: + CheckUserData>(L, 1, kName).lock(); + if (!la) { + luaL_error(L, "lambda expired"); + } + return la; + }; + + lua.NewUserData(weak_from_this()); + if (luaL_newmetatable(*lua, kName)) { + lua_pushcfunction(*lua, [](auto L) { + TaskContext:: + CheckUserData>(L, 1, kName).~weak_ptr(); + return 0; + }); + lua_setfield(*lua, -2, "__gc"); + + lua_createtable(*lua, 0, 0); + { + lua_pushcfunction(*lua, [](auto L) { + const auto la = self(L); + if (la->recvq_.empty()) { + la->awaiting_value_ = true; + return lua_yield(L, 0); + } + (TaskContext {la->lua_, L}).Push(la->recvq_.front()); + la->recvq_.pop_front(); + return 1; + }); + lua_setfield(*lua, -2, "recv"); + + lua_pushcfunction(*lua, [](auto L) { + const auto la = self(L); + const auto v = (TaskContext {la->lua_, L}).CheckValue(2); + la->concurrency_->Exec( + [la, v](auto&) { la->emitter()->Emit(nf7::Value {v}); }); + return 1; + }); + lua_setfield(*lua, -2, "send"); + } + lua_setfield(*lua, -2, "__index"); + } + lua_setmetatable(*lua, -2); +} + +} // namespace nf7::core::luajit diff --git a/core/luajit/lambda.hh b/core/luajit/lambda.hh new file mode 100644 index 0000000..8b43494 --- /dev/null +++ b/core/luajit/lambda.hh @@ -0,0 +1,58 @@ +// No copyright +#pragma once + +#include +#include +#include +#include +#include + +#include "iface/subsys/concurrency.hh" +#include "iface/env.hh" +#include "iface/lambda.hh" + +#include "core/luajit/context.hh" +#include "core/luajit/thread.hh" + +namespace nf7::core::luajit { + +class Lambda : + public nf7::LambdaBase, + public std::enable_shared_from_this { + public: + explicit Lambda(nf7::Env& env, const std::shared_ptr& func) + : LambdaBase(), + concurrency_(env.Get()), + lua_(env.Get()), + func_(func) { } + + uint64_t exitCount() const noexcept { return exit_count_; } + uint64_t abortCount() const noexcept { return abort_count_; } + + private: + class Thread; + + private: + void Main(const nf7::Value& v) noexcept override; + + void Resume(TaskContext&) noexcept; + void PushLuaContextObject(TaskContext&) noexcept; + + private: + const std::shared_ptr concurrency_; + + const std::shared_ptr lua_; + const std::shared_ptr func_; + + std::shared_ptr thread_; + std::shared_ptr ctx_; + + std::atomic exit_count_ = 0; + std::atomic abort_count_ = 0; + + std::deque recvq_; + uint64_t recv_count_ = 0; + bool awaiting_value_ = false; +}; + +} // namespace nf7::core::luajit diff --git a/core/luajit/lambda_test.cc b/core/luajit/lambda_test.cc new file mode 100644 index 0000000..6a676cd --- /dev/null +++ b/core/luajit/lambda_test.cc @@ -0,0 +1,135 @@ +// No copyright +#include "core/luajit/lambda.hh" + +#include + +#include +#include + +#include "core/luajit/context.hh" + +#include "iface/common/observer_test.hh" + +#include "core/luajit/context_test.hh" + + +class LuaJIT_Lambda : public nf7::core::luajit::test::ContextFixture { + public: + using ContextFixture::ContextFixture; + + std::shared_ptr Compile( + const char* script) noexcept { + auto lua = env_->Get(); + + std::shared_ptr func; + lua->Exec([&](auto& lua) { + luaL_loadstring(*lua, script); + func = lua.Register(); + }); + ConsumeTasks(); + return func; + } + + void Expect(const char* script, + const std::vector& in, + uint32_t expectExit = 1, uint32_t expectAbort = 0, + const std::vector& out = {}) { + auto func = Compile(script); + + auto sut = std::make_shared(*env_, func); + for (const auto& v : in) { + sut->taker()->Take(v); + } + + ::testing::StrictMock< + nf7::test::ObserverMock> obs {*sut->maker()}; + + ::testing::Sequence seq; + for (const auto& v : out) { + EXPECT_CALL(obs, NotifyWithMove(nf7::Value {v})) + .InSequence(seq); + } + + ConsumeTasks(); + EXPECT_EQ(sut->exitCount(), expectExit); + EXPECT_EQ(sut->abortCount(), expectAbort); + } +}; + + +TEST_P(LuaJIT_Lambda, Run) { + Expect("local ctx = ...", {nf7::Value {}}); +} +TEST_P(LuaJIT_Lambda, RunWithMultiInputs) { + Expect("local ctx = ...", + {nf7::Value {}, nf7::Value {}, nf7::Value {}}, + 3); +} + +TEST_P(LuaJIT_Lambda, CtxRecv) { + Expect( + "local ctx = ...\nnf7:assert(\"integer\" == ctx:recv():type())", + {nf7::Value::Integer {77}}); +} +TEST_P(LuaJIT_Lambda, CtxRecvWithMultiInput) { + Expect("local ctx = ...\nnf7:assert(\"null\" == ctx:recv():type())", + {nf7::Value {}, nf7::Value {}, nf7::Value {}}, + 3, 0); +} +TEST_P(LuaJIT_Lambda, CtxMultiRecv) { + Expect("local ctx = ...\n" + "nf7:assert(\"null\" == ctx:recv():type())\n" + "nf7:assert(\"integer\" == ctx:recv():type())", + {nf7::Value::Null {}, nf7::Value::Integer {}}); +} +TEST_P(LuaJIT_Lambda, CtxMultiRecvWithDelay) { + auto func = Compile("local ctx = ...\n" + "nf7:assert(\"null\" == ctx:recv():type())\n" + "nf7:assert(\"integer\" == ctx:recv():type())"); + + auto sut = std::make_shared(*env_, func); + sut->taker()->Take(nf7::Value::Null {}); + ConsumeTasks(); + EXPECT_EQ(sut->exitCount(), 0); + EXPECT_EQ(sut->abortCount(), 0); + + sut->taker()->Take(nf7::Value::Integer {}); + ConsumeTasks(); + EXPECT_EQ(sut->exitCount(), 1); + EXPECT_EQ(sut->abortCount(), 0); +} +TEST_P(LuaJIT_Lambda, CtxMultiRecvAbort) { + Expect("local ctx = ...\n" + "nf7:assert(\"null\" == ctx:recv():type())\n" + "nf7:assert(\"integer\" == ctx:recv():type())", + {nf7::Value::Null {}}, + 0, 0); +} + +TEST_P(LuaJIT_Lambda, CtxSend) { + Expect( + "local ctx = ...\nctx:send(nf7:value())", + {nf7::Value {}}, + 1, 0, + {nf7::Value {}}); +} +TEST_P(LuaJIT_Lambda, CtxSendWithMultiInput) { + Expect( + "local ctx = ...\nctx:send(nf7:value())", + {nf7::Value {}, nf7::Value {}, nf7::Value {}}, + 3, 0, + {nf7::Value {}, nf7::Value {}, nf7::Value {}}); +} +TEST_P(LuaJIT_Lambda, CtxMultiSend) { + Expect( + "local ctx = ...\nctx:send(nf7:value())\nctx:send(nf7:value())", + {nf7::Value {}}, + 1, 0, + {nf7::Value {}, nf7::Value {}}); +} + +INSTANTIATE_TEST_SUITE_P( + SyncOrAsync, LuaJIT_Lambda, + testing::Values( + nf7::core::luajit::Context::kSync, + nf7::core::luajit::Context::kAsync)); -- 2.45.2 From 9ac59076879a230932ccd9622362d4c12f7c7bfc Mon Sep 17 00:00:00 2001 From: falsycat Date: Sun, 6 Aug 2023 09:28:31 +0900 Subject: [PATCH 17/18] improve error message of nf7:value() function --- core/luajit/thread.cc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/luajit/thread.cc b/core/luajit/thread.cc index 7806c07..dc6e69e 100644 --- a/core/luajit/thread.cc +++ b/core/luajit/thread.cc @@ -38,10 +38,10 @@ void Thread::SetUpThread() noexcept { TaskContext lua {th->context_, L}; - const auto type = std::string {lua_tostring(L, 3)}; + const auto type = std::string {luaL_optstring(L, 3, "")}; if (type.empty()) { - const auto type = lua_type(L, 2); - switch (type) { + switch (lua_type(L, 2)) { + case LUA_TNONE: case LUA_TNIL: lua.Push(nf7::Value {}); break; @@ -58,7 +58,7 @@ void Thread::SetUpThread() noexcept { lua.Push(lua.CheckValue(2)); break; default: - return luaL_error(L, "invalid type in #1 param"); + return luaL_error(L, "invalid type to make a value"); } } else { if ("null" == type) { -- 2.45.2 From 585f0daecdc785c4d9fbbac428efb3cdb0d009ca Mon Sep 17 00:00:00 2001 From: falsycat Date: Sun, 6 Aug 2023 09:36:41 +0900 Subject: [PATCH 18/18] add unittests for nf7:value --- core/luajit/thread.cc | 2 +- core/luajit/thread_test.cc | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/core/luajit/thread.cc b/core/luajit/thread.cc index dc6e69e..95d694c 100644 --- a/core/luajit/thread.cc +++ b/core/luajit/thread.cc @@ -69,7 +69,7 @@ void Thread::SetUpThread() noexcept { } else if ("real" == type) { lua.Push(nf7::Value { static_cast(lua_tonumber(L, 2))}); - } else if ("string" == type) { + } else if ("buffer" == type) { size_t len; const auto ptr = lua_tolstring(L, 2, &len); lua.Push(nf7::Value::MakeBuffer(ptr, ptr+len)); diff --git a/core/luajit/thread_test.cc b/core/luajit/thread_test.cc index 904daee..147a891 100644 --- a/core/luajit/thread_test.cc +++ b/core/luajit/thread_test.cc @@ -105,6 +105,40 @@ TEST_P(LuaJIT_Thread, StdAssertWithFalse) { "nf7:assert(false)"); } +TEST_P(LuaJIT_Thread, StdValueWithNull) { + TestThread([](auto& sut) { + EXPECT_CALL(sut, onExited); + }, + "nf7:assert(nf7:value() :type() == \"null\")\n" + "nf7:assert(nf7:value(nil, \"null\"):type() == \"null\")\n"); +} +TEST_P(LuaJIT_Thread, StdValueWithInteger) { + TestThread([](auto& sut) { + EXPECT_CALL(sut, onExited); + }, + "nf7:assert(nf7:value(666, \"integer\"):type() == \"integer\")"); +} +TEST_P(LuaJIT_Thread, StdValueWithReal) { + TestThread([](auto& sut) { + EXPECT_CALL(sut, onExited); + }, + "nf7:assert(nf7:value(666) :type() == \"real\")\n" + "nf7:assert(nf7:value(666, \"real\"):type() == \"real\")"); +} +TEST_P(LuaJIT_Thread, StdValueWithBuffer) { + TestThread([](auto& sut) { + EXPECT_CALL(sut, onExited); + }, + "nf7:assert(nf7:value(\"hello\") :type() == \"buffer\")\n" + "nf7:assert(nf7:value(\"hello\", \"buffer\"):type() == \"buffer\")"); +} +TEST_P(LuaJIT_Thread, StdValueWithValue) { + TestThread([](auto& sut) { + EXPECT_CALL(sut, onExited); + }, + "nf7:assert(nf7:value(nf7:value()):type() == \"null\")"); +} + INSTANTIATE_TEST_SUITE_P( SyncOrAsync, LuaJIT_Thread, -- 2.45.2