#
Bindings
Bindings are methods and variables that are defined in C# and can be accessed from Python. We provide two types of bindings: static bindings and dynamic bindings.
#
Static Bindings
Static bindings wrap a C# class or struct and expose its methods and variables to Python. This is the most common way to define bindings. Static bindings are initialized at compile time.
#
Manual Static Bindings
Manual static bindings directly create a Python equivalent of def f(a, b, *args)
in C#.
To use it, you need to create a class that inherits from PyTypeObject
.
And implement some abstract methods to specify the name and type of the Python type.
For example, to make UnityEngine.Vector2
available in Python, you can write a PyVector2Type
class like the following.
public class PyVector2Type: PyTypeObject{
// The name of the type in Python
public override string Name => "Vector2";
// The corresponding C# type
public override System.Type CSType => typeof(Vector2);
}
Next, you need to define each method and variable to be exposed to Python,
by using [PythonBinding]
attribute.
We assume that you have necessary knowledge about
Python's data model.
Such as magic methods, __new__
, __init__
, __add__
and so on.
Otherwise, you may have trouble understanding the following code.
Let's define a magic method __add__
, it is used to implement the +
operator in Python.
With __add__
, Vector2
object in Python can be added with another Vector2
object.
public class PyVector2Type: PyTypeObject{
public override string Name => "Vector2";
public override System.Type CSType => typeof(Vector2);
[PythonBinding]
public object __add__(Vector2 self, object other){
// If the other object is not a Vector2, return NotImplemented
if(!(other is Vector2)) return VM.NotImplemented;
// Otherwise, return the result of addition
return self + (Vector2)other;
}
}
This is easy to understand.
Let's see another example, __mul__
, it is used to implement the *
operator in Python.
Vector2
object in C# can be multiplied with a float
object in Python.
The following code shows this usage.
Vector2 a = new Vector2(1, 2);
Vector2 b = a * 2.0f;
Vector2 c = 2.0f * a;
As you can see, things are slightly different from __add__
.
Because the float
operand can be on the left or right side of the *
operator.
In this case, you need to define __mul__
and __rmul__
at the same time.
public class PyVector2Type: PyTypeObject{
public override string Name => "Vector2";
public override System.Type CSType => typeof(Vector2);
// ...
[PythonBinding]
public object __mul__(Vector2 self, object other){
if(!(other is float)) return VM.NotImplemented;
return self * (float)other;
}
[PythonBinding]
public object __rmul__(Vector2 self, object other){
if(!(other is float)) return VM.NotImplemented;
return self * (float)other;
}
}
Finally, let's implement the constructor of Vector2
.
__new__
magic method must be defined.
public class PyVector2Type: PyTypeObject{
public override string Name => "Vector2";
public override System.Type CSType => typeof(Vector2);
[PythonBinding]
public object __new__(PyTypeObject cls, params object[] args){
if(args.Length == 0) return new Vector2();
if(args.Length == 2){
float x = vm.PyCast<float>(args[0]);
float y = vm.PyCast<float>(args[1]);
return new Vector2(x, y);
}
vm.TypeError("Vector2.__new__ takes 0 or 2 arguments");
return null;
}
}
Here we use params object[] args
to tell the bindings that the constructor can take any number of arguments.
It is equivalent to def __new__(cls, *args)
in Python.
Note that Python does not support method overloading.
So we manually check the number of arguments and their types to determine which constructor to call.
For fields, we can form a Python property by defining a getter and a setter.
By using [PythonBinding(BindingType.Getter)]
and [PythonBinding(BindingType.Setter)]
attributes.
However, this has certain limitations for value types. Because Vector2
is a struct, it is passed by value.
So our setter will not be able to modify the original Vector2
object.
public class PyVector2Type: PyTypeObject{
public override string Name => "Vector2";
public override System.Type CSType => typeof(Vector2);
[PythonBinding(BindingType.Getter)]
public object x(Vector2 self) => self.x;
[PythonBinding(BindingType.Setter)]
public void x(Vector2 self, object value) => self.x = vm.PyCast<float>(value);
[PythonBinding(BindingType.Getter)]
public object y(Vector2 self) => self.y;
[PythonBinding(BindingType.Setter)]
public void y(Vector2 self, object value) => self.y = vm.PyCast<float>(value);
}
Once you have done all the above, you must register the type to the VM.
Here we set it into builtins
module, so that it can be accessed from anywhere.
vm.RegisterType(new PyVector2Type(), vm.builtins);
To summarize, manual static bindings provide detailed control for exposing a C# class to Python. You decide which methods and variables to expose, and how to expose them. This is our recommended way to define bindings. Also it is the most performant way.
#
Automatic Static Bindings
Automatic static bindings use C# reflection to automatically generate bindings for a C# class. It is convenient for testing and prototyping, but it is slow and unsafe since the user can access any member of the class.
vm.RegisterAutoType<Vector2>(vm.builtins);
That's all you need to do. The RegisterAutoType<T>
method will automatically generate bindings for Vector2
.
#
Dynamic Bindings
Dynamic bindings allow you to add a single C# lambda function to an object at runtime.
delegate object NativeFuncC(VM vm, object[] args);
CSharpLambda BindFunc(PyObject obj, string name, int argc, NativeFuncC f)
It is similar to bind_func
in C++ API.