CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
Plugin.cpp
Go to the documentation of this file.
3#include "cdc_core/Raii.h"
4#include "cdc_ui/I18n.h"
5#include "cJSON.h"
6
7extern "C" {
8#include "wasm_export.h"
9void plg_log_info (const char* msg);
10void plg_log_warn (const char* msg);
11void plg_log_error(const char* msg);
12void plg_set_active_plugin(void* plugin);
13void* plg_get_active_plugin(void);
14}
15
16#include <algorithm>
17#include <cstdio>
18#include <cstring>
19
20namespace cdc::plugin_manager {
21
22void WasmModuleDeleter::operator()(WASMModuleCommon* m) const noexcept
23{
24 if (m) wasm_runtime_unload(m);
25}
26
27void WasmInstanceDeleter::operator()(WASMModuleInstanceCommon* m) const noexcept
28{
29 if (m) wasm_runtime_deinstantiate(m);
30}
31
32void WasmExecEnvDeleter::operator()(WASMExecEnv* e) const noexcept
33{
34 if (e) wasm_runtime_destroy_exec_env(e);
35}
36
37Plugin::Plugin() noexcept = default;
38Plugin::~Plugin() = default;
39
40namespace {
41
42// Per-call WASM instruction budget. Caps a runaway or hostile guest loop so the
43// call traps with an exception and returns instead of hanging the plugin tick
44// task forever. Generous enough that legitimate per-tick work (e.g. a few Olm
45// session establishes in one tick) never reaches it, while a true infinite loop
46// still traps within a few seconds of tick-task time.
47constexpr int kInstrLimit = 500'000'000;
48
49// WASM bytecode can be hundreds of KB. Keep it in PSRAM so it does not chew
50// into internal SRAM, which is the project-wide bottleneck (see CLAUDE.md).
51[[nodiscard]] bool load_bytecode_psram(const std::string& path,
53 std::size_t& out_len)
54{
55 out.reset();
56 out_len = 0;
57
58 auto fp = ::cdc::core::openFile(path.c_str(), "rb");
59 if (!fp) return false;
60 std::fseek(fp.get(), 0, SEEK_END);
61 long n = std::ftell(fp.get());
62 if (n <= 0) return false;
63 std::fseek(fp.get(), 0, SEEK_SET);
64
65 auto buf = psramAlloc<uint8_t>(static_cast<std::size_t>(n));
66 if (!buf) return false;
67
68 if (std::fread(buf.get(), 1, n, fp.get()) != static_cast<std::size_t>(n)) {
69 return false;
70 }
71 out = std::move(buf);
72 out_len = static_cast<std::size_t>(n);
73 return true;
74}
75
76} // namespace
77
78bool Plugin::load(const std::string& id, const PluginManifest& manifest)
79{
80 id_ = id;
81 manifest_ = manifest;
82
83 if (!load_bytecode_psram(PluginStorage::binaryPath(id), bytecode_, bytecode_len_)) {
84 plg_log_error("plugin: failed to read plugin binary into PSRAM");
85 return false;
86 }
87
88 char err_buf[128] = {0};
89 module_.reset(wasm_runtime_load(bytecode_.get(),
90 static_cast<uint32_t>(bytecode_len_),
91 err_buf, sizeof(err_buf)));
92 if (!module_) {
93 plg_log_error("plugin: wasm_runtime_load failed");
94 plg_log_error(err_buf);
95 unload();
96 return false;
97 }
98
99 (void)manifest_.linear_memory_kb;
100 const uint32_t stack_bytes = 64 * 1024;
101 // heap_bytes=0 is mandatory for Rust plugins. Any non-zero value makes
102 // WAMR re-purpose __heap_base for its own GC allocator, which collides
103 // with Rust's allocator (Rust grows linear memory directly via
104 // memory.grow). Plugins that need more memory must set --initial-memory
105 // / --max-memory via their own build.rs.
106 const uint32_t heap_bytes = 0;
107
108 module_inst_.reset(wasm_runtime_instantiate(module_.get(), stack_bytes,
109 heap_bytes, err_buf,
110 sizeof(err_buf)));
111 if (!module_inst_) {
112 plg_log_error("plugin: wasm_runtime_instantiate failed");
113 plg_log_error(err_buf);
114 unload();
115 return false;
116 }
117
118 exec_env_.reset(wasm_runtime_create_exec_env(module_inst_.get(), stack_bytes));
119 if (!exec_env_) {
120 plg_log_error("plugin: wasm_runtime_create_exec_env failed");
121 unload();
122 return false;
123 }
124
125 return true;
126}
127
128bool Plugin::hasExport(const char* name) const
129{
130 if (!isLoaded() || !name) return false;
131 return wasm_runtime_lookup_function(module_inst_.get(), name) != nullptr;
132}
133
134void Plugin::unload() noexcept
135{
136 // RAII order: exec_env -> module_inst -> module -> bytecode.
137 exec_env_.reset();
138 module_inst_.reset();
139 module_.reset();
140 bytecode_.reset();
141 bytecode_len_ = 0;
142 acquired_prereqs.clear();
143}
144
145bool Plugin::callI(const char* name,
146 std::initializer_list<int32_t> args,
147 int32_t* out_i32)
148{
149 if (!isLoaded()) return false;
150
151 // WAMR uses argv as both input slots and the return-value slot, so it
152 // must hold at least max(argc, 1) uint32_t.
153 constexpr std::size_t kMaxArgs = 8;
154 if (args.size() > kMaxArgs) {
155 plg_log_error("plugin: callI argument overflow");
156 return false;
157 }
158 uint32_t argv[kMaxArgs + 1] = {0};
159 std::size_t i = 0;
160 for (int32_t a : args) argv[i++] = static_cast<uint32_t>(a);
161 const uint32_t argc = static_cast<uint32_t>(args.size());
162
163 void* prev_active = plg_get_active_plugin();
165 wasm_runtime_set_instruction_count_limit(exec_env_.get(), kInstrLimit);
166 bool ok = true;
167 last_call_trapped_ = false;
168 wasm_function_inst_t fn = wasm_runtime_lookup_function(module_inst_.get(), name);
169 if (!fn) {
170 if (missingExports_.insert(name).second) {
171 char buf[96];
172 std::snprintf(buf, sizeof(buf), "plugin: missing export '%s'", name);
173 plg_log_warn(buf);
174 }
175 ok = false;
176 } else if (!wasm_runtime_call_wasm(exec_env_.get(), fn, argc, argv)) {
177 const char* exc = wasm_runtime_get_exception(module_inst_.get());
178 std::snprintf(last_trap_, sizeof(last_trap_), "%s", exc ? exc : "unknown trap");
179 last_call_trapped_ = true;
180 char buf[256];
181 std::snprintf(buf, sizeof(buf), "plugin: call '%s' failed: %s", name, last_trap_);
182 plg_log_error(buf);
183 wasm_runtime_clear_exception(module_inst_.get());
184 ok = false;
185 } else if (out_i32) {
186 *out_i32 = static_cast<int32_t>(argv[0]);
187 }
188 plg_set_active_plugin(prev_active);
189 return ok;
190}
191
192bool Plugin::loadLangOverlay(const char* path)
193{
194 langOverlay_.clear();
195 langOverlayLang_.clear();
196
197 std::string default_path;
198 if (!path || !*path) {
199 default_path = PluginStorage::langPath(id_);
200 path = default_path.c_str();
201 }
202
203 auto fp = ::cdc::core::openFile(path, "rb");
204 if (!fp) return false;
205
206 std::fseek(fp.get(), 0, SEEK_END);
207 long size = std::ftell(fp.get());
208 std::fseek(fp.get(), 0, SEEK_SET);
209 if (size <= 0 || size > 256 * 1024) {
210 plg_log_warn("plugin: lang file size invalid");
211 return false;
212 }
213
214 auto buf = ::cdc::core::psramAlloc<char>(static_cast<std::size_t>(size) + 1);
215 if (!buf) return false;
216 if (std::fread(buf.get(), 1, size, fp.get()) != static_cast<size_t>(size)) {
217 return false;
218 }
219 buf.get()[size] = '\0';
220
221 cJSON* root = cJSON_Parse(buf.get());
222 if (!root) {
223 plg_log_warn("plugin: lang file JSON parse failed");
224 return false;
225 }
226
227 cJSON* translations = cJSON_GetObjectItemCaseSensitive(root, "translations");
228 if (!translations || !cJSON_IsObject(translations)) {
229 cJSON_Delete(root);
230 return false;
231 }
232
233 const std::string& active = ::cdc::ui::I18n::instance().getLanguageCode();
234 if (active == "en") {
235 cJSON_Delete(root);
236 return true;
237 }
238
239 cJSON* lang_obj = cJSON_GetObjectItemCaseSensitive(translations, active.c_str());
240 if (!lang_obj || !cJSON_IsObject(lang_obj)) {
241 cJSON_Delete(root);
242 return true;
243 }
244
245 cJSON* entry = nullptr;
246 cJSON_ArrayForEach(entry, lang_obj) {
247 if (!cJSON_IsString(entry) || !entry->string || !entry->valuestring) continue;
248 langOverlay_.push_back({entry->string, entry->valuestring});
249 }
250 std::sort(langOverlay_.begin(), langOverlay_.end(),
251 [](const OverlayEntry& a, const OverlayEntry& b) {
252 return a.key < b.key;
253 });
254 langOverlayLang_ = active;
255
256 cJSON_Delete(root);
257 return true;
258}
259
260const char* Plugin::trKey(const char* key) const noexcept
261{
262 if (!key || langOverlay_.empty()) return nullptr;
263 auto it = std::lower_bound(
264 langOverlay_.begin(), langOverlay_.end(), key,
265 [](const OverlayEntry& e, const char* k) { return e.key < k; });
266 if (it != langOverlay_.end() && it->key == key) return it->value.c_str();
267 return nullptr;
268}
269
270} // namespace cdc::plugin_manager
Internationalization with English fallbacks in code and overlay translations loaded at runtime from a...
Mounts the FAT-FS partition that holds plugin .wasm + .meta files.
void plg_log_warn(const char *msg)
void * plg_get_active_plugin(void)
void plg_set_active_plugin(void *plugin)
void plg_log_error(const char *msg)
void plg_log_info(const char *msg)
Owned WAMR module instance + per-plugin state.
char name[cdc::hal::ISecureElement::RMEM_NAME_LEN]
Shared RAII wrappers for firmware resources.
static std::string langPath(const std::string &id)
Returns the full VFS path of <id>.lang (translation overlay).
static std::string binaryPath(const std::string &id)
Returns the path that should be loaded for <id>: <id>.aot if it exists on disk, otherwise <id>....
bool load(const std::string &id, const PluginManifest &manifest)
Load bytecode + instantiate the module. Does NOT run plugin_init yet - the manager handles ordering o...
Definition Plugin.cpp:78
std::vector< std::string > acquired_prereqs
Definition Plugin.h:118
bool hasExport(const char *name) const
Definition Plugin.cpp:128
const char * trKey(const char *key) const noexcept
Look up a plugin-local translation key in the loaded overlay.
Definition Plugin.cpp:260
bool isLoaded() const noexcept
Definition Plugin.h:90
bool callI(const char *name, std::initializer_list< int32_t > args={}, int32_t *out_i32=nullptr)
Call an exported i32(i32...)->i32 function by name.
Definition Plugin.cpp:145
void unload() noexcept
Destroy WAMR instance + free bytecode buffer. Idempotent.
Definition Plugin.cpp:134
bool loadLangOverlay(const char *path=nullptr)
Load the plugin's translation overlay from disk into PSRAM.
Definition Plugin.cpp:192
const std::string & id() const noexcept
Definition Plugin.h:89
const PluginManifest & manifest() const noexcept
Definition Plugin.h:88
const std::string & getLanguageCode() const
Current language code (lower-case ISO-639-1, e.g. "en", "de").
Definition I18n.h:130
static I18n & instance()
Singleton accessor.
Definition I18n.cpp:287
void * plg_get_active_plugin(void)
void plg_log_warn(const char *msg)
FilePtr openFile(const char *path, const char *mode) noexcept
Open a FILE* and wrap it in a FilePtr.
Definition Raii.h:87
PsramUniquePtr< T > psramAlloc(std::size_t count) noexcept
Allocate count elements of T in PSRAM (8-bit capable region).
Definition Raii.h:51
PsramUniquePtr< T > psramAlloc(std::size_t count) noexcept
Definition Raii.h:27
::cdc::core::PsramUniquePtr< T > PsramUniquePtr
Definition Raii.h:21
void operator()(WASMExecEnv *e) const noexcept
Definition Plugin.cpp:32
void operator()(WASMModuleInstanceCommon *m) const noexcept
Definition Plugin.cpp:27
void operator()(WASMModuleCommon *m) const noexcept
Definition Plugin.cpp:22