-
Notifications
You must be signed in to change notification settings - Fork 70
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Overhaul generation of bindings layer #1
Comments
|
GCHandles are not reference counted, only explicitly allocated and freed. C++ wrappers hold onto a GCHandle. So there are two broad ways of working: a. Every C++ wrapper instance owns its GCHandle, which it frees in its destructor. If a wrapper is copied, the GCHandle must be copied as well (i.e. call into C# to resolve GCHandle to object, create a new GCHandle from the same object, and return it to the C++ code).
|
We need to support at least the following types in method/property signatures:
|
Some useful resources:
|
We can access non-blittable value types (C# structs) from the C++ side in two possible ways:
(1) will always work, but requires boxing, which is a heap allocation and a copy. (2) costs only as much as the getting a pointer to the object. If that requires pinning, it's not worth it. But getting a pointer to a method parameter or local variable is basically free. As long as we accept that the pointer is only valid for the lifetime of the method. Ideally, we do both. Use the pointer when we can, and otherwise use a handle to the boxed value. This can be implemented as a variant on the C++ side. On copy or move of the C++ object, we convert the pointer to the GCHandle. But it probably makes sense to stick to (1) until we see a clear benefit to optimizing with (2). |
Currently, managed classes that have some of their methods implemented in C++ (e.g. MonoBehaviours) hold an IntPtr to the C++ implementation class. And they have a Dispose method and a finalizer to make sure that this C++ implementation class is destroyed at the appropriate time. This isn't great for two reasons:
Both of these problems can be solved by holding a |
Events and Delegates are tricky. UnityNativeScripting's approach works, but it gets the semantics wrong (IMO). Its approach is described here: https://www.jacksondunstan.com/articles/4174
Then B won't get garbage collected as long as A is ineligible for garbage collection and that delegate instance continues to exist. That's true even if no one else in the entire app has a reference to B. On the other hand, the existence of this delegate has no impact on the lifetime of A. If no references to A exists anywhere, then A will be garbage collected, as will myDelegate. If no other references to B exist (other than the delegate), then B will be garbage collected too. This is all sort of obvious if you understand how delegates work. A delegate instance is really nothing more than a class instance with a target object field and a target method field. However, it often trips up people who are new to C#. Now extending this to C++, we should be able to create a delegate around a C++ std::function or class instance, and that function or class instance should be kept alive for as long as the delegate is kept alive. With UnityNativeScripting, however, the C++ object's lifetime must be controlled explicitly, and when it is destroyed the delegate effectively becomes inert. Invoking it does nothing. This is confusing, and attempts to work around it can easily lead to memory leaks. Consistent with out how we handle C# classes with some of their methods implemented in C++, we should aim to make the semantics match those in C#. To achieve this, we need:
One thing that's not great about this plan is that the std::functions will only ever be freed by the finalizer, because C# doesn't have any pattern for explicitly disposing delegates. As long as we're not creating them rapid-fire this should be fine, though. If it does turn out to be a problem, we can probably optimize for certain cases, e.g. provide an explicit dispose on the C++ side, or dispose when the delegate is reassigned null. |
The "bindings" layer that allows our C++ plugin to call into Unity and the .NET platform is currently generated by a fork of UnityNativeScripting. UnityNativeScripting is a fantastic idea, it's backed up by really useful tech articles, and is a solid proof-of-concept implementation. To take Cesium for Unity to production, however, we need something a bit more production-ready. Some of the problems with it:
Append
on aStringBuilder
, rather than using templating.GCHandle
does this already.MonoBehaviour
in C++. However, it doesn't handle the ownership/lifetime in a principled way, leading to hard-to-debug gotchas. For example, you can't create an instance ofMyCPPMonoBehaviour
and attach it to aGameObject
without carefully ensuring that the C++ class instance sticks around until the GameObject is destroyed or the MonoBehaviour is removed. Otherwise you'll end up with a broken MonoBehaviour attached to the GameObject. This means the rules for using C++ objects derived from C# classes are different from the rules for using regular C# classes, which is super confusing. I believe the right way to solve this is to separate the "C++ wrapper class" from the "C++ implementation class." The C++ wrapper instance simply refers to the C# instance and allows it to be used from C++, just like any wrapper for any C# object. The C++ implementation instance is created by and owned by the C# instance, and allows for the implementation of parts of the C# class to be delegated to C++.The text was updated successfully, but these errors were encountered: