# Copyright 2026 DeepMind Technologies Limited # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================== """Generates C# bindings for MuJoCo from introspect modules.""" import re import sys import ctypes from absl import app from absl import flags from introspect import ast_nodes from introspect import enums as introspect_enums from introspect import functions as introspect_functions from introspect import structs as introspect_structs _HEADER_FILES = flags.DEFINE_string( 'header_file_list', '', 'Space-separated list of MuJoCo header files.' ) _BOILERPLATE_IMPORTS = [ 'using System;', 'using System.Runtime.InteropServices;', 'using System.Text;', 'using System.Collections.Generic;', 'using SizeDict = System.Collections.Generic.Dictionary;', ( 'using NestedDictionary = System.Collections.Generic' '.Dictionary>;' ), ] _CS_RESERVED_KEYWORDS = { 'abstract', 'as', 'base', 'bool', 'break', 'byte', 'case', 'catch', 'char', 'checked', 'class', 'const', 'continue', 'decimal', 'default', 'delegate', 'do', 'double', 'else', 'enum', 'event', 'explicit', 'extern', 'false', 'finally', 'fixed', 'float', 'for', 'foreach', 'goto', 'if', 'implicit', 'in', 'int', 'interface', 'internal', 'is', 'lock', 'long', 'namespace', 'new', 'null', 'object', 'operator', 'out', 'override', 'params', 'private', 'protected', 'public', 'readonly', 'ref', 'return', 'sbyte', 'sealed', 'short', 'sizeof', 'stackalloc', 'static', 'string', 'struct', 'switch', 'this', 'throw', 'true', 'try', 'typeof', 'uint', 'ulong', 'unchecked', 'unsafe', 'ushort', 'using', 'virtual', 'void', 'volatile', 'while', } C_TO_CS_TYPE = { 'int': 'int', 'unsigned int': 'uint', 'float': 'float', 'double': 'double', 'char': 'char', 'unsigned char': 'byte', 'size_t': 'UIntPtr', 'int64_t': 'Int64', 'uint64_t': 'UInt64', 'uintptr_t': 'UIntPtr', 'void': 'void', 'mjtNum': 'double', 'mjtByte': 'byte', 'mjtSize': 'UInt64' if ctypes.sizeof(ctypes.c_size_t) == 8 else 'UInt32', } PYTHON_TYPE_TO_CS_TYPE = { 'float': 'double', } PYTHON_VALUE_TO_CS_VALUE = { True: 'true', False: 'false', } _BLITTABLE_TYPES = { 'Byte', 'byte', 'SByte', 'sbyte', 'Int16', 'short', 'UInt16', 'ushort', 'Int32', 'int', 'UInt32', 'uint', 'Int64', 'long', 'UInt64', 'ulong', 'IntPtr', 'UIntPtr', 'Single', 'float', 'double', 'void', 'char', } _ALLOWED_FIXED_ARRAYS = { 'bool', 'byte', 'short', 'int', 'long', 'char', 'sbyte', 'ushort', 'uint', 'ulong', 'float', 'double', } _FUNCTION_POINTER_TYPES = { 'mjfItemEnable', } _STRUCT_NAME_OVERRIDES = {} _STUB_STRUCTS = [ '_mjVFS', 'mjuiItemSingle_', 'mjuiItemMulti_', 'mjuiItemSlider_', 'mjuiItemEdit_', ] _OPAQUE_STRUCTS = [ 'mjVFS', 'mjSDF', 'mjTask', 'mjThreadPool', 'mjpDecoder', 'mjpEncoder', 'mjpResourceProvider', 'mjsElement', 'mjString', 'mjStringVec', 'mjIntVec', 'mjIntVecVec', 'mjFloatVec', 'mjFloatVecVec', 'mjDoubleVec', 'mjByteVec', 'mjSpec', 'mjsOrientation', 'mjsPlugin', 'mjsBody', 'mjsFrame', 'mjsJoint', 'mjsGeom', 'mjsSite', 'mjsCamera', 'mjsLight', 'mjsFlex', 'mjsMesh', 'mjsHField', 'mjsSkin', 'mjsTexture', 'mjsMaterial', 'mjsPair', 'mjsExclude', 'mjsEquality', 'mjsTendon', 'mjsWrap', 'mjsActuator', 'mjsSensor', 'mjsNumeric', 'mjsText', 'mjsTuple', 'mjsKey', 'mjsDefault', ] _OMITTED_FUNCTIONS = [ 'mj_loadAllPluginLibraries', 'mjc_distance', 'mjc_getSDF', 'mjc_gradient', 'mjp_', 'mju_decodeResource', 'mju_defaultTask', 'mju_task', 'mju_thread', 'mju_openResource', 'mju_closeResource', 'mju_readResource', 'mju_getResourceDir', 'mju_isModifiedResource', 'mj_compile', 'mj_copyBack', 'mj_copySpec', 'mj_deleteSpec', 'mj_makeSpec', 'mj_parse', 'mj_parseXML', 'mj_parseXMLString', 'mj_recompile', 'mj_saveXML', 'mj_saveXMLString', 'mj_mountVFS', 'mj_unmountVFS', 'mjs_', ] def _mangle_name(name): while name in _CS_RESERVED_KEYWORDS: name += '_' return name def _strip_struct_prefix(name): if name.startswith('struct '): return name[len('struct ') :] return name def _resolve_type_name(raw_name): name = _strip_struct_prefix(raw_name) if name in _FUNCTION_POINTER_TYPES: return 'IntPtr' if name in C_TO_CS_TYPE: return C_TO_CS_TYPE[name] if name in _STRUCT_NAME_OVERRIDES: return _STRUCT_NAME_OVERRIDES[name] if name.endswith('_'): return name return name + '_' def _is_opaque_type(raw_name): name = _strip_struct_prefix(raw_name) bare = name.rstrip('_') return bare in _OPAQUE_STRUCTS def _resolve_cs_type(type_node): if isinstance(type_node, ast_nodes.ValueType): return _resolve_type_name(type_node.name) elif isinstance(type_node, ast_nodes.PointerType): if isinstance( type_node.inner_type, ast_nodes.ValueType ) and _is_opaque_type(type_node.inner_type.name): return 'void*' inner_cs = _resolve_cs_type(type_node.inner_type) return inner_cs + '*' elif isinstance(type_node, ast_nodes.ArrayType): inner_cs = _resolve_cs_type(type_node.inner_type) return inner_cs return 'void' def _get_struct_typename(struct_decl): name = _strip_struct_prefix(struct_decl.declname) if name in _STRUCT_NAME_OVERRIDES: return _STRUCT_NAME_OVERRIDES[name] return name def _is_struct_type(type_name): return type_name.endswith('_') and type_name not in ( set(C_TO_CS_TYPE.values()) | _BLITTABLE_TYPES ) def _is_omitted_struct(struct_name): for omitted in _OPAQUE_STRUCTS: if struct_name.startswith(omitted): return True return False def _is_omitted_function(func_name): for omitted in _OMITTED_FUNCTIONS: if func_name.startswith(omitted): return True return False def _parse_constants_from_headers(header_files): """Parses #define constants from MuJoCo header files.""" constants = {} define_with_value = re.compile( r'^[ \t]*#[ \t]*define[ \t]+(\w+)[ \t]+(\S.+?)(?:[ \t]*//.*)?$', re.MULTILINE, ) define_no_value = re.compile( r'^[ \t]*#[ \t]*define[ \t]+(\w+)[ \t]*$', re.MULTILINE ) for header_file in header_files: try: with open(header_file, 'r') as f: content = f.read() except (IOError, OSError): continue for match in define_no_value.finditer(content): name = match.group(1) if name.startswith('_'): continue constants[name] = True for match in define_with_value.finditer(content): name = match.group(1) value_str = match.group(2).strip() if name.startswith('_'): continue if '(' in name: continue if value_str.startswith('(') and value_str.endswith(')'): value_str = value_str[1:-1].strip() clean_val = value_str.rstrip('UuFf') try: value = float(clean_val) if ( value == int(value) and '.' not in clean_val and 'e' not in clean_val.lower() ): constants[name] = int(value) else: constants[name] = value continue except ValueError: pass try: constants[name] = int(clean_val, 0) continue except ValueError: pass try: evaled = eval(clean_val, {'__builtins__': {}}, constants) # pylint: disable=eval-used if isinstance(evaled, (int, float)): constants[name] = evaled except Exception: # pylint: disable=broad-except pass return constants _COPYRIGHT_HEADER = """// Copyright 2022 DeepMind Technologies Limited // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ============================================================================== // DO NOT EDIT. THIS FILE IS AUTOMATICALLY GENERATED. """ def _write_header(output): """Writes the file header including copyright and imports.""" output.write(_COPYRIGHT_HEADER) output.write( '\n'.join( _BOILERPLATE_IMPORTS + ['namespace Mujoco {', 'public static class MujocoLib {\n'] ) ) def _write_footer(output): output.write('}\n}\n') def _write_constants(output, constants): output.write('\n// ' + '-' * 34 + 'Constants' + '-' * 34 + '\n\n') for name, value in constants.items(): const_type = type(value).__name__ const_type = PYTHON_TYPE_TO_CS_TYPE.get(const_type, const_type) const_value = PYTHON_VALUE_TO_CS_VALUE.get(value, value) if name == 'mjVERSION_HEADER': output.write('// copy' + 'bara:strip_begin\n') output.write(f'public const {const_type} {name} = {const_value};\n') output.write('/* copy' + 'bara:strip_end_and_replace\n') output.write( f'public const {const_type} {name} = __COPYBARA' + '_MUJOCO_VERSION_NUMERIC__;\n' ) output.write('*/\n') else: output.write(f'public const {const_type} {name} = {const_value};\n') output.write('\n') def _write_enums(output): output.write('\n// ' + '-' * 36 + 'Enums' + '-' * 36 + '\n') for enum_name, enum_decl in introspect_enums.ENUMS.items(): output.write(f'public enum {enum_name} : int{{\n') for member_name, member_value in enum_decl.values.items(): output.write(f' {member_name} = {member_value},\n') output.write('}\n') def _field_to_cs_member(field, known_structs): if isinstance(field, ast_nodes.AnonymousUnionDecl): return _anonymous_union_to_cs(field, known_structs) if isinstance(field, ast_nodes.AnonymousStructDecl): return _anonymous_struct_to_cs(field, known_structs) name = _mangle_name(field.name) field_type = field.type if isinstance( field_type, (ast_nodes.AnonymousStructDecl, ast_nodes.AnonymousUnionDecl) ): return f' public {field.name} {name};\n' if isinstance(field_type, ast_nodes.ValueType): cs_type = _resolve_type_name(field_type.name) return f' public {cs_type} {name};\n' elif isinstance(field_type, ast_nodes.ArrayType): inner_type = field_type.inner_type if isinstance(inner_type, ast_nodes.ValueType): cs_type = _resolve_type_name(inner_type.name) elif isinstance(inner_type, ast_nodes.PointerType): cs_type = _resolve_cs_type(inner_type) else: cs_type = 'int' extents = field_type.extents total_size = ' * '.join(str(e) for e in extents) if cs_type in _ALLOWED_FIXED_ARRAYS: return f' public fixed {cs_type} {name}[{total_size}];\n' elif cs_type in known_structs or cs_type in _BLITTABLE_TYPES: result = '' cs_display = cs_type if cs_type != 'void' else 'IntPtr' if len(extents) == 1: for i in range(extents[0]): result += f' public {cs_display} {name}{i};\n' elif len(extents) == 2: for i in range(extents[0]): for j in range(extents[1]): result += f' public {cs_display} {name}{i}_{j};\n' return result else: return f' public fixed {cs_type} {name}[{total_size}];\n' elif isinstance(field_type, ast_nodes.PointerType): inner = field_type.inner_type if isinstance(inner, ast_nodes.ValueType): if _is_opaque_type(inner.name): return f' public void* {name};\n' cs_type = _resolve_type_name(inner.name) if inner.name == 'char' and not field_type.is_const: return f' public char* {name};\n' return f' public {cs_type}* {name};\n' elif isinstance(inner, ast_nodes.PointerType): inner_cs = _resolve_cs_type(inner) return f' public {inner_cs}* {name};\n' else: return f' public void* {name};\n' return f' public int {name};\n' def _anonymous_union_to_cs(union_decl, known_structs): result = '' for sub_field in union_decl.fields: result += _field_to_cs_member(sub_field, known_structs) return result def _anonymous_struct_to_cs(struct_decl, known_structs): result = '' for sub_field in struct_decl.fields: result += _field_to_cs_member(sub_field, known_structs) return result def _emit_anonymous_sub_structs(output, struct_decl, known_structs): """Emits separate struct declarations for anonymous sub-structs.""" for field in struct_decl.fields: if not isinstance(field, ast_nodes.StructFieldDecl): continue if not isinstance( field.type, (ast_nodes.AnonymousStructDecl, ast_nodes.AnonymousUnionDecl), ): continue sub_type_name = field.name output.write('\n[StructLayout(LayoutKind.Sequential)]\n') output.write(f'public unsafe struct {sub_type_name} {{\n') for sub_field in field.type.fields: if isinstance(sub_field, ast_nodes.StructFieldDecl): output.write(_field_to_cs_member(sub_field, known_structs)) elif isinstance( sub_field, (ast_nodes.AnonymousUnionDecl, ast_nodes.AnonymousStructDecl), ): for inner_field in sub_field.fields: output.write(_field_to_cs_member(inner_field, known_structs)) output.write('}\n') known_structs.add(sub_type_name) def _write_structs(output, known_structs): """Writes struct declarations to a file.""" output.write('\n// ' + '-' * 31 + 'struct declarations' + '-' * 27 + '\n') for struct_name, struct_decl in introspect_structs.STRUCTS.items(): if _is_omitted_struct(struct_name): continue typename = _get_struct_typename(struct_decl) _emit_anonymous_sub_structs(output, struct_decl, known_structs) output.write('\n[StructLayout(LayoutKind.Sequential)]\n') output.write(f'public unsafe struct {typename} {{\n') for field in struct_decl.fields: if isinstance(field, ast_nodes.StructFieldDecl): if field.array_extent is not None: cs_type = _resolve_cs_type(field.type) name = _mangle_name(field.name) output.write(f' public {cs_type} {name};\n') else: output.write(_field_to_cs_member(field, known_structs)) elif isinstance( field, (ast_nodes.AnonymousUnionDecl, ast_nodes.AnonymousStructDecl) ): for sub_field in field.fields: output.write(_field_to_cs_member(sub_field, known_structs)) output.write('}\n') known_structs.add(typename) for stub in _STUB_STRUCTS: if stub not in known_structs: output.write(f'\n[StructLayout(LayoutKind.Sequential)]\n') output.write(f'public unsafe struct {stub} {{}}\n') known_structs.add(stub) def _func_param_to_cs(param): param_type = param.type param_name = _mangle_name(param.name) if isinstance(param_type, ast_nodes.ValueType): cs_type = _resolve_type_name(param_type.name) return cs_type, param_name, None elif isinstance(param_type, ast_nodes.PointerType): inner = param_type.inner_type if isinstance(inner, ast_nodes.ValueType): if inner.name == 'char': if param_type.is_const or ( hasattr(inner, 'is_const') and inner.is_const ): return 'string', param_name, 'MarshalAs(UnmanagedType.LPStr)' else: return 'StringBuilder', param_name, None if _is_opaque_type(inner.name): return 'void*', param_name, None cs_type = _resolve_type_name(inner.name) return cs_type + '*', param_name, None elif isinstance(inner, ast_nodes.PointerType): inner_cs = _resolve_cs_type(inner) return inner_cs + '*', param_name, None elif isinstance(inner, ast_nodes.ArrayType): inner_cs = _resolve_cs_type(inner.inner_type) return inner_cs + '*', param_name, None else: return 'void*', param_name, None elif isinstance(param_type, ast_nodes.ArrayType): inner_cs = _resolve_cs_type(param_type.inner_type) return inner_cs + '*', param_name, None return 'int', param_name, None def _func_return_to_cs(return_type): if isinstance(return_type, ast_nodes.ValueType): cs_type = _resolve_type_name(return_type.name) return cs_type, None elif isinstance(return_type, ast_nodes.PointerType): inner = return_type.inner_type if isinstance(inner, ast_nodes.ValueType): if inner.name == 'char': return 'IntPtr', None if _is_opaque_type(inner.name): return 'void*', None cs_type = _resolve_type_name(inner.name) return cs_type + '*', None elif isinstance(inner, ast_nodes.PointerType): inner_cs = _resolve_cs_type(inner) return inner_cs + '*', None return 'void*', None return 'void', None def _write_functions(output): output.write('\n// ' + '-' * 30 + 'Function declarations' + '-' * 26 + '\n') for func_name, func_decl in introspect_functions.FUNCTIONS.items(): if _is_omitted_function(func_name): continue name = _mangle_name(func_name) args = [] for param in func_decl.parameters: cs_type, param_name, annotation = _func_param_to_cs(param) if annotation: args.append(f'[{annotation}]{cs_type} {param_name}') else: args.append(f'{cs_type} {param_name}') args_str = ', '.join(args) return_type, return_annotation = _func_return_to_cs(func_decl.return_type) output.write( '\n[DllImport("mujoco", CallingConvention = CallingConvention.Cdecl)]\n' ) if return_annotation: output.write(f'[{return_annotation}]\n') output.write( f'public static unsafe extern {return_type} {name}({args_str});\n' ) def main(argv): del argv constants = {} if _HEADER_FILES.value: header_files = _HEADER_FILES.value.split() constants = _parse_constants_from_headers(header_files) known_structs = set() for struct_name, struct_decl in introspect_structs.STRUCTS.items(): if not _is_omitted_struct(struct_name): known_structs.add(_get_struct_typename(struct_decl)) output = sys.stdout _write_header(output) if constants: _write_constants(output, constants) _write_enums(output) _write_structs(output, known_structs) _write_functions(output) _write_footer(output) if __name__ == '__main__': app.run(main)