C# Bindings in BHL
May 21, 2026 · View on GitHub
BHL provides a system for exposing C# classes, functions, and fields to BHL scripts via IUserBindings.
Entry point
Implement IUserBindings and register it with the Types instance:
public class MyBindings : IUserBindings
{
public void Register(Types types)
{
// define symbols here
}
}
// attach to the VM
var types = new Types();
new MyBindings().Register(types);
var vm = new VM(types);
Binding free functions
Use FuncSymbolNative. The callback receives VM.ExecState exec — read arguments from exec.stack in reverse order (last argument is on top), and push the return value back:
var fn = new FuncSymbolNative(new Origin(), "min", Types.Float,
delegate(VM.ExecState exec, FuncArgsInfo args_info)
{
float b = (float)exec.stack.PopFast().num;
float a = (float)exec.stack.PopFast().num;
exec.stack.Push(Math.Min(a, b));
return null; // return a Coroutine to make the function a coroutine
},
new FuncArgSymbol("a", Types.Float),
new FuncArgSymbol("b", Types.Float)
);
types.ns.Define(fn);
float result = min(10, 5) // returns 5
Binding classes
var cl = new ClassSymbolNative(new Origin(), "Color", null,
// constructor — called when BHL code does: new Color
delegate(VM.ExecState exec, ref Val v, IType type)
{
v.SetObj(new Color(), type);
}
);
types.ns.Define(cl);
cl.Setup();
Fields
cl.Define(new FieldSymbol(new Origin(), "r", Types.Float,
// getter
delegate(VM.ExecState exec, Val ctx, ref Val v, FieldSymbol fld)
{
v.SetFlt(((Color)ctx.obj).r);
},
// setter
delegate(VM.ExecState exec, ref Val ctx, Val nv, FieldSymbol fld)
{
((Color)ctx.obj).r = (float)nv.num;
}
));
Instance methods
Instance methods are FuncSymbolNative members of the class. Access self via exec.GetSelfRef():
cl.Define(new FuncSymbolNative(new Origin(), "Lerp", Types.Void,
delegate(VM.ExecState exec, FuncArgsInfo args_info)
{
ref var self = ref exec.GetSelfRef(); // must be called first
var color = (Color)self.obj;
float t = (float)exec.stack.PopFast().num;
exec.stack.Pop(); // pop self
color.r *= t;
color.g *= t;
return null;
},
new FuncArgSymbol("t", Types.Float)
));
Static methods
Static methods do not access self:
cl.Define(new FuncSymbolNative(new Origin(), "White", FuncAttrib.Static, Types.Float, 0,
delegate(VM.ExecState exec, FuncArgsInfo args_info)
{
exec.stack.Push(Val.NewObj(exec.vm, new Color(1, 1, 1), cl));
return null;
}
));
Coroutine functions
Return a Coroutine object from the callback to make a function a coroutine:
var fn = new FuncSymbolNative(new Origin(), "WaitFrames", FuncAttrib.Coro, Types.Void, 0,
delegate(VM.ExecState exec, FuncArgsInfo args_info)
{
int frames = (int)exec.stack.PopFast().num;
return CoroutinePool.New<WaitFramesCoroutine>(exec.vm).Init(frames);
},
new FuncArgSymbol("n", Types.Int)
);
Important rules
- Always call
cl.Setup()after all fields and methods have been defined. - Arguments are popped from
exec.stackin reverse order (last argument first). - Return
nullfrom a non-coroutine callback; return aCoroutineinstance to suspend. - Use
exec.stack.Push(value)to return a value to the caller. - Retain/release
Valobjects that are stored beyond the callback lifetime.
Example BHL usage
Color c = new Color
c.r = 0.5
c.Lerp(0.5)
Color white = Color.White()
For the full working example see example/bindings/bindings.cs.