// (c) 2012-2020 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using BuildData = Flax.Build.Builder.BuildData; namespace Flax.Build.Bindings { partial class BindingsGenerator { private static readonly Dictionary CSharpNativeToManagedBasicTypes = new Dictionary() { // Language types { "int8", "sbyte" }, { "int16", "short" }, { "int32", "int" }, { "int64", "long" }, { "uint8", "byte" }, { "byte", "byte" }, { "uint16", "ushort" }, { "uint32", "uint" }, { "uint64", "ulong" }, { "char", "sbyte" }, { "Char", "char" }, { "float", "float" }, { "double", "double" }, }; private static readonly Dictionary CSharpNativeToManagedDefault = new Dictionary() { // Engine types { "String", "string" }, { "StringView", "string" }, { "StringAnsi", "string" }, { "StringAnsiView", "string" }, { "ScriptingObject", "FlaxEngine.Object" }, { "PersistentScriptingObject", "FlaxEngine.Object" }, { "ManagedScriptingObject", "FlaxEngine.Object" }, { "MClass", "System.Type" }, { "Variant", "object" }, { "VariantType", "System.Type" }, // Mono types { "MonoObject", "object" }, { "MonoReflectionType", "System.Type" }, { "MonoType", "IntPtr" }, { "MonoArray", "Array" }, }; private static string GenerateCSharpDefaultValueNativeToManaged(BuildData buildData, string value, ApiTypeInfo caller) { if (string.IsNullOrEmpty(value)) return null; value = value.Replace("::", "."); // Skip constants unsupported in C# var dot = value.LastIndexOf('.'); if (dot != -1) { var type = new TypeInfo { Type = value.Substring(0, dot) }; var apiType = FindApiTypeInfo(buildData, type, caller); if (apiType != null && apiType.IsStruct) return null; } // Convert from C++ to C# switch (value) { case "nullptr": case "NULL": case "String.Empty": case "StringView.Empty": return "null"; case "MAX_int8": return "sbyte.MaxValue"; case "MAX_uint8": return "byte.MaxValue"; case "MAX_int16": return "short.MaxValue"; case "MAX_uint16": return "ushort.MaxValue"; case "MAX_int32": return "int.MaxValue"; case "MAX_uint32": return "uint.MaxValue"; case "MAX_int64": return "long.MaxValue"; case "MAX_uint64": return "ulong.MaxValue"; case "MAX_float": return "float.MaxValue"; case "MAX_double": return "double.MaxValue"; default: return value; } } private static string GenerateCSharpNativeToManaged(BuildData buildData, TypeInfo typeInfo, ApiTypeInfo caller) { string result; // Use dynamic array as wrapper container for fixed-size native arrays if (typeInfo.IsArray) { typeInfo.IsArray = false; result = GenerateCSharpNativeToManaged(buildData, typeInfo, caller); typeInfo.IsArray = true; return result + "[]"; } // Special case for bit-fields if (typeInfo.IsBitField) { if (typeInfo.BitSize == 1) return "bool"; throw new NotImplementedException("TODO: support bit-fields with more than 1 bit."); } // In-build basic if (CSharpNativeToManagedBasicTypes.TryGetValue(typeInfo.Type, out result)) { if (typeInfo.IsPtr) return result + '*'; return result; } // In-build default if (CSharpNativeToManagedDefault.TryGetValue(typeInfo.Type, out result)) { return result; } // ScriptingObjectReference or AssetReference or WeakAssetReference if ((typeInfo.Type == "ScriptingObjectReference" || typeInfo.Type == "AssetReference" || typeInfo.Type == "WeakAssetReference") && typeInfo.GenericArgs != null) return typeInfo.GenericArgs[0].Type.Replace("::", "."); // Array or Span if ((typeInfo.Type == "Array" || typeInfo.Type == "Span") && typeInfo.GenericArgs != null) return GenerateCSharpNativeToManaged(buildData, typeInfo.GenericArgs[0], caller) + "[]"; // Dictionary if (typeInfo.Type == "Dictionary" && typeInfo.GenericArgs != null) return string.Format("System.Collections.Generic.Dictionary<{0}, {1}>", GenerateCSharpNativeToManaged(buildData, typeInfo.GenericArgs[0], caller), GenerateCSharpNativeToManaged(buildData, typeInfo.GenericArgs[1], caller)); // HashSet if (typeInfo.Type == "HashSet" && typeInfo.GenericArgs != null) return string.Format("System.Collections.Generic.HashSet<{0}>", GenerateCSharpNativeToManaged(buildData, typeInfo.GenericArgs[0], caller)); // BitArray if (typeInfo.Type == "BitArray" && typeInfo.GenericArgs != null) return "bool[]"; // BytesContainer if (typeInfo.Type == "BytesContainer" && typeInfo.GenericArgs == null) return "byte[]"; // Find API type info var apiType = FindApiTypeInfo(buildData, typeInfo, caller); if (apiType != null) { if (apiType.IsScriptingObject) return typeInfo.Type.Replace("::", "."); if (typeInfo.IsPtr && (apiType is LangType || apiType.IsPod)) return typeInfo.Type.Replace("::", ".") + '*'; } // Pointer if (typeInfo.IsPtr) return "IntPtr"; return typeInfo.Type.Replace("::", "."); } private static string GenerateCSharpManagedToNativeType(BuildData buildData, TypeInfo typeInfo, ApiTypeInfo caller) { // Fixed-size array if (typeInfo.IsArray) return GenerateCSharpNativeToManaged(buildData, typeInfo, caller); // Find API type info var apiType = FindApiTypeInfo(buildData, typeInfo, caller); if (apiType != null) { if (apiType.IsScriptingObject) return "IntPtr"; } // ScriptingObjectReference or AssetReference or WeakAssetReference if ((typeInfo.Type == "ScriptingObjectReference" || typeInfo.Type == "AssetReference" || typeInfo.Type == "WeakAssetReference") && typeInfo.GenericArgs != null) return "IntPtr"; return GenerateCSharpNativeToManaged(buildData, typeInfo, caller); } private static string GenerateCSharpManagedToNativeConverter(BuildData buildData, TypeInfo typeInfo, ApiTypeInfo caller) { // Use dynamic array as wrapper container for fixed-size native arrays if (typeInfo.IsArray) return string.Empty; // Special case for bit-fields if (typeInfo.IsBitField) { if (typeInfo.BitSize == 1) return string.Empty; throw new NotImplementedException("TODO: support bit-fields with more than 1 bit."); } switch (typeInfo.Type) { case "String": case "StringView": case "StringAnsi": case "StringAnsiView": // String return string.Empty; case "ScriptingObject": case "PersistentScriptingObject": case "ManagedScriptingObject": // object return "FlaxEngine.Object.GetUnmanagedPtr"; default: var apiType = FindApiTypeInfo(buildData, typeInfo, caller); if (apiType != null) { // Scripting Object if (apiType.IsScriptingObject) return "FlaxEngine.Object.GetUnmanagedPtr"; } // ScriptingObjectReference or AssetReference or WeakAssetReference if ((typeInfo.Type == "ScriptingObjectReference" || typeInfo.Type == "AssetReference" || typeInfo.Type == "WeakAssetReference") && typeInfo.GenericArgs != null) return "FlaxEngine.Object.GetUnmanagedPtr"; // Default return string.Empty; } } private static void GenerateCSharpWrapperFunction(BuildData buildData, StringBuilder contents, string indent, ApiTypeInfo caller, FunctionInfo functionInfo) { string returnValueType; if (UsePassByReference(buildData, functionInfo.ReturnType, caller)) { returnValueType = "void"; } else { returnValueType = GenerateCSharpNativeToManaged(buildData, functionInfo.ReturnType, caller); } contents.AppendLine().Append(indent).Append("[MethodImpl(MethodImplOptions.InternalCall)]"); contents.AppendLine().Append(indent).Append("internal static extern "); contents.Append(returnValueType).Append(" Internal_").Append(functionInfo.UniqueName).Append('('); var separator = false; if (!functionInfo.IsStatic) { contents.Append("IntPtr obj"); separator = true; } foreach (var parameterInfo in functionInfo.Parameters) { if (separator) contents.Append(", "); separator = true; var nativeType = GenerateCSharpManagedToNativeType(buildData, parameterInfo.Type, caller); if (parameterInfo.IsOut) contents.Append("out "); else if (parameterInfo.IsRef || UsePassByReference(buildData, parameterInfo.Type, caller)) contents.Append("ref "); contents.Append(nativeType); contents.Append(' '); contents.Append(parameterInfo.Name); } foreach (var parameterInfo in functionInfo.Glue.CustomParameters) { if (separator) contents.Append(", "); separator = true; var nativeType = GenerateCSharpManagedToNativeType(buildData, parameterInfo.Type, caller); if (parameterInfo.IsOut) contents.Append("out "); else if (parameterInfo.IsRef || UsePassByReference(buildData, parameterInfo.Type, caller)) contents.Append("ref "); contents.Append(nativeType); contents.Append(' '); contents.Append(parameterInfo.Name); } contents.Append(')'); contents.Append(';'); contents.AppendLine(); } private static void GenerateCSharpWrapperFunctionCall(BuildData buildData, StringBuilder contents, ApiTypeInfo caller, FunctionInfo functionInfo, bool isSetter = false) { if (functionInfo.Glue.UseReferenceForResult) { } else if (!functionInfo.ReturnType.IsVoid) { contents.Append("return "); } contents.Append("Internal_").Append(functionInfo.UniqueName).Append('('); var separator = false; if (!functionInfo.IsStatic) { contents.Append("__unmanagedPtr"); separator = true; } for (var i = 0; i < functionInfo.Parameters.Count; i++) { var parameterInfo = functionInfo.Parameters[i]; if (separator) contents.Append(", "); separator = true; var convertFunc = GenerateCSharpManagedToNativeConverter(buildData, parameterInfo.Type, caller); if (string.IsNullOrWhiteSpace(convertFunc)) { if (parameterInfo.IsOut) contents.Append("out "); else if (parameterInfo.IsRef || UsePassByReference(buildData, parameterInfo.Type, caller)) contents.Append("ref "); // Pass value contents.Append(isSetter ? "value" : parameterInfo.Name); } else { if (parameterInfo.IsOut) throw new Exception($"Cannot use Out meta on parameter {parameterInfo} in function {functionInfo.Name} in {caller}."); if (parameterInfo.IsRef) throw new Exception($"Cannot use Ref meta on parameter {parameterInfo} in function {functionInfo.Name} in {caller}."); // Convert value contents.Append(convertFunc); contents.Append('('); contents.Append(isSetter ? "value" : parameterInfo.Name); contents.Append(')'); } } foreach (var parameterInfo in functionInfo.Glue.CustomParameters) { if (separator) contents.Append(", "); separator = true; var convertFunc = GenerateCSharpManagedToNativeConverter(buildData, parameterInfo.Type, caller); if (string.IsNullOrWhiteSpace(convertFunc)) { if (parameterInfo.IsOut) contents.Append("out "); else if (parameterInfo.IsRef || UsePassByReference(buildData, parameterInfo.Type, caller)) contents.Append("ref "); // Pass value contents.Append(parameterInfo.DefaultValue); } else { // Convert value contents.Append(convertFunc); contents.Append('('); contents.Append(parameterInfo.DefaultValue); contents.Append(')'); } } contents.Append(");"); if (functionInfo.Glue.UseReferenceForResult) { contents.Append(" return resultAsRef;"); } } private static void GenerateCSharpComment(StringBuilder contents, string indent, string[] comment) { foreach (var c in comment) { contents.Append(indent).Append(c.Replace("::", ".")).AppendLine(); } } private static void GenerateCSharpAttributes(BuildData buildData, StringBuilder contents, string indent, string attributes, string[] comment, bool canUseTooltip, bool useUnmanaged) { var writeTooltip = true; if (!string.IsNullOrEmpty(attributes)) { // Write attributes contents.Append(indent).Append('[').Append(attributes).Append(']').AppendLine(); writeTooltip = !attributes.Contains("Tooltip("); } if (useUnmanaged) { // Write attribute for C++ calling code contents.Append(indent).AppendLine("[Unmanaged]"); } if (canUseTooltip && comment != null && writeTooltip && buildData.Configuration != TargetConfiguration.Release) { // Write documentation comment as tooltip if (comment.Length >= 3 && comment[0] == "/// " && comment[2] == "/// ") { var tooltip = comment[1].Substring(4); if (tooltip.StartsWith("Gets the ")) tooltip = "The " + tooltip.Substring(9); if (tooltip.IndexOf('\"') != -1) tooltip = tooltip.Replace("\"", "\\\""); contents.Append(indent).Append("[Tooltip(\"").Append(tooltip).Append("\")]").AppendLine(); } } } private static void GenerateCSharpAttributes(BuildData buildData, StringBuilder contents, string indent, ApiTypeInfo apiTypeInfo, bool useUnmanaged) { GenerateCSharpAttributes(buildData, contents, indent, apiTypeInfo.Attributes, apiTypeInfo.Comment, true, useUnmanaged); } private static void GenerateCSharpAttributes(BuildData buildData, StringBuilder contents, string indent, MemberInfo memberInfo, bool useUnmanaged) { GenerateCSharpAttributes(buildData, contents, indent, memberInfo.Attributes, memberInfo.Comment, true, useUnmanaged); } private static void GenerateCSharpClass(BuildData buildData, StringBuilder contents, string indent, ClassInfo classInfo) { var useUnmanaged = classInfo.IsStatic || classInfo.IsScriptingObject; contents.AppendLine(); // Namespace begin if (!string.IsNullOrEmpty(classInfo.Namespace)) { contents.AppendFormat("namespace "); contents.AppendLine(classInfo.Namespace); contents.AppendLine("{"); indent += " "; } // Class docs GenerateCSharpComment(contents, indent, classInfo.Comment); // Class begin GenerateCSharpAttributes(buildData, contents, indent, classInfo, useUnmanaged); contents.Append(indent); if (classInfo.Access == AccessLevel.Public) contents.Append("public "); else if (classInfo.Access == AccessLevel.Protected) contents.Append("protected "); else if (classInfo.Access == AccessLevel.Private) contents.Append("private "); if (classInfo.IsStatic) contents.Append("static "); else if (classInfo.IsSealed) contents.Append("sealed "); else if (classInfo.IsAbstract) contents.Append("abstract "); contents.Append("unsafe partial class ").Append(classInfo.Name); if (classInfo.BaseType != null && classInfo.BaseTypeInheritance != AccessLevel.Private) contents.Append(" : ").Append(GenerateCSharpNativeToManaged(buildData, classInfo.BaseType, classInfo)); contents.AppendLine(); contents.Append(indent + "{"); indent += " "; // Constructor for managed objects if (!classInfo.IsStatic && !classInfo.NoConstructor) { contents.AppendLine(); contents.Append(indent).AppendLine("/// "); contents.Append(indent).Append("/// Initializes a new instance of the ."); contents.Append(indent).AppendLine("/// "); contents.Append(indent).Append(classInfo.IsAbstract ? "protected " : "public ").Append(classInfo.Name).AppendLine("() : base()"); contents.Append(indent + "{").AppendLine(); contents.Append(indent + "}").AppendLine(); } // Events foreach (var eventInfo in classInfo.Events) { if (!useUnmanaged) throw new NotImplementedException("TODO: support events inside non-static and non-scripting API class types."); contents.AppendLine(); foreach (var comment in eventInfo.Comment) { if (comment.Contains("/// ")) continue; var c = comment.Replace("::", "."); contents.Append(indent); contents.Append(c); contents.AppendLine(); } GenerateCSharpAttributes(buildData, contents, indent, eventInfo, useUnmanaged); contents.Append(indent); if (eventInfo.Access == AccessLevel.Public) contents.Append("public "); else if (eventInfo.Access == AccessLevel.Protected) contents.Append("protected "); else if (eventInfo.Access == AccessLevel.Private) contents.Append("private "); if (eventInfo.IsStatic) contents.Append("static "); string eventSignature = "event Action"; if (eventInfo.Type.GenericArgs.Count != 0) { eventSignature += '<'; for (var i = 0; i < eventInfo.Type.GenericArgs.Count; i++) { if (i != 0) eventSignature += ", "; CppParamsWrappersCache[i] = GenerateCSharpNativeToManaged(buildData, eventInfo.Type.GenericArgs[i], classInfo); eventSignature += CppParamsWrappersCache[i]; } eventSignature += '>'; } contents.Append(eventSignature); contents.Append(' ').AppendLine(eventInfo.Name); contents.Append(indent).Append('{').AppendLine(); indent += " "; var eventInstance = eventInfo.IsStatic ? string.Empty : "__unmanagedPtr, "; contents.Append(indent).Append($"add {{ Internal_{eventInfo.Name} += value; if (Internal_{eventInfo.Name}_Count++ == 0) Internal_{eventInfo.Name}_Bind({eventInstance}true); }}").AppendLine(); contents.Append(indent).Append($"remove {{ Internal_{eventInfo.Name} -= value; if (--Internal_{eventInfo.Name}_Count == 0) Internal_{eventInfo.Name}_Bind({eventInstance}false); }}").AppendLine(); indent = indent.Substring(0, indent.Length - 4); contents.Append(indent).Append('}').AppendLine(); contents.AppendLine(); contents.Append(indent).Append("private "); if (eventInfo.IsStatic) contents.Append("static "); contents.Append($"int Internal_{eventInfo.Name}_Count;"); contents.AppendLine(); contents.Append("#pragma warning disable 67").AppendLine(); contents.Append(indent).Append("private "); if (eventInfo.IsStatic) contents.Append("static "); contents.Append($"{eventSignature} Internal_{eventInfo.Name};"); contents.AppendLine(); contents.AppendLine(); contents.Append(indent).Append("internal "); if (eventInfo.IsStatic) contents.Append("static "); contents.Append($"void Internal_{eventInfo.Name}_Invoke("); for (var i = 0; i < eventInfo.Type.GenericArgs.Count; i++) { if (i != 0) contents.Append(", "); contents.Append(CppParamsWrappersCache[i]); contents.Append(" arg").Append(i); } contents.Append(')').AppendLine(); contents.Append(indent).Append('{').AppendLine(); contents.Append(indent).Append(" Internal_").Append(eventInfo.Name); contents.Append('('); for (var i = 0; i < eventInfo.Type.GenericArgs.Count; i++) { if (i != 0) contents.Append(", "); contents.Append("arg").Append(i); } contents.Append(");").AppendLine(); contents.Append(indent).Append('}').AppendLine(); contents.AppendLine(); contents.Append(indent).Append("[MethodImpl(MethodImplOptions.InternalCall)]").AppendLine(); contents.Append(indent).Append($"internal static extern void Internal_{eventInfo.Name}_Bind("); if (!eventInfo.IsStatic) contents.Append("IntPtr obj, "); contents.Append("bool bind);"); contents.AppendLine(); } // Fields foreach (var fieldInfo in classInfo.Fields) { if (fieldInfo.Getter == null) continue; contents.AppendLine(); foreach (var comment in fieldInfo.Comment) { if (comment.Contains("/// ")) continue; var c = comment.Replace("::", "."); contents.Append(indent); contents.Append(c); contents.AppendLine(); } GenerateCSharpAttributes(buildData, contents, indent, fieldInfo, useUnmanaged); contents.Append(indent); if (fieldInfo.Access == AccessLevel.Public) contents.Append("public "); else if (fieldInfo.Access == AccessLevel.Protected) contents.Append("protected "); else if (fieldInfo.Access == AccessLevel.Private) contents.Append("private "); if (fieldInfo.IsStatic) contents.Append("static "); var returnValueType = GenerateCSharpNativeToManaged(buildData, fieldInfo.Type, classInfo); contents.Append(returnValueType).Append(' ').Append(fieldInfo.Name); if (!useUnmanaged) { contents.AppendLine(";"); continue; } contents.AppendLine().AppendLine(indent + "{"); indent += " "; contents.Append(indent).Append("get { "); GenerateCSharpWrapperFunctionCall(buildData, contents, classInfo, fieldInfo.Getter); contents.Append(" }").AppendLine(); if (!fieldInfo.IsReadOnly) { contents.Append(indent).Append("set { "); GenerateCSharpWrapperFunctionCall(buildData, contents, classInfo, fieldInfo.Setter, useUnmanaged); contents.Append(" }").AppendLine(); } indent = indent.Substring(0, indent.Length - 4); contents.AppendLine(indent + "}"); GenerateCSharpWrapperFunction(buildData, contents, indent, classInfo, fieldInfo.Getter); if (!fieldInfo.IsReadOnly) GenerateCSharpWrapperFunction(buildData, contents, indent, classInfo, fieldInfo.Setter); } // Properties foreach (var propertyInfo in classInfo.Properties) { if (!useUnmanaged) throw new NotImplementedException("TODO: support properties inside non-static and non-scripting API class types."); contents.AppendLine(); foreach (var comment in propertyInfo.Comment) { if (comment.Contains("/// ") || comment.Contains(" binaryModule) { var contents = new StringBuilder(); var binaryModuleName = binaryModule.Key; var project = Builder.GetModuleProject(binaryModule.First(), buildData); // Generate binary module ID GuidInterop idInterop; idInterop.A = binaryModuleName.GetHashCode(); idInterop.B = project.Name.GetHashCode(); idInterop.C = project.Company.GetHashCode(); idInterop.D = project.Copyright.GetHashCode(); var id = *(Guid*)&idInterop; // Generate C# binary module information var dstFilePath = Path.Combine(project.ProjectFolderPath, "Source", binaryModuleName + ".Gen.cs"); contents.AppendLine("// This code was auto-generated. Do not modify it."); contents.AppendLine(); contents.AppendLine("using System.Reflection;"); contents.AppendLine("using System.Runtime.InteropServices;"); contents.AppendLine(); contents.AppendLine($"[assembly: AssemblyTitle(\"{binaryModuleName}\")]"); contents.AppendLine("[assembly: AssemblyDescription(\"\")]"); contents.AppendLine("[assembly: AssemblyConfiguration(\"\")]"); contents.AppendLine($"[assembly: AssemblyCompany(\"{project.Company}\")]"); contents.AppendLine("[assembly: AssemblyProduct(\"FlaxEngine\")]"); contents.AppendLine($"[assembly: AssemblyCopyright(\"{project.Copyright}\")]"); contents.AppendLine("[assembly: AssemblyTrademark(\"\")]"); contents.AppendLine("[assembly: AssemblyCulture(\"\")]"); contents.AppendLine("[assembly: ComVisible(false)]"); contents.AppendLine($"[assembly: Guid(\"{id:d}\")]"); contents.AppendLine($"[assembly: AssemblyVersion(\"{project.Version}\")]"); contents.AppendLine($"[assembly: AssemblyFileVersion(\"{project.Version}\")]"); Utilities.WriteFileIfChanged(dstFilePath, contents.ToString()); } } }