Rebel Fork Framework
C# Scripting

About C# Support

C# support is provided by customized SWIG wrapper interface generator.

Extensive API coverage estimated at 90%+ Support for inheriting native classes and overriding their virtual methods Support for ref parameters Support for native containers (ea::map<T>, ea::unordered_map<T>, ea::vector<T>) Transparently unwrapping SharedPtr<T> and WeakPtr<T> Serialization and deserialization of managed components Member variable and method renaming to match C# conventions Support for registering C# fields as attributes Managed editor plugins with hot code reload

Requirements

.NET framework (4.7+) (Windows) Mono (5.4+) (Linux/MacOS/Android/iOS) -DBUILD_SHARED_LIBS=ON build parameter. -DURHO3D_CSHARP=ON build parameter. -DURHO3D_PLUGINS=ON build parameter (optional).

IDE support

On Windows it is advised to use Visual Studio. C# projects will be automatically embedded into generated solution. C# code can be edited and debugged, including hot-reloadable plugins running in the editor. To debug these plugins set EditorHost as your startup application and start debugger.

Scripting

Building engine with -DURHO3D_PLUGINS=ON includes a dummy plugin that is in charge of compiling Scripts/*.cs files from resource path at runtime. Build option name is rather misleading because said flag actually implements hot code reload in the editor, by building reloadable code as plugins that can be loaded and unloaded.

Implementation details

Due to differences between C++ and C# languages generated bindings are inherently unsafe. Wrapper tries to make usage as transparent as possible.

There are two kinds of wrapped objects:

  1. Objects that inherit from RefCounted.
  2. Objects that do not inherit from RefCounted.

Using RefCounted objects

Memory management of RefCounted objects is semi-manual in C# code. There are three rules that user must follow in order to achieve deterministic object destruction.

  1. Always call obj.Dispose() when obj is no longer in use. Required if object is constructed using Context::CreateObject or new keyword. Failure to dispose of object when required will defer object deallocation to garbage collector. A good practice is to always call obj.Dispose() or use using pattern, without being concerned whether it is required or not.
  2. Always call RefCounted::AddRef if you intend to hold object reference for a prolonged period of time. Optional, if object is constructed using Context::CreateObject or new keyword.
  3. Always call RefCounted::ReleaseRef for each corresponding RefCounted::AddRef call, when you are done using the object. RefCounted::ReleaseRef will immdeiately free an underlying native object when last reference is released, managed object will enter "expired" state (RefCounted.Expired will return true). Calls to obj.Dispose() are noop after object is freed. Failure to release a reference when required will result in memory leaks.

C# object is a thin wrapper object containing a pointer to a native object implementing core logic. Manual memory management means that C# code may hold an expired reference. This happens when engine owned an object, user accessed it, saved reference to an object and engine freed this object. For example Scene owns it's \Node instances. If user saved a reference to a particular Node and this node was later removed from the scene - saved instance is no longer valid. In order to determine validity of a reference please use RefCounted.Expired or RefCounted.NotExpired properties.

Engine guarantees that a wrapper C# object will never be destroyed unless a native instance is also destroyed.

Please implement protected override void Dispose(bool disposing) method and implement any logic for releasing resources. Engine guarantees that user's implementation of Dispose(true) will be called before native object is freed.

Other classes

When class does not inherit from Urho3D::RefCounted - it's lifetime is managed manually. Managed instances of such objects can be returned as owning references or non-owning references.

Owning reference allocates it's native object instance and frees it when instances Dispose() method is called. These objects must be referenced through their entire lifetime and disposed of manually. If such object is passed to the engine API - object must remain alive as long as engine is using this object.

Non-owning references only provide a temporary wrapper around native object. Such instances are returned when T& object is accessed. In this case engine manages lifetime of the object and it is not safe to keep a reference to such object in managed code.

Wrapper keeps a cache of wrapped native pointers and their wrappers. Non-owning wrapper object will reside in cache for some time and be reused. Example of such behavior is VariantMap parameter passed to event handler. Every time such argument is returned a hashmap lookup is performed to retrieve cached wrapper object from the cache.

Best practices

Object reference counting is atomic, but not thread-safe. Executing RefCounted::ReleaseRef when object has one reference and calling Dispose() method of managed instance on different thread is undefined behavior. Note that "Dispose(false)" may be implicitly called from a finalizer thread if all the references to managed object are lost.

Classes that inherit from native classes should override Dispose(bool)` method and dispose of other native resources class owns. Failure to do so may or may not result in memory leaks. Wrapper does free native resources in object finalizer but managed runtime does not guarantee execution of finalizers, nor their execution is deterministic.

It is advised to manually dispose objects by calling their Dispose() method. In order to dispose object it must have 1 remaining reference. This means that native objects may not keep any references to object that is being disposed. Calling Dispose() on an object with more than 1 reference will throw an exception and leave an object in an unknown state, therefore caching of such exceptions is not advised. Instead application should be fixed to not throw them.

Calling Dispose() on non-owning wrapper instances will dispose of managed wrapper instance only. Native object is not freed and may be accessed later.

Common Problems

BadImageFormatException

Application may throw a BadImageFormatException on startup if you configured PlatformTarget property of your .csproj incorrectly. Always set PlatformTarget to your target platform. x64 for 64bit and x86 for 32bit. Never use AnyCPU.