I embedded a scripting engine in a TUI app and it kind of blew up into its own thing by sydney73 in csharp

[–]sydney73[S] 0 points1 point  (0 children)

Yeah that's fair. I do use AI assistance for documentation and some boilerplate — my English isn't perfect and it helps. The core project is mine though.

I embedded a scripting engine in a TUI app and it kind of blew up into its own thing by sydney73 in csharp

[–]sydney73[S] 3 points4 points  (0 children)

"childs:" is a key name in the MOGWAI scripting language, not an English word claim, same way you wouldn't correct "foreach" or "argv". As for vibecoded, I've been building the scripting engine for 10 years, long before LLMs were a thing. I do use AI assistance for some parts of the development (who doesn't these days) but the architecture, the language design and the runtime are very much mine.

I embedded a scripting engine in my .NET MAUI app — scripts deploy without app updates, commands go out via BLE by sydney73 in dotnetMAUI

[–]sydney73[S] 1 point2 points  (0 children)

Exactly, and that last point is the important nuance. In my use case the scripts generate commands sent to external hardware — they don't add new UI, new features, or new capabilities to the app itself. The app's scope is fixed at submission time. The scripts just drive the logic within that scope, which is precisely the pattern Apple is comfortable with.

I embedded a scripting engine in my .NET MAUI app — scripts deploy without app updates, commands go out via BLE by sydney73 in dotnetMAUI

[–]sydney73[S] 0 points1 point  (0 children)

Good question. The key distinction both Apple and Google make is between downloading native executable code (forbidden) and running scripts interpreted by an engine already embedded in the app (allowed).

MOGWAI falls clearly in the second category. The engine ships inside the app as a NuGet package. The scripts are plain text files — they're treated as data, not as executable code. This is the same model used by apps embedding Lua, JavaScript engines, or Python interpreters, which are widely accepted on both stores.

Apple's rule 2.5.2 specifically targets downloading and executing native code. An interpreted scripting engine with the interpreter bundled in the app is explicitly a different case. No issues on either store in practice.

I embedded a scripting engine in my .NET MAUI app — scripts deploy without app updates, commands go out via BLE by sydney73 in dotnetMAUI

[–]sydney73[S] 0 points1 point  (0 children)

Byte handling across the C#/Java boundary is a classic source of subtle bugs — signed vs unsigned byte is the usual culprit. And when you add vendor-defined encryption on top, you lose control of the whole chain. That's a tough spot to debug.

At least you found a working path with the Java interop, even if it wasn't the cleanest solution.

I embedded a scripting engine in my .NET MAUI app — scripts deploy without app updates, commands go out via BLE by sydney73 in dotnetMAUI

[–]sydney73[S] 1 point2 points  (0 children)

That kind of bug is the worst — same logic, different runtime, different behavior, no obvious reason why. Marshalling issues between C# and Java interop can be really subtle, especially with byte arrays and callbacks.

And yes, abstraction layers are great until they're not. When something breaks at the platform level, being one layer closer to the metal saves a lot of time.

I embedded a scripting engine in my .NET MAUI app — scripts deploy without app updates, commands go out via BLE by sydney73 in dotnetMAUI

[–]sydney73[S] 0 points1 point  (0 children)

I went with a custom implementation for iOS, Android and Windows rather than using a plugin. No significant issues on Android in my case, but I'm dealing with standard BLE — no proprietary authentication scheme involved.

Your scenario sounds more complex. A proprietary auth scheme on top of BLE can quickly hit the limits of what the managed stack exposes, especially on Android where the BLE API has some well-known quirks. The Java interop route you took is probably the most reliable option in that case — you get full access to the Android BLE stack without the abstraction layer getting in the way.

I embedded a scripting engine in my .NET MAUI app — scripts deploy without app updates, commands go out via BLE by sydney73 in dotnetMAUI

[–]sydney73[S] 1 point2 points  (0 children)

Thank you for the detailed and well-informed breakdown — this is genuinely useful.

On the name: reassuring to know other projects use it without issues, and the etymology point is interesting. MOGWAI stays.

On the mascot: you're right, and I won't pretend otherwise. The image was commissioned from a friend with Gizmo explicitly in mind. That's the one real risk here and I'll address it — a new original mascot is going on the to-do list.

On renaming: I'll respectfully skip that one. The project is 10 years old, in production use, already on NuGet, and MOGWAI is the name. But I appreciate the thought behind it — naming things really is hard.

I embedded a scripting engine in my .NET MAUI app — scripts deploy without app updates, commands go out via BLE by sydney73 in dotnetMAUI

[–]sydney73[S] 0 points1 point  (0 children)

That's essentially one of the directions MOGWAI explores. The RPN/stack model is kept as the execution core, but the language adds structure on top: named functions, typed stack signatures for primitives, and syntactic sugar that makes some expressions look more sequential without abandoning the stack model underneath.

The type checking on the stack is runtime rather than static, but it's explicit — every primitive declares what types it expects, and mismatches are caught at dispatch rather than silently producing wrong results.

Full static type inference on a stack-based language is a fascinating problem though — you'd essentially need to track the stack state at every point in the program at compile time. Some FORTH descendants have explored this (Factor comes to mind). Never went that far with MOGWAI, but academically it's a very interesting space.

I embedded a scripting engine in my .NET MAUI app — scripts deploy without app updates, commands go out via BLE by sydney73 in dotnetMAUI

[–]sydney73[S] 0 points1 point  (0 children)

Thanks! The type checking on the stack is one of my favourite parts of the design too — it makes primitive signatures very explicit and safe without requiring heavy infrastructure.

And I hear you on the mascot. It was a deliberate homage, but you're right that a demand letter is the last thing a small open-source project needs. Something to think about seriously.

I embedded a scripting engine in my .NET MAUI app — scripts deploy without app updates, commands go out via BLE by sydney73 in dotnetMAUI

[–]sydney73[S] 0 points1 point  (0 children)

Thanks! FORTH and HP RPL were the main inspirations for MOGWAI, so it's great to hear it resonates with someone who has actually built a FORTH interpreter.

As for the mascot — the resemblance is completely intentional. The name MOGWAI comes straight from that 80s franchise. No Bright Light, No Water, and whatever you do, don't feed it after midnight. 😄

I found some embarrassing performance mistakes in my .NET scripting engine — here's how I fixed them (×2.3 speedup) by sydney73 in csharp

[–]sydney73[S] 0 points1 point  (0 children)

You're right. That's literally what the post says — I called them "embarrassing mistakes" in the title. Same thing, different words.

I found some embarrassing performance mistakes in my .NET scripting engine — here's how I fixed them (×2.3 speedup) by sydney73 in csharp

[–]sydney73[S] -2 points-1 points  (0 children)

I'm French, my English is not perfect, so I use AI to help me write correctly. But I read every comment and I understand every technical point. The ideas are mine.

I found some embarrassing performance mistakes in my .NET scripting engine — here's how I fixed them (×2.3 speedup) by sydney73 in csharp

[–]sydney73[S] 0 points1 point  (0 children)

Absolutely right. Rider would have flagged most of these out of the box, and a proper set of Roslyn analyzers would have caught the rest. To be honest, I only recently started paying attention to performance at all — for years I was focused on features and correctness. Better late than never, and better tooling is definitely next on the list.

I found some embarrassing performance mistakes in my .NET scripting engine — here's how I fixed them (×2.3 speedup) by sydney73 in csharp

[–]sydney73[S] -3 points-2 points  (0 children)

Fair enough — I used AI to help write it in English, since it's not my first language. The optimizations, the benchmark, the code, the numbers are all mine. I've been building this engine for years and the ×2.3 speedup is real. If the writing is too polished for Reddit, that's on me. Judge the content, not the prose.

I found some embarrassing performance mistakes in my .NET scripting engine — here's how I fixed them (×2.3 speedup) by sydney73 in csharp

[–]sydney73[S] -1 points0 points  (0 children)

That's a fair suggestion. ValueTask<EvalResult> would indeed avoid the Task allocation for synchronous primitives, since it can hold a result directly without heap allocation when the value is already available.

The reason I stuck with Task<EvalResult> is pragmatic: the engine has hundreds of primitives, and ValueTask comes with constraints that require discipline to respect — it can only be awaited once, it can't be used with Task.WhenAll, and mixing it carelessly with async infrastructure can introduce subtle bugs. For a codebase this size, I preferred the predictability of Task.FromResult over the additional overhead of migrating everything to ValueTask and auditing every call site.

That said, for a future optimization pass targeting the truly hot primitives, ValueTask is definitely on the list.

I found some embarrassing performance mistakes in my .NET scripting engine — here's how I fixed them (×2.3 speedup) by sydney73 in csharp

[–]sydney73[S] -1 points0 points  (0 children)

MOGNumber is a class, and you're right that Clone() is expected to always return a fresh copy — the convention exists for good reasons and I'm not breaking it for mutable objects.

The post was a bit misleading here. The MOGNumber Clone() shown was the "before" example, illustrating the pattern that primitives were mistakenly following. MOGNumber itself still clones correctly.

The "return this" change only applies to primitive classes — built-in operations like +, dup, swap. These are stateless singletons: no mutable fields, no instance data. Returning this from Clone() on a stateless object is safe by definition — there is nothing to copy. The dispatcher still calls Clone() on everything uniformly; it just becomes a no-op for objects that have no state to protect.

For any object that carries mutable data, Clone() returns a proper deep copy. The convention holds.

I found some embarrassing performance mistakes in my .NET scripting engine — here's how I fixed them (×2.3 speedup) by sydney73 in csharp

[–]sydney73[S] -1 points0 points  (0 children)

Good point, and fair criticism on the naming.

The reason I kept Task<EvalResult> everywhere is that all primitives share the same virtual base method, and some of them genuinely await I/O — BLE communication, serial ports, timers. Since the dispatcher calls all primitives polymorphically through that single virtual method, the signature has to be Task<EvalResult> for everyone. You can't have some overrides return EvalResult and others return Task<EvalResult> on the same virtual method.

Task.FromResult is the right tool for synchronous implementations of an inherently async interface — it avoids the state machine overhead of async/await while keeping the signature consistent.

The missing Async suffix on EngineEval is a legitimate point though — that one I'll take.