forked from OrcaSlicer/OrcaSlicer
-
Notifications
You must be signed in to change notification settings - Fork 62
feat: Flutter engine integration #427
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
LuckZAE
wants to merge
7
commits into
Snapmaker:feature_flutter_engine
Choose a base branch
from
LuckZAE:develop
base: feature_flutter_engine
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
eac4237
feat: add FlutterChannel communication layer
LuckZAE e72a9e6
feat: enhance middleware (Logging/ThreadGuard) and add MessageQueue
LuckZAE 81f51ce
fix: clean up fired timers in TimeoutMiddleware to prevent memory leak
LuckZAE 31aaea7
fix: executeChain onOutbound now covers all middlewares, not just [st…
LuckZAE 47ef4fa
fix: replace assert with runtime IsMain guard in invoke()
LuckZAE b156e69
fix: onOutbound in executeChain error paths cover all middlewares; m_…
LuckZAE 98fb6f8
fix: add destructor to TimeoutMiddleware to stop timers before base c…
LuckZAE File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| #include "FlutterChannel.h" | ||
| #include "FlutterMiddleware.h" | ||
| #include <wx/app.h> | ||
| #include <boost/log/trivial.hpp> | ||
|
|
||
| // ── FlutterChannel ──────────────────────────────────────────────── | ||
|
|
||
| FlutterChannel::FlutterChannel(const std::string& name) | ||
| : m_channelName(name) | ||
| { | ||
| m_routes["system.ready"] = [this](const std::string&, FlutterViewHost::Reply) { | ||
| if (!m_view) { | ||
| BOOST_LOG_TRIVIAL(warning) << "[flutter] drain: view is null, dropping " | ||
| << m_pendingMessages.size() << " messages"; | ||
| m_pendingMessages.clear(); | ||
| return; | ||
| } | ||
| m_dartReady = true; | ||
| while (!m_pendingMessages.empty()) { | ||
| auto& msg = m_pendingMessages.front(); | ||
| try { | ||
| m_view->invokeMethod(msg.first, msg.second); | ||
| } catch (const std::exception& e) { | ||
| BOOST_LOG_TRIVIAL(error) << "[flutter] drain failed for " | ||
| << msg.first << ": " << e.what(); | ||
| } catch (...) { | ||
| BOOST_LOG_TRIVIAL(error) << "[flutter] drain failed for " | ||
| << msg.first; | ||
| } | ||
| m_pendingMessages.pop_front(); | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| FlutterChannel& FlutterChannel::on(const std::string& method, Handler h) { | ||
| assert(!m_handlerCalled && "FlutterChannel::on: called after handler()"); | ||
| assert(h); | ||
| if (method == "system.ready") { | ||
| BOOST_LOG_TRIVIAL(warning) << "[flutter] system.ready is reserved, ignoring user handler"; | ||
| return *this; | ||
| } | ||
| assert(m_routes.find(method) == m_routes.end() && | ||
| "FlutterChannel::on: duplicate method"); | ||
| m_routes[method] = std::move(h); | ||
| return *this; | ||
| } | ||
|
|
||
| FlutterChannel& FlutterChannel::use(std::shared_ptr<Middleware> m) { | ||
| assert(m); | ||
| assert(!m_handlerCalled && "FlutterChannel::use: called after handler()"); | ||
| if (!m_handlerCalled) | ||
| m_middleware.push_back(std::move(m)); | ||
| return *this; | ||
| } | ||
|
|
||
| FlutterViewHost::MethodCallHandler | ||
| FlutterChannel::handler(FlutterViewHost* view) { | ||
| assert(view); | ||
| assert(!m_handlerCalled && "FlutterChannel::handler: already called"); | ||
| m_view = view; | ||
| m_handlerCalled = true; | ||
|
|
||
| auto routes = m_routes; | ||
| auto middleware = std::make_shared<std::vector<std::shared_ptr<Middleware>>>( | ||
| m_middleware); | ||
|
|
||
| auto replied = std::make_shared<bool>(false); | ||
| return [routes = std::move(routes), middleware, replied]( | ||
| const std::string& method, const std::string& args, | ||
| FlutterViewHost::Reply rawReply) { | ||
|
|
||
| FlutterViewHost::Reply reply = [replied, rawReply = std::move(rawReply)] | ||
| (const std::string& r) { | ||
| if (*replied) return; | ||
| *replied = true; | ||
| rawReply(r); | ||
| }; | ||
|
|
||
| // onInbound (forward) | ||
| auto& mws = *middleware; | ||
|
|
||
| auto executeChain = [&](const std::string& m, const std::string& a, | ||
| FlutterViewHost::Reply r, size_t startIdx = 0) { | ||
| for (size_t i = startIdx; i < mws.size(); ++i) { | ||
| try { | ||
| if (!mws[i]->onInbound(m, a, r)) { | ||
| for (int j = static_cast<int>(i) - 1; j >= 0; --j) | ||
| mws[j]->onOutbound(m, a); | ||
| return; | ||
| } | ||
| } catch (const std::exception& e) { | ||
| r(std::string("Error: ") + e.what()); | ||
| for (int j = static_cast<int>(i) - 1; j >= 0; --j) | ||
| mws[j]->onOutbound(m, a); | ||
| return; | ||
| } catch (...) { | ||
| r("Error: middleware exception"); | ||
| for (int j = static_cast<int>(i) - 1; j >= 0; --j) | ||
| mws[j]->onOutbound(m, a); | ||
| return; | ||
| } | ||
| } | ||
| auto it = routes.find(m); | ||
| if (it != routes.end()) { | ||
| try { it->second(a, r); } | ||
| catch (const std::exception& e) { r(std::string("Error: ") + e.what()); } | ||
| catch (...) { r("Error: unknown"); } | ||
| } else { | ||
| r(""); | ||
| } | ||
| for (int i = static_cast<int>(mws.size()) - 1; i >= 0; --i) | ||
| mws[i]->onOutbound(m, a); | ||
| }; | ||
|
|
||
| for (size_t i = 0; i < mws.size(); ++i) { | ||
| bool pass = false; | ||
| try { | ||
| pass = mws[i]->onInbound(method, args, reply); | ||
| } catch (const std::exception& e) { | ||
| reply(std::string("Error: ") + e.what()); | ||
| for (int j = static_cast<int>(i) - 1; j >= 0; --j) | ||
| mws[j]->onOutbound(method, args); | ||
| return; | ||
| } catch (...) { | ||
| reply("Error: middleware exception"); | ||
| for (int j = static_cast<int>(i) - 1; j >= 0; --j) | ||
| mws[j]->onOutbound(method, args); | ||
| return; | ||
| } | ||
| if (!pass) { | ||
| // ThreadGuardMiddleware defers execution to UI thread via CallAfter. | ||
| // dynamic_pointer_cast is used intentionally instead of a virtual method | ||
| // on the Middleware base class to keep the base interface minimal. | ||
| if (auto guard = std::dynamic_pointer_cast<ThreadGuardMiddleware>(mws[i])) { | ||
| guard->CallAfter([executeChain, method, args, reply, i]() { | ||
| executeChain(method, args, reply, i); | ||
| }); | ||
| return; | ||
| } | ||
| for (int j = static_cast<int>(i) - 1; j >= 0; --j) | ||
| mws[j]->onOutbound(method, args); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| // All middlewares passed — run handler + onOutbound | ||
| auto it = routes.find(method); | ||
| if (it != routes.end()) { | ||
| try { it->second(args, reply); } | ||
| catch (const std::exception& e) { reply(std::string("Error: ") + e.what()); } | ||
| catch (...) { reply("Error: unknown"); } | ||
| } else { | ||
| reply(""); | ||
| } | ||
| for (int i = static_cast<int>(mws.size()) - 1; i >= 0; --i) | ||
| mws[i]->onOutbound(method, args); | ||
| }; | ||
| } | ||
|
|
||
| void FlutterChannel::invoke(const std::string& method, const std::string& args) | ||
| { | ||
| if (!wxThread::IsMain()) { | ||
| BOOST_LOG_TRIVIAL(warning) | ||
| << "[flutter] invoke(" << method << ") called from non-UI thread, dropping"; | ||
| return; | ||
| } | ||
| if (!m_dartReady) { | ||
| if (m_pendingMessages.size() >= 256) { | ||
| BOOST_LOG_TRIVIAL(warning) << "[flutter] queue full, dropping oldest message"; | ||
| m_pendingMessages.pop_front(); | ||
| } | ||
| m_pendingMessages.emplace_back(method, args); | ||
| return; | ||
| } | ||
| if (m_view) m_view->invokeMethod(method, args); | ||
| } | ||
|
|
||
| FlutterChannel::Namespace FlutterChannel::ns(const std::string& prefix) { | ||
| return Namespace(this, prefix); | ||
| } | ||
|
|
||
| // ── Namespace ───────────────────────────────────────────────────── | ||
|
|
||
| FlutterChannel::Namespace::Namespace(FlutterChannel* parent, | ||
| std::string prefix) | ||
| : m_parent(parent), m_prefix(std::move(prefix)) {} | ||
|
|
||
| FlutterChannel::Namespace& | ||
| FlutterChannel::Namespace::on(const std::string& method, Handler h) { | ||
| m_parent->on(m_prefix + "." + method, std::move(h)); | ||
| return *this; | ||
| } | ||
|
|
||
| void FlutterChannel::Namespace::invoke(const std::string& method, | ||
| const std::string& args) { | ||
| m_parent->invoke(m_prefix + "." + method, args); | ||
| } | ||
|
|
||
| // ── Middleware defaults ─────────────────────────────────────────── | ||
|
|
||
| bool FlutterChannel::Middleware::onInbound(const std::string&, | ||
| const std::string&, Reply) { | ||
| return true; | ||
| } | ||
|
|
||
| void FlutterChannel::Middleware::onOutbound(const std::string&, | ||
| const std::string&) {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| #pragma once | ||
| #include "flutter_host.h" | ||
| #include <string> | ||
| #include <functional> | ||
| #include <memory> | ||
| #include <deque> | ||
| #include <unordered_map> | ||
| #include <utility> | ||
| #include <vector> | ||
| #include <cassert> | ||
|
|
||
| class FlutterChannel { | ||
| public: | ||
| using Handler = std::function<void(const std::string&, FlutterViewHost::Reply)>; | ||
| using Reply = FlutterViewHost::Reply; | ||
| using MiddlewarePtr = std::shared_ptr<class Middleware>; | ||
|
|
||
| explicit FlutterChannel(const std::string& name); | ||
| FlutterChannel(const FlutterChannel&) = delete; | ||
| FlutterChannel& operator=(const FlutterChannel&) = delete; | ||
|
|
||
| // Handler registration (must precede handler(view)) | ||
| FlutterChannel& on(const std::string& method, Handler h); | ||
|
|
||
| // Namespace — prefix.method routing | ||
| class Namespace { | ||
| public: | ||
| Namespace(const Namespace&) = delete; | ||
| Namespace& operator=(const Namespace&) = delete; | ||
| Namespace& on(const std::string& method, Handler h); | ||
| void invoke(const std::string& method, const std::string& args); | ||
| private: | ||
| friend class FlutterChannel; | ||
| Namespace(FlutterChannel* parent, std::string prefix); | ||
| FlutterChannel* m_parent; | ||
| std::string m_prefix; | ||
| }; | ||
| Namespace ns(const std::string& prefix); | ||
|
|
||
| // Middleware pipeline | ||
| class Middleware { | ||
| public: | ||
| virtual ~Middleware() = default; | ||
| virtual bool onInbound(const std::string& method, | ||
| const std::string& args, Reply reply); | ||
| virtual void onOutbound(const std::string& method, | ||
| const std::string& args); | ||
| }; | ||
| FlutterChannel& use(std::shared_ptr<Middleware> m); | ||
|
|
||
| // View binding (one-step, cannot call twice) | ||
| FlutterViewHost::MethodCallHandler handler(FlutterViewHost* view); | ||
|
|
||
| // C++ → Dart (fire-and-forget) | ||
| void invoke(const std::string& method, const std::string& args); | ||
|
|
||
| const std::string& name() const { return m_channelName; } | ||
|
|
||
| private: | ||
| std::string m_channelName; | ||
| std::unordered_map<std::string, Handler> m_routes; | ||
| std::vector<std::shared_ptr<Middleware>> m_middleware; | ||
| FlutterViewHost* m_view = nullptr; // main-thread only | ||
| bool m_handlerCalled = false; | ||
| bool m_dartReady = false; | ||
| std::deque<std::pair<std::string, std::string>> m_pendingMessages; | ||
| }; | ||
|
|
||
| // Middleware factory functions | ||
| std::shared_ptr<FlutterChannel::Middleware> makeLoggingMiddleware(); | ||
| std::shared_ptr<FlutterChannel::Middleware> makeThreadGuardMiddleware(); | ||
| std::shared_ptr<FlutterChannel::Middleware> makeTimeoutMiddleware(int seconds); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 建议用 makeTimeoutMiddleware (std::chrono::seconds seconds) |
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| #include "FlutterChannel.h" | ||
| #include "FlutterMiddleware.h" | ||
| #include <algorithm> | ||
| #include <wx/app.h> | ||
| #include <wx/timer.h> | ||
| #include <boost/log/trivial.hpp> | ||
|
|
||
| // ── LoggingMiddleware ───────────────────────────────────────── | ||
|
|
||
| class LoggingMiddleware : public FlutterChannel::Middleware { | ||
| uint64_t m_nextId = 0; | ||
| bool onInbound(const std::string& method, const std::string& args, | ||
| FlutterViewHost::Reply) override { | ||
| uint64_t id = ++m_nextId; | ||
| #ifdef ORCA_FLUTTER_VERBOSE | ||
| BOOST_LOG_TRIVIAL(trace) << "[flutter][" << id << "] " << method << " " << args; | ||
| #endif | ||
| return true; | ||
| } | ||
| void onOutbound(const std::string& method, | ||
| const std::string& args) override { | ||
| #ifdef ORCA_FLUTTER_VERBOSE | ||
| BOOST_LOG_TRIVIAL(trace) << "[flutter][" << m_nextId << "] out " << method << " " << args; | ||
| #endif | ||
| } | ||
| }; | ||
|
|
||
| // ── ThreadGuardMiddleware ───────────────────────────────────── | ||
|
|
||
| bool ThreadGuardMiddleware::onInbound(const std::string&, const std::string&, | ||
| FlutterViewHost::Reply) { | ||
| if (!wxThread::IsMain()) { | ||
| wxASSERT(wxThread::IsMain()); | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| // ── TimeoutMiddleware ───────────────────────────────────────── | ||
|
|
||
| class TimeoutMiddleware : public FlutterChannel::Middleware, | ||
| public wxEvtHandler { | ||
| int m_sec; | ||
| public: | ||
| explicit TimeoutMiddleware(int s) : m_sec(s) {} | ||
| ~TimeoutMiddleware() { | ||
| for (auto& t : m_timers) { | ||
| if (t->IsRunning()) t->Stop(); | ||
| } | ||
| } | ||
|
|
||
| bool onInbound(const std::string&, const std::string&, | ||
| FlutterViewHost::Reply reply) override { | ||
| // Clean up fired one-shot timers | ||
| m_timers.erase( | ||
| std::remove_if(m_timers.begin(), m_timers.end(), | ||
| [](const auto& t) { return !t->IsRunning(); }), | ||
| m_timers.end()); | ||
| auto fn = std::make_shared<FlutterViewHost::Reply>(std::move(reply)); | ||
| auto fired = std::make_shared<bool>(false); | ||
| auto t = std::make_unique<wxTimer>(this); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. handler 正常返回后主动取消对应的 timer 即可,这样写 m_timers 会无限增长 |
||
| int sec = m_sec; | ||
| t->Bind(wxEVT_TIMER, [fired, fn, sec](wxTimerEvent&) { | ||
| if (!*fired) { | ||
| *fired = true; | ||
| (*fn)("{\"code\":\"TIMEOUT\"," | ||
| "\"message\":\"Handler timed out after " + | ||
| std::to_string(sec) + "s\"}"); | ||
| } | ||
| }); | ||
| t->Start(sec * 1000, true); | ||
| m_timers.push_back(std::move(t)); | ||
| return true; | ||
| } | ||
|
|
||
| private: | ||
| std::vector<std::unique_ptr<wxTimer>> m_timers; | ||
| }; | ||
|
|
||
| // ── Factories ───────────────────────────────────────────────── | ||
|
|
||
| std::shared_ptr<FlutterChannel::Middleware> makeLoggingMiddleware() | ||
| { return std::make_shared<LoggingMiddleware>(); } | ||
|
|
||
| std::shared_ptr<FlutterChannel::Middleware> makeThreadGuardMiddleware() | ||
| { return std::make_shared<ThreadGuardMiddleware>(); } | ||
|
|
||
| std::shared_ptr<FlutterChannel::Middleware> makeTimeoutMiddleware(int s) | ||
| { return std::make_shared<TimeoutMiddleware>(s); } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
显式删除了拷贝构造/赋值,但没有声明移动构造/赋值,建议显式加上移动构造