Hello,
I am going to start working on a user interface framework, and my current plan is to have it based on a tree of objects. Objects can have any number of properties of any type, or at least a wide range of types, and in addition to storing values, properties can contain constraints which are functions that take values from other properties, either properties of the current object, child objects, or the parent object, and returns a calculated value. Every time a referenced field changes, the constraint function should be called to update the value. Also inheritance should be supported (prototype based), so that you can create an object using another object as its prototype or more than one, and the new object will have all the properties of its prototypes and their values, but unless the properties of the new object are explicitly set, when the prototype properties change the new object will also change. I need new properties to be able to be added at runtime, because I want to implement a scripting language (probably Scheme) where you can customize everything and develop new applications by creating objects and properties and linking them, and it would be nice to support loading plugins in other languages that can create and manipulate objects in the same way, and perhaps eventually allow this to be done from another process over IPC so that all UI applications do not have to be within the same process. However, I want to use Rust because it has very high performance, there are Rust libraries I want to use for things like audio, GPU, etc, and I was thinking Rust's type system and memory system would give me more guarantees than implementing everything in a dynamic language. But what data structures should I use for this kind of object system? I could create a bunch of HashMaps with strings as keys, but that sounds like it would have bad performance especially compared to Rust trait objects. Also probably for constraints and inheritance to work, I need everything wrapped in something like Arc so that everything can be referenced and modified from multiple places. Would Rust even be the best fit for this if I lose the compile-time guarantees the type system provides by having objects be dynamic and their properties not being guaranteed at compile time, and if I end up using Arcs for everything moving reference counting from the compiler to the runtime level?
Your scope grows with every sentence: UI framework, reactive constraints, prototype inheritance, embedded Scheme, foreign language plugins, cross-process IPC.
This is a lot of stuff...
It might help to first figure out what matters more to you right now: Learning Rust, or building a UI framework?
Whatever you decide: Start small. One widget, one constraint binding a text field to a label. No inheritance, no scripting. Get that working, see how it feels, then add the next thing.
In my experience, ambitious solo projects tend to stall. The ones that work out usually started with something tiny and grew from there.
Thanks for your response, that makes sense. The reason I mentioned everything like scripting support, plugin support, etc is because I want to keep that in mind when choosing data structures and designing the object system. I do not want to end up designing something that seems to work at first, and then run into a wall because what I designed doesn't support something important that I plan to add and I have to start over, or realize that I designed something with very poor performance and have to redo everything. But that makes sense to start small and get something working and add to it over time so I'm not doing too much at once and so I actually get something working and have small successes at the start. By the way I am not quite a complete beginner, since I have done some small to medium sized Rust projects, including brlapi-rs, hid-rs, monarch-brlapi-server, btspeak-key-interceptor, and whisp-rs, so while I definitely expect to improve and learn more about Rust while doing this, learning the language will not be a main focus. I was mostly wondering if there's a better data structure than a bunch of HashMaps for a dynamic object system like this that I should know about before I start, or whether there is a crate I should use to implement this that I don't know about.
I tried to achieve a similar goal but gave up, ended by relatively simple a single thread model. So, when you achieve your goal, I really interested to see results. Currently I use Rust for backend only.
Am I understanding correctly that you're trying to re-implement Javascript and the DOM? All of the things you listed appear to be JS/DOM features (down to the idiosyncratic use of the term "prototype"). If there's something you want out of a UI library that isn't already provided by the web then that might be a good thing to emphasize in your requirements here.
Other dynamic object systems you may want to look into are Objective C, COM, .NET, the JVM, and most game engines. Objective C in particular is the first non-javascript thing that came to mind for me when I read your requirements.
For implementing javascript's object system specifically, no there is not. Web browsers use JIT compilation to convert as many of your JS objects into structs as possible and fall back onto hash maps for the rest. I assume they also use string interning and represent small objects as arrays of key-value pairs to save memory but that's just speculation on my part.
I honestly wasn't thinking of Javascript at all but that's a good point. The object system I'm thinking of is very similar to Javascript, although Javascript doesn't have a constraint system and uses methods instead. What inspired me to create this system was Project Mage and the Garnet system it is based on, and I basically want the KR object system of Project Mage with a Garnet-like GUI framework, although I intend to have it be backend-independent, so objects would have properties that dictate their representation as visual shapes, speech and Braille representations, etc and any backends added later on, and do it in Rust while still supporting LISP (Scheme instead of Common Lisp). Also the difference between this and the DOM is that the DOM is a static representation; it gives you a lot of elements you can build documents and UIs out of, but you can't really make new elements or add new attributes. You can include them but they won't do anything. But in my system, you could, for example, create a new object with the button object as its prototype, and change some of its properties and constraints to make it into a radio button object or a checkbox. There may not need to be any statically defined behavior for what different properties do if I create the system so that for each backend each object creates a rendering of itself by combining the renderings of its child objects with its own rendering. Then the top object in the tree has a rendering of the entire application that is built up recursively, and for a visual backend you could just convert that into a Vello scene and render it with a small adapter letting constraints and object properties do most of the work.
pre/next raw pointer can help with building the tree. Although unsafe, it can avoid MaybeUninit, NonNull, and easier to write. And relatively easy to be checked by miri.
HashMap/DashMap and std::any::Any. Or enum Type { U8, Bool, String ... } with match everywhere.
parameter injection maybe, see this note: corust-hackathon/parameter_injection/src/lib.rs at dev · kingwingfly/corust-hackathon · GitHub
Maybe ECS is all you need. For example, bevy_ecs can detect Changed, and filter property(Component) by Entity(ID)
trait cast and auto trait... For example
- trait cast:
dyn FoowhereFoo: Anycan be cast todyn Anysince Rust 2024 - auto trait: See crate futures, you can find traits like Stream and StreamExt.
TisStreamExtifT: Stream
Inheritance may be thought as outdated (Just my point of view)
This is very hard. And I happened to give up plugin system recently. I found zellij uses WASM. It borrows WASM stable ABI and could be run anywhere. Apart from this, Solana uses modified eBPF, it too advanced. Anyway, IPC is not so good: without stable ABI, (de)serialization is unavoidable when communication with other language, leading terrible performance. (Unless extern C and #[repr(C)] everywhere)
I have some suggestions:
- try bevy_ecs
- If async trait is needed, use dynosaur + trait_variant + async_trait to make it dynamic-compatible
- make full use of std::any
- Try DashMap/arc-swap/arcshift to get internal mutability
I do not believe things having already been constructed need updating frequently. In my practice, DashMap/arc_swap/AtomicXX is enough.
Arc is for shared, maybe just moving things is more suitable?
Even with dynamic types everywhere, it's also compile-time guaranteed (If you do not use unwrap on transmute/downgrade/... randomly). It only brings vtable consumption (maybe).
In the end, to achieve these in Rust, one certainly have to know raw pointer/unsafe/ErasedTrait/downcast/libloading and hand-writing vtable very well... Moreover, it's also tedious to write pyO3, bridge code with WASM/JS. And async trait will drive people crazy without years learning and practice. Inheritance is also hard to be used in Rust: Composition over inheritance.
OK, based on this sentence I think I understand. Not DOM, but nevertheless a port of an existing GUI system with the added goal of backend independence.
If your scripting language is Scheme, then I think you're going to find yourself implementing a Scheme runtime more so than a GUI framework. If you're using an existing Scheme implementation, then I'd implement as much of the object framework as possible in Scheme and use Rust only for a thin platform layer. Later on if you want to migrate the object system "downwards" for performance reasons you still can (but I suspect the performance will end up being fine).
There are other GUI libraries that have attempted to be backend-independent; I can think of emacs, Dear IMGUI, and React off the top of my head. React might be the closest of the three to what you want: it's basically a hierarchical object system of custom and built-in elements with automatic event dispatching on changes. A react "backend" only has to implement a handful of functions to map the library's object system onto the underlying platform APIs.
All that aside:
Javascript accessor methods appear to match the description of constraints in the manual you posted:
const o = {
val: 20,
get mirror(){ return -this.val; }
set mirror(v){ this.val = -v; }
};
console.log(o.mirror); // -20
o.mirror = 10;
console.log(o.val); // -10
I'm not sure what the important difference is, here.
This is also possible with the DOM, although I've never had a need for it:
class Check extends HTMLButtonElement {
static observedAttributes = ['checked'];
constructor(){
super();
this.addEventListener('click', () => {
this.checked = !this.checked;
});
}
get checked(){
const att = this.getAttribute('checked');
return att !== null && att !== 'false';
}
set checked(val){
this.setAttribute('checked', !!val);
}
connectedCallback(){
this.render();
}
attributeChangedCallback(name, oldAtt, newAtt){
this.render();
}
render(){
if(this.checked){
this.style.backgroundColor = 'black';
}else{
this.style.backgroundColor = 'white';
}
}
}
customElements.define('my-check', Check, { extends: 'button' });
const c = new Check();
c.innerText = "Hello, World!";
document.body.appendChild(c);
One thing to keep in mind here is:
Rust will get you high performance and strong guarantees because it specifically isn't doing a lot of these features dynamic languages have.
This shopping list is, as mentioned, essentially for a dynamic programming language like scheme or JavaScript, and these mostly only get decent performance though pretty heroic effort.
While I would definitely suggest looking into the options others have suggested here like ECS (not quite what you're asking for, but a big chunk for very little effort), you should definitely look at a hybrid approach where you use Rust to host an existing, tested, and sufficiently optimized scripting language and have all your dynamic stuff in the dynamic language, and the crunchy high performance stuff in the crunchy high performance language.
I'd suggest v8 (JavaScript) for this as it's a well suited, well optimized engine that has a lot of good integration work done for Rust due to it's use in Deno, but take a look around in scripting-language or embedded-scripting keywords too .