1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
// Custom MarkupExtension registration FFI.
//
// Same architectural pattern as noesis_classes.cpp: a per-base C++
// trampoline subclass + synthetic per-name TypeClassBuilder + Factory
// creator + Symbol → ClassData side table. The trampoline's virtual
// override is `ProvideValue` (rather than `OnPropertyChanged`), and it
// dispatches to the Rust callback with the current `Key` value the XAML
// parser set via the ContentProperty mechanism.
//
// Takes a single positional `Key` string argument. Returns either a
// borrowed C string (most common, wrapped into a BoxedValue<String>) or
// a borrowed BaseComponent* (for value types that can't be expressed as
// text, e.g. an existing resource lookup).
#include "noesis_shim.h"
#include <NsCore/Boxing.h>
#include <NsCore/Factory.h>
#include <NsCore/Noesis.h>
#include <NsCore/Ptr.h>
#include <NsCore/Reflection.h>
#include <NsCore/ReflectionImplement.h>
#include <NsCore/String.h>
#include <NsCore/Symbol.h>
#include <NsCore/TypeClassBuilder.h>
#include <NsCore/TypeClassCreator.h>
#include <NsCore/TypeOf.h>
#include <NsGui/ContentPropertyMetaData.h>
#include <NsGui/MarkupExtension.h>
#include <NsGui/ValueTargetProvider.h>
#include <atomic>
#include <mutex>
#include <unordered_map>
#include <vector>
namespace {
// ── ClassData + registry ───────────────────────────────────────────────────
// Same intrusive-refcount model as ClassData in noesis_classes.cpp; see
// the comment there for the full lifetime contract. Short version: each
// live `RustMarkupExtension` instance bumps the count; the Rust caller's
// `MarkupExtensionRegistration` holds the +1 created at register time;
// final free runs the `free_handler` Rust trampoline.
struct MarkupClassData {
Noesis::String name;
Noesis::Symbol sym;
Noesis::TypeClassBuilder* typeClass; // owned by Reflection registry
noesis_markup_provide_fn cb;
void* userdata;
noesis_markup_free_fn free_handler;
std::atomic<int> ref_count;
MarkupClassData(): ref_count(1) {}
void AddRef() noexcept {
ref_count.fetch_add(1, std::memory_order_relaxed);
}
void Release() {
if (ref_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
std::atomic_thread_fence(std::memory_order_acquire);
// See ClassData::Release in noesis_classes.cpp. Same
// rationale: never delete typeClass or `this` from inside
// the destructor chain. Just free the Rust handler box;
// the rest leaks until process shutdown sweeps it.
void* ud = userdata;
userdata = nullptr;
if (free_handler && ud) {
free_handler(ud);
}
}
}
};
std::mutex g_markup_registry_mutex;
std::unordered_map<uint32_t, MarkupClassData*> g_markup_registry;
// Same shape as g_all_class_data in noesis_classes.cpp; see the comment
// there. Holds every successfully-registered MarkupClassData; the
// shutdown sweep iterates the whole list and frees any handler box
// whose `userdata` is still set (entries with userdata=null are
// no-ops, the common case after normal teardown).
std::mutex g_all_markup_data_mutex;
std::vector<MarkupClassData*> g_all_markup_data;
void track_markup_data(MarkupClassData* cd) {
std::lock_guard<std::mutex> lock(g_all_markup_data_mutex);
g_all_markup_data.push_back(cd);
}
MarkupClassData* markup_registry_find(Noesis::Symbol sym) {
std::lock_guard<std::mutex> lock(g_markup_registry_mutex);
auto it = g_markup_registry.find((uint32_t)sym);
return it == g_markup_registry.end() ? nullptr : it->second;
}
bool markup_registry_insert(Noesis::Symbol sym, MarkupClassData* cd) {
std::lock_guard<std::mutex> lock(g_markup_registry_mutex);
return g_markup_registry.emplace((uint32_t)sym, cd).second;
}
void markup_registry_erase(Noesis::Symbol sym) {
std::lock_guard<std::mutex> lock(g_markup_registry_mutex);
g_markup_registry.erase((uint32_t)sym);
}
// ── Trampoline subclass: MarkupExtension ───────────────────────────────────
//
// Same hand-rolled-reflection pattern as noesis_classes.cpp's
// RustContentControl: NS_DECLARE_REFLECTION's macros generate a
// GetClassType() that always returns the static type, but we need our
// override to report the synthetic per-name class so XAML's parser
// finds the right factory creator.
class RustMarkupExtension: public Noesis::MarkupExtension {
public:
Noesis::String Key; // ContentProperty, populated by XAML parser
RustMarkupExtension() = default;
~RustMarkupExtension() {
if (mClassData) {
mClassData->Release();
mClassData = nullptr;
}
}
void BindClassData(MarkupClassData* cd) {
if (mClassData) mClassData->Release();
mClassData = cd;
if (cd) cd->AddRef();
}
MarkupClassData* GetClassData() const { return mClassData; }
Noesis::Ptr<Noesis::BaseComponent>
ProvideValue(const Noesis::ValueTargetProvider* /*provider*/) override;
// Hand-rolled reflection. See noesis_classes.cpp::RustContentControl
// for the rationale.
static const Noesis::TypeClass*
StaticGetClassType(Noesis::TypeTag<RustMarkupExtension>*);
const Noesis::TypeClass* GetClassType() const override;
private:
MarkupClassData* mClassData = nullptr;
typedef RustMarkupExtension SelfClass;
typedef Noesis::MarkupExtension ParentClass;
friend class Noesis::TypeClassCreator;
static void StaticFillClassType(Noesis::TypeClassCreator& helper) {
// Register `Key` as a reflection property so XAML's parser can
// populate it from `{my:Localize SOME_KEY}`. Marking it as the
// ContentProperty makes the positional argument syntax work
// without callers having to write `Key=...` explicitly.
helper.Prop("Key", &RustMarkupExtension::Key);
helper.Meta<Noesis::ContentPropertyMetaData>("Key");
}
};
const Noesis::TypeClass*
RustMarkupExtension::StaticGetClassType(Noesis::TypeTag<RustMarkupExtension>*) {
static const Noesis::TypeClass* type;
if (NS_UNLIKELY(type == 0)) {
type = static_cast<const Noesis::TypeClass*>(Noesis::Reflection::RegisterType(
"DmNoesis.RustMarkupExtension",
Noesis::TypeClassCreator::Create<RustMarkupExtension>,
Noesis::TypeClassCreator::Fill<RustMarkupExtension, Noesis::MarkupExtension>));
}
return type;
}
const Noesis::TypeClass* RustMarkupExtension::GetClassType() const {
if (mClassData && mClassData->typeClass) {
return static_cast<const Noesis::TypeClass*>(mClassData->typeClass);
}
return StaticGetClassType((Noesis::TypeTag<RustMarkupExtension>*)nullptr);
}
Noesis::Ptr<Noesis::BaseComponent>
RustMarkupExtension::ProvideValue(const Noesis::ValueTargetProvider* /*provider*/) {
if (!mClassData || !mClassData->cb) {
return nullptr;
}
const char* out_string = nullptr;
void* out_component = nullptr;
bool produced = mClassData->cb(
mClassData->userdata, Key.Str(), &out_string, &out_component);
if (!produced) {
// Returning a null Ptr signals UnsetValue to Noesis's parser.
return nullptr;
}
if (out_string) {
// Box the C string into a BoxedValue<String>. Boxing copies the
// bytes; the caller's pointer can go away after this call.
return Noesis::Boxing::Box(out_string);
}
if (out_component) {
// Borrowed BaseComponent*; increment the ref count for the
// returned Ptr (Noesis::Ptr's adopt-from-raw form would consume
// the caller's ref, which contract-wise we don't have).
auto* obj = static_cast<Noesis::BaseComponent*>(out_component);
return Noesis::Ptr<Noesis::BaseComponent>(obj);
}
return nullptr;
}
// ── Factory creator ────────────────────────────────────────────────────────
Noesis::BaseComponent* markup_creator(Noesis::Symbol name) {
MarkupClassData* cd = markup_registry_find(name);
if (!cd) return nullptr;
auto* ext = new RustMarkupExtension();
ext->BindClassData(cd);
return ext;
}
} // namespace
// ── C ABI surface ──────────────────────────────────────────────────────────
extern "C" void* noesis_markup_extension_register(
const char* name,
noesis_markup_provide_fn cb,
void* userdata,
noesis_markup_free_fn free_handler) {
if (!name || !cb) return nullptr;
Noesis::Symbol sym = Noesis::Symbol(name);
if (Noesis::Reflection::IsTypeRegistered(sym)) {
return nullptr;
}
auto* cd = new MarkupClassData();
cd->name = name;
cd->sym = sym;
cd->cb = cb;
cd->userdata = userdata;
cd->free_handler = free_handler;
cd->typeClass = new Noesis::TypeClassBuilder(sym, /*isInterface*/ false);
cd->typeClass->AddBase(Noesis::TypeOf<RustMarkupExtension>());
Noesis::Reflection::RegisterType(cd->typeClass);
Noesis::Factory::RegisterComponent(sym, Noesis::Symbol(""), markup_creator);
if (!markup_registry_insert(sym, cd)) {
// No instances exist, no destructor chain in play, `cd` not yet in
// the shutdown sweep list, so tear down everything we built here and
// free MarkupClassData itself. We do NOT free the handler box: a null
// return leaves ownership of `userdata` with the Rust caller, which
// frees it. See MarkupExtensionRegistration::new.
Noesis::Factory::UnregisterComponent(sym);
Noesis::Reflection::Unregister(cd->typeClass);
delete cd;
return nullptr;
}
track_markup_data(cd);
return cd;
}
extern "C" void noesis_markup_extension_unregister(void* token) {
if (!token) return;
auto* cd = static_cast<MarkupClassData*>(token);
// Stop new instances; existing live instances retain their own refs.
// Reflection::Unregister is deliberately NOT called. Noesis::Shutdown
// tears down the registry on its own and walking it manually mid-
// process trips on instance destructor chains. See noesis_classes.cpp.
Noesis::Factory::UnregisterComponent(cd->sym);
markup_registry_erase(cd->sym);
// Drop the Rust caller's ref. The Rust handler box is freed here if
// no extension instances are alive, or deferred to the last instance
// dying (RustMarkupExtension's destructor).
cd->Release();
}
// Process-shutdown sweep. See noesis_classes.cpp's
// `noesis_classes_force_free_at_shutdown` for the rationale.
extern "C" void noesis_markup_extensions_force_free_at_shutdown(void) {
std::vector<MarkupClassData*> all;
{
std::lock_guard<std::mutex> lock(g_all_markup_data_mutex);
all = std::move(g_all_markup_data);
}
for (MarkupClassData* cd : all) {
void* ud = cd->userdata;
cd->userdata = nullptr;
if (cd->free_handler && ud) {
cd->free_handler(ud);
}
}
}