FastLED 3.10.6
Loading...
Searching...
No Matches
runtime_rpc_binding.h
Go to the documentation of this file.
1#pragma once
2
3// =============================================================================
4// RuntimeRpcBinding -- non-template ErasedInvoker (FastLED #3246 / #3244 Tier 1A)
5// =============================================================================
6//
7// Replaces per-signature `TypedRpcBinding<Sig>::invokeWithReturn` (~2.1 KB
8// observed on LPC845 / AutoResearch) with a non-template invoke() body that
9// dispatches through a tiny per-Sig trampoline plus a per-type ops table.
10//
11// Per-Sig cost goes from ~2 KB (full JsonArgConverter + TypeToJson +
12// invokeImpl bodies emitted per signature) down to ~50-150 B (just the
13// per-Sig trampoline). The shared dispatch lives in
14// `runtime_rpc_binding.cpp.hpp`.
15//
16// Acceptance is in FastLED #3246: enabled on `!FL_PLATFORM_HAS_LARGE_MEMORY`
17// targets via the compile-time fork in `Rpc::registerMethod`. Large-memory
18// targets keep `TypedRpcBinding<Sig>` (faster -- no per-call heap, no
19// indirect dispatch).
20
21#include "fl/stl/json.h"
22#include "fl/stl/tuple.h"
23#include "fl/stl/function.h"
24#include "fl/stl/string.h"
25#include "fl/stl/vector.h"
26#include "fl/stl/shared_ptr.h"
27#include "fl/stl/type_traits.h"
28#include "fl/stl/new.h" // placement-new
29#include "fl/stl/move.h"
30#include "fl/stl/noexcept.h"
31#include "fl/stl/stdint.h"
33#include "fl/remote/rpc/json_to_type.h" // JsonToType<T>, ConstCharPtrWrapper, ConstSpanWrapper // IWYU pragma: keep
34#include "fl/remote/rpc/type_to_json.h" // detail::TypeToJson<T> // IWYU pragma: keep
35#include "fl/remote/rpc/json_arg_converter.h" // rpc_storage_type
36#include "fl/remote/rpc/rpc_invokers.h" // detail::ErasedInvoker
37
38namespace fl {
39namespace detail {
40
41// =============================================================================
42// RpcTypeOps -- per-type operations table
43// =============================================================================
44//
45// One `static const RpcTypeOps` is emitted per unique storage type used
46// across all RPC signatures (shared, not per-Sig). All cross-type behavior
47// in `RuntimeRpcBinding::invoke` indirects through this table.
48
49struct RpcTypeOps {
50 fl::size storage_size;
51 fl::size storage_align;
52 void (*default_construct)(void* p);
53 void (*destroy)(void* p);
54 TypeConversionResult (*decode)(const json& src, void* dst);
55 json (*encode)(const void* src);
56};
57
58// Helper that emits one `static const RpcTypeOps` per concrete storage type
59// `T`. Requires that `JsonToType<T>` and `TypeToJson<T>` specializations
60// exist for `T` (which is exactly the set of RPC-supported types today).
61template <typename T>
63 static void default_construct_(void* p) FL_NO_EXCEPT { new (p) T(); }
64 static void destroy_(void* p) FL_NO_EXCEPT { static_cast<T*>(p)->~T(); }
65
66 static TypeConversionResult decode_(const json& src, void* dst) FL_NO_EXCEPT {
68 *static_cast<T*>(dst) = fl::move(fl::get<0>(tup));
69 return fl::get<1>(tup);
70 }
71 static json encode_(const void* src) FL_NO_EXCEPT {
72 return TypeToJson<T>::convert(*static_cast<const T*>(src));
73 }
74
75 static const RpcTypeOps* ops() FL_NO_EXCEPT {
76 static const RpcTypeOps kOps = {
77 sizeof(T), alignof(T),
79 &destroy_,
80 &decode_,
81 &encode_,
82 };
83 return &kOps;
84 }
85};
86
87// Void return: no storage / encode / decode. Caller (RuntimeRpcBinding)
88// branches on a null `mResultType`.
89template <>
90struct RpcTypeOpsFor<void> {
91 static const RpcTypeOps* ops() FL_NO_EXCEPT { return nullptr; }
92};
93
94// =============================================================================
95// RpcTrampolineFn -- per-Sig trampoline signature
96// =============================================================================
97//
98// Generated by `makeRuntimeRpcBinding<R(Args...)>`. Body is ~50-100 B:
99// 1) Cast `user_fn` to `fl::function<R(Args...)>*`.
100// 2) Cast each `arg_slots[i]` to the matching storage type and forward.
101// 3) Place the return value into `result_slot` (if non-null).
102//
103// All decode/encode/lifetime work happens outside the trampoline, in
104// `RuntimeRpcBinding::invoke`, so there is no per-Sig copy of that code.
105
106using RpcTrampolineFn = void (*)(void* user_fn,
107 void* result_slot,
108 void* const* arg_slots);
109
110// Per-Sig user-fn destroyer (deletes the heap-allocated `fl::function<Sig>`).
111using RpcDestroyFn = void (*)(void* user_fn);
112
113namespace runtime_rpc {
114
115// =============================================================================
116// TrampolineHelper -- emits per-Sig trampoline + destroy bodies
117// =============================================================================
118
119template <typename R, typename... Args>
121 using FunctionType = fl::function<R(Args...)>;
122
123 template <fl::size... Is>
124 static R invoke_(FunctionType& fn, void* const* arg_slots, index_sequence<Is...>) FL_NO_EXCEPT {
125 // Each slot stores a `rpc_storage_type<Args>::type` value;
126 // implicit conversion (ConstCharPtrWrapper -> const char*,
127 // ConstSpanWrapper<T> -> span<const T>) yields the original Args.
128 return fn(*static_cast<typename rpc_storage_type<Args>::type*>(arg_slots[Is])...);
129 }
130
131 static void trampoline(void* user_fn, void* result_slot, void* const* arg_slots) FL_NO_EXCEPT {
132 FunctionType& fn = *static_cast<FunctionType*>(user_fn);
133 R value = invoke_(fn, arg_slots, make_index_sequence<sizeof...(Args)>{});
134 if (result_slot) {
135 *static_cast<R*>(result_slot) = fl::move(value);
136 }
137 }
138
139 static void destroy(void* user_fn) FL_NO_EXCEPT {
140 delete static_cast<FunctionType*>(user_fn); // ok bare allocation
141 }
142};
143
144// Void-return specialization.
145template <typename... Args>
146struct TrampolineHelper<void, Args...> {
147 using FunctionType = fl::function<void(Args...)>;
148
149 template <fl::size... Is>
150 static void invoke_(FunctionType& fn, void* const* arg_slots, index_sequence<Is...>) FL_NO_EXCEPT {
151 fn(*static_cast<typename rpc_storage_type<Args>::type*>(arg_slots[Is])...);
152 }
153
154 static void trampoline(void* user_fn, void* result_slot, void* const* arg_slots) FL_NO_EXCEPT {
155 (void)result_slot;
156 FunctionType& fn = *static_cast<FunctionType*>(user_fn);
157 invoke_(fn, arg_slots, make_index_sequence<sizeof...(Args)>{});
158 }
159
160 static void destroy(void* user_fn) FL_NO_EXCEPT {
161 delete static_cast<FunctionType*>(user_fn); // ok bare allocation
162 }
163};
164
165// Per-Sig static arg-type table. Owning storage for the
166// `const RpcTypeOps* const*` pointer handed to RuntimeRpcBinding. Lives in
167// `.rodata` (one entry per Sig, ~8 B per arg). For zero-arg signatures the
168// `mArgTypes` pointer is unused so the array is sized to 1 with a nullptr
169// entry to keep ISO C++ happy (no zero-sized array).
170template <typename... Args>
172 static const RpcTypeOps* const* table() FL_NO_EXCEPT {
173 static const RpcTypeOps* const kTable[] = {
175 };
176 return kTable;
177 }
178};
179
180template <>
182 static const RpcTypeOps* const* table() FL_NO_EXCEPT {
183 // 0-arg signatures pass `arg_count == 0`; the table is never
184 // dereferenced, but we still need a valid (non-null) pointer.
185 static const RpcTypeOps* const kTable[1] = { nullptr };
186 return kTable;
187 }
188};
189
190} // namespace runtime_rpc
191
192// =============================================================================
193// RuntimeRpcBinding -- non-template ErasedInvoker
194// =============================================================================
195//
196// All decode/encode/alloc/destroy/dispatch logic lives in one non-template
197// `invoke` body (in `runtime_rpc_binding.cpp.hpp`). Per-Sig instantiation
198// tax is bounded to:
199// * The per-Sig trampoline (50-150 B)
200// * The per-Sig destroy fn (~10 B)
201// * The per-Sig arg_types table (8 B * argc, in .rodata)
202// * The per-T RpcTypeOps entries (~32 B per unique type, *shared* across Sigs)
203
204class RuntimeRpcBinding final : public ErasedInvoker {
205public:
206 RuntimeRpcBinding(void* user_fn,
207 RpcTrampolineFn trampoline,
208 RpcDestroyFn destroy_user_fn,
209 const RpcTypeOps* const* arg_types,
210 fl::size arg_count,
211 const RpcTypeOps* result_type) FL_NO_EXCEPT
212 : mUserFn(user_fn),
213 mTrampoline(trampoline),
214 mDestroyUserFn(destroy_user_fn),
215 mArgTypes(arg_types),
216 mArgCount(arg_count),
217 mResultType(result_type) {}
218
222
225
227
228private:
229 void* mUserFn;
232 const RpcTypeOps* const* mArgTypes;
233 fl::size mArgCount;
234 const RpcTypeOps* mResultType; // nullptr for void
235};
236
237// =============================================================================
238// makeRuntimeRpcBinding<Sig> -- per-Sig construction helper
239// =============================================================================
240//
241// One emission per unique signature, but the work is *light*: just a heap
242// allocation for the captured fl::function and pointer-stamping into the
243// new RuntimeRpcBinding. The expensive per-Sig template tax (~2 KB of
244// invokeWithReturn body) is gone.
245
246template <typename R, typename... Args>
248 using Helper = runtime_rpc::TrampolineHelper<R, Args...>;
249 using FunctionType = fl::function<R(Args...)>;
250 // user_fn is owned by the RuntimeRpcBinding via mDestroyUserFn (see Helper::destroy).
251 FunctionType* user_fn = new FunctionType(fl::move(fn)); // ok bare allocation
253 static_cast<void*>(user_fn),
254 &Helper::trampoline,
255 &Helper::destroy,
257 sizeof...(Args),
259 );
260}
261
262} // namespace detail
263} // namespace fl
const RpcTypeOps *const * mArgTypes
RuntimeRpcBinding(void *user_fn, RpcTrampolineFn trampoline, RpcDestroyFn destroy_user_fn, const RpcTypeOps *const *arg_types, fl::size arg_count, const RpcTypeOps *result_type) FL_NO_EXCEPT
fl::tuple< TypeConversionResult, json > invoke(const json &jsonArgs) FL_NO_EXCEPT override
~RuntimeRpcBinding() FL_NO_EXCEPT override
RuntimeRpcBinding & operator=(const RuntimeRpcBinding &) FL_NO_EXCEPT=delete
RuntimeRpcBinding(const RuntimeRpcBinding &) FL_NO_EXCEPT=delete
FastLED's Elegant JSON Library: fl::json
fl::shared_ptr< RuntimeRpcBinding > makeRuntimeRpcBinding(fl::function< R(Args...)> fn) FL_NO_EXCEPT
void(*)(void *user_fn, void *result_slot, void *const *arg_slots) RpcTrampolineFn
void(*)(void *user_fn) RpcDestroyFn
Compile-time linker keep-alive hook for a single fl::Bus.
Definition bus_info.h:57
TypeConversionResult(* decode)(const json &src, void *dst)
void(* default_construct)(void *p)
json(* encode)(const void *src)
constexpr remove_reference< T >::type && move(T &&t) FL_NO_EXCEPT
Definition move.h:28
constexpr int type_rank< T >::value
shared_ptr< T > make_shared(Args &&... args) FL_NO_EXCEPT
Definition shared_ptr.h:414
InputGamut g FL_NO_EXCEPT
Definition rgbw.h:121
pair_element< I, T1, T2 >::type & get(pair< T1, T2 > &p) FL_NO_EXCEPT
Definition pair.h:115
typename make_index_sequence_impl< N >::type make_index_sequence
Base definition for an LED controller.
Definition crgb.hpp:179
typename fl::remove_cv< typename fl::remove_reference< T >::type >::type type
static fl::tuple< T, TypeConversionResult > convert(const json &j)
static const RpcTypeOps * ops() FL_NO_EXCEPT
static void destroy_(void *p) FL_NO_EXCEPT
static TypeConversionResult decode_(const json &src, void *dst) FL_NO_EXCEPT
static json encode_(const void *src) FL_NO_EXCEPT
static const RpcTypeOps * ops() FL_NO_EXCEPT
static void default_construct_(void *p) FL_NO_EXCEPT
static json convert(const T &value)
static const RpcTypeOps *const * table() FL_NO_EXCEPT
static const RpcTypeOps *const * table() FL_NO_EXCEPT
static void invoke_(FunctionType &fn, void *const *arg_slots, index_sequence< Is... >) FL_NO_EXCEPT
static void trampoline(void *user_fn, void *result_slot, void *const *arg_slots) FL_NO_EXCEPT
static R invoke_(FunctionType &fn, void *const *arg_slots, index_sequence< Is... >) FL_NO_EXCEPT
static void trampoline(void *user_fn, void *result_slot, void *const *arg_slots) FL_NO_EXCEPT
static void destroy(void *user_fn) FL_NO_EXCEPT