Chickensoft.Introspection
3.0.0
dotnet add package Chickensoft.Introspection --version 3.0.0
NuGet\Install-Package Chickensoft.Introspection -Version 3.0.0
<PackageReference Include="Chickensoft.Introspection" Version="3.0.0" />
<PackageVersion Include="Chickensoft.Introspection" Version="3.0.0" />
<PackageReference Include="Chickensoft.Introspection" />
paket add Chickensoft.Introspection --version 3.0.0
#r "nuget: Chickensoft.Introspection, 3.0.0"
#addin nuget:?package=Chickensoft.Introspection&version=3.0.0
#tool nuget:?package=Chickensoft.Introspection&version=3.0.0
๐ฎ Introspection
Create mixins and generate metadata about types at build time to enable reflection in ahead-of-time (AOT) environments.
<p align="center"> <img alt="Chickensoft.Introspection" src="Chickensoft.Introspection/icon.png" width="200"> </p>
๐ฅ Installation
Find the latest version of the Introspection and Introspection Generator packages from nuget and add them to your C# project.
[!WARNING] Introspection is compiled against the latest .NET 8 SDK. Because .NET 8 encompasses multiple versions of the .NET compiler, compiling your project against an older version of .NET 8 may generate a
CS9057
warning to indicate a compiler-version mismatch. However, if the warning is ignored, less-tractable downstream compilation errors, or even runtime errors, may result. Therefore, we strongly recommend treatingCS9057
as an error to catch compiler versioning issues at the earliest opportunity.
<Project Sdk=...>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
...
<WarningsAsErrors>CS9057</WarningsAsErrors>
...
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Chickensoft.Introspection" Version=... />
<PackageReference Include="Chickensoft.Introspection.Generator" Version=... PrivateAssets="all" OutputItemType="analyzer" />
</ItemGroup>
</Project>
๐ Background
This is a metaprogramming tool that powers some of the other Chickensoft tools. Introspection exists to enable C# developers to get information about a type at runtime without needing reflection that wouldn't work or be guaranteed to work in all scenarios when compiling for ahead-of-time targets like iOS.
The introspection package provides the following features:
- โ Create a registry of all types visible from the global scope.
- โ Generate metadata about visible types.
- โ Track types by id and version.
- โ Allow types to implement and look up mixins.
- โ Compute and cache type hierarchies, attributes, and properties.
- โ Track generic types of properties in a way that enables convenient serialization in AOT environments.
You don't need to fully understand this package to make the most of it. In fact, you may never need to use it directly since you are more likely to encounter it as a dependency of one of the other Chickensoft tools:
- ๐พ Serialization uses this tool to gather information about types at build-time to perform serialization and deserialization at runtime without having to resort to unsupported reflection techniques in AOT environments.
- ๐ AutoInject uses this tool to allow you to add mixins to classes at build-time and invoke their methods at runtime without reflection. It also leverages this to read attributes on types without having to use reflection.
- ๐ก LogicBlocks uses this tool to look up possible states for a state machine so it can pre-allocate them at runtime without needing reflection.
[!NOTE] In general, you will typically just include this project and its source generator in your project so that other tools can make use of the generated metadata about the types in your project. For those interested in building metaprogramming tools which leverage this package, read on!
The introspection generator is designed to be performant as a project grows. The generator only uses syntax information to generate metadata, rather than relying on the C# analyzer's symbol data, which can be very slow.
[!NOTE] This tool exists because many reflection-based API's don't work (or aren't guaranteed to work reliably in all scenarios) when compiling for AOT targets (like iOS). Chickensoft is striving to create packages that work in AOT environments like iOS, as mobile games and apps are extremely widespread. For more information, read our philosophy.
๐ Usage
๐งโโ๏ธ Introspective Types
Simply add the [Meta]
attribute to a partial class or record that is visible from the global scope.
using Chickensoft.Introspection;
[Meta]
public partial class MyType;
public partial class Container {
// Nested types are supported, too.
[Meta]
public partial class MyType;
}
The generator will generate a type registry for your assembly that lists every type it can discover in the codebase, along with their generated metadata. Introspective types have much additional metadata compared to types without the [Meta]
attribute.
The generated registry automatically registers types with the Introspection library's type graph using a module initializer, so no action is needed on the developer's part. The module initializer registration process also performs some logic at runtime to resolve the type graph and cache the type hierarchy in a way that makes it performant to lookup. This preprocessing runs in roughly linear time and is negligible.
All introspective types must be a class, record, struct, or record struct. They must also be partial and visible from the global scope (nesting inside other types is allowed if all nested types are visible from the global scope and partial). Introspective types cannot be generic โ this is an intentional limitation.
๐ชช Identifiable Types
An introspective type can also be an identifiable type if it is given the [Id]
attribute. Identifiable types get additional metadata generated about them, allowing them to be looked up by their identifier. Identifiable types can be used to create a serialization system, since id's are strings which should be kept stable even if the type itself is renamed.
[Meta, Id("my_type")]
public partial class MyType;
โคต๏ธ The Type Graph
The type graph can be used to query information about types at runtime. If the type graph has to compute a query, the results are cached for all future queries. Most api's are simple O(1)
lookups.
// Get every type that is a valid subtype of Ancestor.
var allSubtypes = Types.Graph.GetDescendantSubtypes(typeof(Ancestor));
// Only get the types that directly inherit from Parent.
var subtypes = Types.Graph.GetSubtypes(typeof(Parent));
// Get generated metadata associated with a type.
if (Types.Graph.GetMetadata(typeof(Model)) is { } metadata) {
// ...
}
// Get properties, including those from parent introspective types.
var properties = Types.Graph.GetProperties(typeof(Model));
// ...see the source for all possible type graph operations.
๐ฏโโ๏ธ Versioning
All concrete (not abstract) introspective types have a simple integer version associated with them. By default, the version is 1
. You can use the [Version]
attribute to denote the version of introspective reference types.
[Meta, Version(2)]
public partial class MyType;
A single identifiable type can have multiple versions. Each version is indicated by making another type which extends it and denotes its version with the [Version]
attribute.
[!CAUTION] Value types cannot be versioned since C# does not support struct inheritance..
// Or, multiple versions of the same identifiable type.
[Meta, Id("my_type")]
public abstract class MyType;
[Meta, Version(1)]
public class MyType1 : MyType;
[Meta, Version(2)]
public class MyType2 : MyType;
[Meta, Version(3)]
public class MyType3 : MyType;
During type registration, the type graph will "promote" introspective types which inherit from an identifiable type to an identifiable type themselves, sharing the same identifier as their parent or ancestor identifiable type. Promoted identifiable types must, however, have uniquely specified versions. For example, the types shown above (MyType1
, MyType2
, and MyType3
) would be promoted at runtime by the type graph when the application first starts, allowing them to be seen as the same identifiable type as their parent, MyType
. Fortunately, this all happens automatically.
๐ Metadata Types
The introspection generator differentiates between the following categories of types and constructs the appropriate metadata for the type, depending on which category it belongs to.
Category | Metadata |
---|---|
๐ซฅ Abstract or generic types | TypeMetadata |
๐ชจ Non-generic, concrete types | ConcreteTypeMetadata |
๐ป Abstract introspective types | AbstractIntrospectiveTypeMetadata |
๐ฟ Concrete introspective types | IntrospectiveTypeMetadata |
๐ Abstract identifiable types | AbstractIdentifiableTypeMetadata |
๐ชช Concrete identifiable types | IdentifiableTypeMetadata |
You can check the type of metadata that a type has to understand what its capabilities are. Each type of metadata has different fields associated with it.
In addition to the metadata classes, each metadata class implements the appropriate interfaces:
Metadata | Conforms To |
---|---|
TypeMetadata |
ITypeMetadata |
ConcreteTypeMetadata |
..., IClosedTypeMetadata , IConcreteMetadata |
IntrospectiveTypeMetadata |
..., IConcreteIntrospectiveTypeMetadata |
IdentifiableTypeMetadata |
..., IIdentifiableTypeMetadata |
... etc. |
public class MyTypeReceiver : ITypeReceiver {
public void Receive<T>() {
// Do whatever you want with the type as a generic parameter.
}
}
var metadata = Types.Graph.GetMetadata(typeof(Model));
if (metadata is IClosedTypeMetadata closedMetadata) {
// Closed types allow you to receive the type as a generic argument in
// a TypeReceiver's Receive<T>() method.
closedMetadata.GenericTypeGetter(new MyTypeReceiver())
}
if (metadata is IConcreteTypeMetadata concreteMetadata) {
// Concrete types allow you to create a new instance of the type, if
// it has a parameterless constructor.
var instance = concreteMetadata.Factory();
}
if (metadata is IIntrospectiveTypeMetadata introMetadata) {
// Introspective types provide a metatype instance which allows you to access
// more information about that type, such as its properties and attributes.
var metatype = introMetadata.Metatype;
}
if (metadata is IConcreteIntrospectiveTypeMetadata concreteIntroMetadata) {
// Concrete introspective types have a version number.
var version = concreteIntroMetadata.Version;
}
if (metadata is IIdentifiableTypeMetadata idMetadata) {
// Identifiable types have an id.
var id = idMetadata.Id;
}
The metadata type hierarchy is admittedly quite crazy, but extremely useful since it allows tools built on this library to understand what information is available about a type depending on how it was attributed with [Meta]
, [Id]
, or [Version]
attributes (or not at all). In the class diagram below, solid lines indicate inheritance, while dashed lines indicate interface implementation.
classDiagram
%% โโโ Interfaces โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
class ITypeMetadata {
+string Name
}
class IClosedTypeMetadata {
+Action<ITypeReceiver> GenericTypeGetter
}
ITypeMetadata <|-- IClosedTypeMetadata
class IConcreteTypeMetadata {
+Func<object> Factory
}
IClosedTypeMetadata <|-- IConcreteTypeMetadata
class IIntrospectiveTypeMetadata {
+IMetatype Metatype
}
IClosedTypeMetadata <|-- IIntrospectiveTypeMetadata
class IConcreteIntrospectiveTypeMetadata {
+int Version
}
IIntrospectiveTypeMetadata <|-- IConcreteIntrospectiveTypeMetadata
IConcreteTypeMetadata <|-- IConcreteIntrospectiveTypeMetadata
class IIdentifiableTypeMetadata {
+string Id
}
IIntrospectiveTypeMetadata <|-- IIdentifiableTypeMetadata
%% โโโ Records / concrete types โโโโโโโโโโโโโโโโโโโโโโโโ
class TypeMetadata {
+string Name
}
ITypeMetadata <|.. TypeMetadata
class ConcreteTypeMetadata {
+Action<ITypeReceiver> GenericTypeGetter
+Func<object> Factory
}
TypeMetadata <|-- ConcreteTypeMetadata
IConcreteTypeMetadata <|.. ConcreteTypeMetadata
class AbstractIntrospectiveTypeMetadata {
+Action<ITypeReceiver> GenericTypeGetter
+IMetatype Metatype
}
TypeMetadata <|-- AbstractIntrospectiveTypeMetadata
IIntrospectiveTypeMetadata <|.. AbstractIntrospectiveTypeMetadata
class IntrospectiveTypeMetadata {
+Action<ITypeReceiver> GenericTypeGetter
+Func<object> Factory
+IMetatype Metatype
+int Version
}
ConcreteTypeMetadata <|-- IntrospectiveTypeMetadata
IConcreteIntrospectiveTypeMetadata <|.. IntrospectiveTypeMetadata
class AbstractIdentifiableTypeMetadata {
+Action<ITypeReceiver> GenericTypeGetter
+IMetatype Metatype
+string Id
}
AbstractIntrospectiveTypeMetadata <|-- AbstractIdentifiableTypeMetadata
IIdentifiableTypeMetadata <|.. AbstractIdentifiableTypeMetadata
class IdentifiableTypeMetadata {
+Action<ITypeReceiver> GenericTypeGetter
+Func<object> Factory
+IMetatype Metatype
+string Id
+int Version
}
IntrospectiveTypeMetadata <|-- IdentifiableTypeMetadata
IIdentifiableTypeMetadata <|.. IdentifiableTypeMetadata
IConcreteIntrospectiveTypeMetadata <|.. IdentifiableTypeMetadata
Metatype data provides information about a specific type, its properties, and attributes. The type graph combines metatype information with its understanding of the type hierarchy to enable you to fetch all properties of an introspective type, including those it inherited from other introspective types. Metatypes will only contain information about the type itself, not anything it inherits from.
To see all of the information that a metatype exposes, please see the Metatype interface definition.
๐๏ธ Mixins
The introspection generator allows you to create mixins on introspective reference types to add additional functionality to the type. Unlike default interface method implementations, mixins are able to add instance state via a blackboard. Every introspective reference type has a MixinState
blackboard which allows mixins to add instance data to the type they are applied to.
Additionally, mixins must implement a single handler method. An introspective type's Metatype
has a Mixins
property containing a list of mixin types that were applied to it. Additionally, a MixinHandler
table is provided which maps the mixin type to a closure which invokes the mixin's handler.
Introspective type instances can also cast themselves to IIntrospectiveRef
to invoke a given mixin easily.
[!WARNING] Value types do not support mixins.
// Declare a mixin
[Mixin]
public interface IMyMixin : IMixin<IMyMixin> {
void IMixin<IMyMixin>.Handler() { }
}
// Use a mixin
[Meta(typeof(Mixin))]
public partial class MyModel {
// Use mixins
public void MyMethod() {
// Call all applied mixin handlers
(this as IIntrospectiveRef).InvokeMixins();
// Call a specific mixin handler
(this as IIntrospectiveRef).InvokeMixin(typeof(IMyMixin));
}
}
๐ฅ Value Types
The introspection generator can generate metadata about value types, too.
[!CAUTION] Value types do not support versioning or mixins.
Mixins could have been supported, but we chose not to support them since they would add a heap-allocated object to each value type, defeating the memory performance gains that value types provide.
[Meta]
public readonly partial record struct ValueType {
public required int Number { get; init; }
public string? Description { get; init; }
}
[Meta, Id("value_type_with_id")]
public readonly partial record struct ValueTypeWithId {
public required int Number { get; init; }
public string? Description { get; init; }
}
Likewise, you can introspect a value type the same as other introspective types:
// Get a type's properties
var properties = Types.Graph.GetProperties(typeof(ValueType));
foreach (var property in properties) {
// ... do something with the properties of the type
}
๐ฃ Package generated from a ๐ค Chickensoft Template โ https://chickensoft.games
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. |
.NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.1 is compatible. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- Chickensoft.Collections (>= 1.12.5)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Chickensoft.Introspection:
Package | Downloads |
---|---|
Chickensoft.LogicBlocks
Human-friendly, hierarchical state machines for games and apps in C#. |
|
Chickensoft.Serialization
Easy to use serializable models with AOT compilation support and System.Text.Json compatibility. |
GitHub repositories (3)
Showing the top 3 popular GitHub repositories that depend on Chickensoft.Introspection:
Repository | Stars |
---|---|
chickensoft-games/GameDemo
The Chickensoft Game Demo โ a fully tested, third-person 3D game built with Godot and C#. Now with saving and loading!
|
|
chickensoft-games/LogicBlocks
Human-friendly, hierarchical and serializable state machines for games and apps in C#.
|
|
chickensoft-games/AutoInject
Node-based dependency injection for C# Godot scripts at build-time, including utilities for automatic node-binding, additional lifecycle hooks, and .net-inspired notification callbacks.
|
Version | Downloads | Last updated |
---|---|---|
3.0.0 | 271 | 12 days ago |
2.2.0 | 2,698 | 4 months ago |
2.1.0 | 428 | 4 months ago |
2.0.0 | 1,768 | 6 months ago |
1.8.0 | 1,265 | 7 months ago |
1.7.0 | 867 | 8 months ago |
1.6.0 | 120 | 8 months ago |
1.5.0 | 356 | 8 months ago |
1.4.0 | 671 | 9 months ago |
1.3.0 | 1,069 | 6/9/2024 |
1.2.0 | 246 | 6/8/2024 |
1.1.0 | 177 | 6/4/2024 |
1.0.0 | 134 | 6/4/2024 |
Chickensoft.Introspection release.