{"id":16680833,"url":"https://github.com/kupiakos/tinydyn","last_synced_at":"2025-06-30T12:37:42.988Z","repository":{"id":181148828,"uuid":"603304213","full_name":"kupiakos/tinydyn","owner":"kupiakos","description":"Tiny dynamic dispatch in Rust","archived":false,"fork":false,"pushed_at":"2023-04-17T18:33:27.000Z","size":44,"stargazers_count":11,"open_issues_count":1,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-06-28T06:58:39.856Z","etag":null,"topics":["embedded","embedded-rust","rust","vtable"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kupiakos.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-02-18T05:02:56.000Z","updated_at":"2024-07-19T15:36:22.000Z","dependencies_parsed_at":"2023-07-14T08:57:39.247Z","dependency_job_id":"c3f05d92-1385-41e1-aa73-93f9d76a5094","html_url":"https://github.com/kupiakos/tinydyn","commit_stats":null,"previous_names":["kupiakos/tinydyn"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/kupiakos/tinydyn","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kupiakos%2Ftinydyn","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kupiakos%2Ftinydyn/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kupiakos%2Ftinydyn/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kupiakos%2Ftinydyn/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kupiakos","download_url":"https://codeload.github.com/kupiakos/tinydyn/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kupiakos%2Ftinydyn/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262775061,"owners_count":23362438,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["embedded","embedded-rust","rust","vtable"],"created_at":"2024-10-12T13:44:04.954Z","updated_at":"2025-06-30T12:37:42.749Z","avatar_url":"https://github.com/kupiakos.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# `tinydyn`\n\nLightweight dynamic dispatch, intended for embedded use.\n\n`Ref\u003cdyn Trait\u003e` and `RefMut\u003cdyn Trait\u003e` wrap a pointer and metadata necessary to call\ntrait methods, and `Deref` into a _tinydyn trait object_ that implements the `Trait`.\n\nTraits must currently opt-in by annotating with `#[tinydyn]`.\nThis defines an alternate, lighter weight [vtable], and if the trait has one method, eliminates\nit entirely by putting the function pointer inline.\nThis does not affect normal behavior of the trait, and can still be made into a `dyn Trait`.\nThis, however, would be wasteful.\n\n[vtable]: https://en.wikipedia.org/wiki/Virtual_method_table\n\n## Example\n\n```rust\nuse tinydyn::{tinydyn, Ref};\n\n#[tinydyn]\ntrait Foo {\n    fn blah(\u0026self) -\u003e i32;\n    fn blue(\u0026self) -\u003e i32 { 10 }\n}\nimpl Foo for i32 {\n    fn blah(\u0026self) -\u003e i32 { *self + 1 }\n}\n\n// Like upcasting to `\u0026dyn Foo`, but with a lighter weight vtable.\nlet x: Ref\u003cdyn Foo\u003e = Ref::new(\u002615);\nassert_eq!(x.blah(), 16);\nassert_eq!(x.blue(), 10);\n```\n\n## Space Savings\n\nTODO: numbers on a real large embedded project\n\nFor every trait and concrete type which upcasts into that trait, Rust creates a new vtable.\nEach vtable includes 3 extra pointer-sized values of layout and drop info.\nThese aren't needed, so tinydyn's custom vtables do not include them.\n\nIn addition, tinydyn places the vtable inline in `Ref[Mut]` if it has only one method.\nThis saves a dereference when making the virtual call as well as removing the need for a static\nvtable to allocate - truly as zero-cost as dynamic dispatch can get!\n\n\n## Design\n\n### Background\n\nTrait objects in Rust are not fully zero-cost.\nIn order for one set of code to handle multiple types with\nvarying sizes, alignments, and behaviors, Rust must include extra\nmetadata with the erased type to be able to work with it.\n\nSay we have a trait `Doggo` with two methods:\n\n```rust\ntrait Doggo {\n    fn wag(\u0026self);\n    fn bark(\u0026self);\n}\n```\n\nA concrete type can implement that trait by defining the necessary methods.\nEach of these methods knows the concrete type at compile time.\n\n```rust\nstruct Pupper {\n    age: u32,\n    name: \u0026'static str,\n}\n\nimpl Doggo for Pupper {\n    fn wag(\u0026self) { /* wag when self.name heard */ }\n    fn bark(\u0026self) { /* yip based on self.age */ }\n}\n\nstruct Woofer {\n    woof_freq: u16,\n}\n\nimpl Doggo for Woofer {\n    fn wag(\u0026self) { /* big woofer wagging */ }\n    fn bark(\u0026self) { /* release a woof at self.woof_freq */ }\n}\n```\n\nTo work with multiple types that implement `Doggo` with the same\ncode, generics can be used. A `fn take_doggo(x: \u0026impl Doggo)` will\ncreate a copy of `take_doggo` for every concrete type passed in,\nand at compile time, this copy knows how the type is laid out in memory,\nhow to call the needed `Doggo`, and how to best inline.\n\nWhen you only have one copy or the code is trivial, this\n[monomorphization] is the best, as the compiler has the most information available to it.\n\n[monomorphization]: https://rustc-dev-guide.rust-lang.org/backend/monomorph.html\n\nIf you need one copy of code to deal with multiple types,\nwe can _erase_ some of this compile-time information.\nA `fn take_doggo(x: \u0026dyn Doggo)` works with a reference to a\n_trait object_, a [dynamically-sized type][dst] that holds the needed info for\nRust to work with it. This function trades some indirection and more\nchallenging inlining with only needing one copy of `take_doggo`.\n\nSay we have a `bluey: Pupper` (`bluey` is a value with type `Pupper`).\nWhen we upcast `\u0026bluey` to a `\u0026dyn Doggo`, we erase its type through\nan [unsizing coercion]. This unsizing coercion tacks on an extra\npointer to a _vtable_, a table that defines runtime-accessible type\ninformation specific to the trait, most notably the method addresses.\nThis creates a wide pointer, like how `\u0026[T]` carries a pointer and a length.\n\n\n[dst]: https://doc.rust-lang.org/nomicon/exotic-sizes.html#dynamically-sized-types-dsts\n[unsizing coercion]: https://doc.rust-lang.org/reference/type-coercions.html#unsized-coercions\n\n\u003cimg src=\"img/builtin-trait-object-1.dot.svg\"/\u003e\n\nMultiple `\u0026dyn Doggo` of the same concrete type can share the same vtable.\n\n\u003cimg src=\"img/builtin-trait-object-2.dot.svg\"/\u003e\n\n### Motivation\n\nThere's three pointer-sized values that are always included, but aren't used for\ndynamic dispatch, but instead for other type-erased operations:\n\n- `size`, which is used by [`mem::size_of_val`], for deallocation, and for\n  type layout inside custom DSTs.\n- `align`, which is used by [`mem::align_of_val`], for deallocation, and for\n  type layout inside custom DSTs.\n- The drop glue, which is optional and is essentially [`drop_in_place`] for the\n  concrete type. Only needed to dynamically drop a type, like `Box\u003cdyn Trait\u003e`.\n\n[`drop_in_place`]: https://doc.rust-lang.org/core/ptr/fn.drop_in_place.html\n[`mem::size_of_val`]: https://doc.rust-lang.org/core/mem/fn.size_of_val.html\n[`mem::align_of_val`]: https://doc.rust-lang.org/core/mem/fn.align_of_val.html\n\nHowever, what if your code doesn't need any of this? If all you need is dynamic\ndispatch through an borrow and have no need for accurate layout information,\nthese values are an unnecessary bloat that pile up on an embedded system.\n\n### `tinydyn` vtables\n\n`tinydyn` defines lighter-weight dynamic dispatch objects through a `#[tinydyn]`\nmacro on a trait. This defines an alternative vtable format and reference\nwrappers to call these methods. This wrapper can't query the runtime layout\ninformation about the concrete type, nor can it drop it. However, it can\ncall trait methods.\n\n\u003cimg src=\"img/tinydyn-trait-object.dot.svg\"/\u003e\n\n### Inline vtable\n\n`tinydyn` optimizes one step further for traits with one method:\nit includes the function pointer for that method alongside the erased\ntype instead of using a static vtable. This is as cheap as this scheme\nof dynamic dispatch can be, and is how one might implement it in C.\n\n\u003cimg src=\"img/tinydyn-trait-object-inline.dot.svg\"/\u003e\n\n\n### Double Pointer\nFor safety reasons, the unsized trait object that `Ref`/ `RefMut` deref into is a\npointer to the trait object, creating a double pointer to the object. So, while you _can_ turn\nthem into a `\u0026(impl Trait + ?Sized)`, that will be marginally larger code size if not optimized.\n\n### Why can't `dyn Trait` be made smaller as an optimization?\n\nIn theory, `rustc` could identify that a trait object's size, align, and drop glue are never\naccessed throughout the whole program and remove them from the vtable, possibly even inlining\nthe vtable as tinydyn does. However, rustc is averse to global analysis, preferring to leave\nthis to LLVM; and LLVM doesn't know how trait object vtables are formatted.\n\nThese are requirements tinydyn doesn't have to uphold. It doesn't have a `Box`.\n\n### A trait object that doesn't know its size\n\nSince tinydyn trait objects don't know the size or alignment of what they point to, no\nreference to the concrete type can be made while the type is erased.\n\nSo, in order for the tinydyn trait object to implement a trait, the implementer itself has to\nhave an erased pointer type. If that pointer type is sized, however, that comes with its own set\nof issues. You can [swap] two `Sized` references, and trait object-unsafe\nfunctions marked with `where Self: Sized` are now available to call, even though there's no\npossible implementation.\n\n[swap]: https://doc.rust-lang.org/core/mem/fn.swap.html\n\ntinydyn trait objects do this with a specific design:\n- They're primarily referenced through the [`Ref`] and [`RefMut`] types, which hold the data\npointer and metadata needed to call trait methods with no overhead.\n- These don't implement the trait, but `Deref` into a `!Sized` wrapper object that does,\ncalled the *dyn wrapper*.\n- The dyn wrapper holds same pointer as the `Ref[Mut]`,\nso the `Deref` creates a double reference to avoid creating a direct reference to the target.\n- The deref wrapper object is discouraged from being used through reference like trait objects\nnormally are. Not only does it have an inaccurate `size_of_val` and `align_of_val`, it is\na double pointer and is more expensive to use directly.\n- The vtable-calling functions are marked `#[inline(always)]` so the double pointer created\nwhen calling trait methods is detected as unnecessary and optimized away by LLVM.\n\nThis fake layout and double dereference is, in the end, a necessary design decision for\nsoundness.\n\n### Function pointer `transmute`\n\nIn order to avoid the generation of duplicate functions or multiple addresses for the same method,\nthis library performs an `unsafe` transmute of a function item cast to `*const ()` into a\n\"`\u0026self`-erased\" `fn` pointer. This is the most problematic operation it performs. It makes these\nassertions about unsafe Rust:\n\n- There is no layout difference between `\u0026'a T` where `T: Sized` and `*const ()`. These pointers can\n  be soundly transmuted between each other for the lifetime `'a`. Similarly for `\u0026mut` and\n  `*mut ()`.\n- Lifetimes are _entirely_ transparent to function call ABI.\n- A function item cast to `*const ()` can soundly be transmuted to a function pointer if:\n  - All parameters have identical layout.\n  - All pointer parameters have the same mutability.\n  - Function pointers are the size of `*const ()` on this platform\n    (checked by `transmute`).\n\n\n## Contributing\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md) for details.\n\n## License\n\nApache 2.0; see [`LICENSE`](LICENSE) for details.\n\n## Disclaimer\n\nThis project is not an official Google project. It is not supported by\nGoogle and Google specifically disclaims all warranties as to its quality,\nmerchantability, or fitness for a particular purpose.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkupiakos%2Ftinydyn","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkupiakos%2Ftinydyn","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkupiakos%2Ftinydyn/lists"}