diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs index c5c58c318..66f93fde3 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Parsing.cs @@ -17,6 +17,7 @@ namespace Flax.Build.Bindings public Stack ScopeTypeStack; public Stack ScopeAccessStack; public Dictionary PreprocessorDefines; + public List StringCache; public ApiTypeInfo ValidScopeInfoFromStack { @@ -46,14 +47,12 @@ namespace Flax.Build.Bindings } } - private static List _commentCache; - private static string[] ParseComment(ref ParsingContext context) { - if (_commentCache == null) - _commentCache = new List(); + if (context.StringCache == null) + context.StringCache = new List(); else - _commentCache.Clear(); + context.StringCache.Clear(); int tokensCount = 0; bool isValid = true; @@ -77,7 +76,7 @@ namespace Flax.Build.Bindings if (commentLine.StartsWith("// ")) commentLine = "/// " + commentLine.Substring(3); - _commentCache.Insert(0, commentLine); + context.StringCache.Insert(0, commentLine); break; } default: @@ -90,14 +89,14 @@ namespace Flax.Build.Bindings for (var i = 0; i < tokensCount; i++) context.Tokenizer.NextToken(true, true); - if (_commentCache.Count == 1) + if (context.StringCache.Count == 1) { // Ensure to have summary begin/end pair - _commentCache.Insert(0, "/// "); - _commentCache.Add("/// "); + context.StringCache.Insert(0, "/// "); + context.StringCache.Add("/// "); } - return _commentCache.ToArray(); + return context.StringCache.ToArray(); } private struct TagParameter diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.cs index 93fe10c0b..1b12cb838 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using Flax.Build.NativeCpp; using BuildData = Flax.Build.Builder.BuildData; @@ -98,324 +100,327 @@ namespace Flax.Build.Bindings } } - // TODO: use async tasks or thread pool to load and parse multiple header files at once using multi-threading - - // Find and load files with API tags - string[] headerFilesContents = null; - using (new ProfileEventScope("LoadHeaderFiles")) + // Parse bindings + Log.Verbose($"Parsing API bindings for {module.Name} ({moduleInfo.Name})"); + int concurrency = Math.Min(Math.Max(1, (int)(Environment.ProcessorCount * Configuration.ConcurrencyProcessorScale)), Configuration.MaxConcurrency); + concurrency = 1; // Disable concurrency for parsing (the gain is unnoticeable or even worse in some cases) + if (concurrency == 1 || headerFiles.Count < 2 * concurrency) { + // Single-threaded for (int i = 0; i < headerFiles.Count; i++) { - var contents = File.ReadAllText(headerFiles[i]); - for (int j = 0; j < ApiTokens.SearchTags.Length; j++) + using (new ProfileEventScope(Path.GetFileName(headerFiles[i]))) { - if (contents.Contains(ApiTokens.SearchTags[j])) - { - if (headerFilesContents == null) - headerFilesContents = new string[headerFiles.Count]; - headerFilesContents[i] = contents; - break; - } + ParseModuleInnerAsync(moduleInfo, moduleOptions, headerFiles, i); } } } - if (headerFilesContents == null) - return moduleInfo; - - // Skip if none of the headers was modified after last time generated C++ file was edited - // TODO: skip parsing if module has API cache file -> then load it from disk - /*if (!forceGenerate) + else { - var lastGenerateTime = File.GetLastWriteTime(bindings.GeneratedCppFilePath); - var anyModified = false; - for (int i = 0; i < headerFiles.Count; i++) + // Sort by file size to improve performance (parse larger files first) + headerFiles.Sort((a, b) => new System.IO.FileInfo(b).Length.CompareTo(new System.IO.FileInfo(a).Length)); + + // Multi-threaded + ThreadPool.GetMinThreads(out var workerThreads, out var completionPortThreads); + if (workerThreads != concurrency) + ThreadPool.SetMaxThreads(concurrency, completionPortThreads); + Parallel.For(0, headerFiles.Count, (i, state) => { - if (File.GetLastWriteTime(headerFiles[i]) > lastGenerateTime) + using (new ProfileEventScope(Path.GetFileName(headerFiles[i]))) { - anyModified = true; - break; + ParseModuleInnerAsync(moduleInfo, moduleOptions, headerFiles, i); } - } - - if (!anyModified) - return; - }*/ - - Log.Verbose($"Parsing API bindings for {module.Name} ({moduleInfo.Name})"); - - // Process all header files to generate the module API code reflection - var context = new ParsingContext - { - CurrentAccessLevel = AccessLevel.Public, - ScopeTypeStack = new Stack(), - ScopeAccessStack = new Stack(), - PreprocessorDefines = new Dictionary(), - }; - for (int i = 0; i < headerFiles.Count; i++) - { - if (headerFilesContents[i] == null) - continue; - var fileInfo = new FileInfo - { - Parent = null, - Children = new List(), - Name = headerFiles[i], - Namespace = moduleInfo.Name, - }; - moduleInfo.AddChild(fileInfo); - - try - { - // Tokenize the source - var tokenizer = new Tokenizer(); - tokenizer.Tokenize(headerFilesContents[i]); - - // Init the context - context.Tokenizer = tokenizer; - context.File = fileInfo; - context.ScopeInfo = null; - context.ScopeTypeStack.Clear(); - context.ScopeAccessStack.Clear(); - context.PreprocessorDefines.Clear(); - context.EnterScope(fileInfo); - - // Process the source code - ApiTypeInfo scopeType = null; - Token prevToken = null; - while (true) - { - // Move to the next token - var token = tokenizer.NextToken(); - if (token == null) - continue; - if (token.Type == TokenType.EndOfFile) - break; - - // Parse API_.. tags in source code - if (token.Type == TokenType.Identifier && token.Value.StartsWith("API_", StringComparison.Ordinal)) - { - if (string.Equals(token.Value, ApiTokens.Class, StringComparison.Ordinal)) - { - if (!(context.ScopeInfo is FileInfo)) - throw new NotImplementedException("TODO: add support for nested classes in scripting API"); - - var classInfo = ParseClass(ref context); - scopeType = classInfo; - context.ScopeInfo.AddChild(scopeType); - context.CurrentAccessLevel = AccessLevel.Public; - } - else if (string.Equals(token.Value, ApiTokens.Property, StringComparison.Ordinal)) - { - var propertyInfo = ParseProperty(ref context); - } - else if (string.Equals(token.Value, ApiTokens.Function, StringComparison.Ordinal)) - { - var functionInfo = ParseFunction(ref context); - - if (context.ScopeInfo is ClassInfo classInfo) - classInfo.Functions.Add(functionInfo); - else if (context.ScopeInfo is StructureInfo structureInfo) - structureInfo.Functions.Add(functionInfo); - else - throw new Exception($"Not supported free-function {functionInfo.Name} at line {tokenizer.CurrentLine}. Place it in the class to use API bindings for it."); - } - else if (string.Equals(token.Value, ApiTokens.Enum, StringComparison.Ordinal)) - { - var enumInfo = ParseEnum(ref context); - context.ScopeInfo.AddChild(enumInfo); - } - else if (string.Equals(token.Value, ApiTokens.Struct, StringComparison.Ordinal)) - { - var structureInfo = ParseStructure(ref context); - scopeType = structureInfo; - context.ScopeInfo.AddChild(scopeType); - context.CurrentAccessLevel = AccessLevel.Public; - } - else if (string.Equals(token.Value, ApiTokens.Field, StringComparison.Ordinal)) - { - var fieldInfo = ParseField(ref context); - var scopeInfo = context.ValidScopeInfoFromStack; - - if (scopeInfo is ClassInfo classInfo) - classInfo.Fields.Add(fieldInfo); - else if (scopeInfo is StructureInfo structureInfo) - structureInfo.Fields.Add(fieldInfo); - else - throw new Exception($"Not supported location for field {fieldInfo.Name} at line {tokenizer.CurrentLine}. Place it in the class or structure to use API bindings for it."); - } - else if (string.Equals(token.Value, ApiTokens.Event, StringComparison.Ordinal)) - { - var eventInfo = ParseEvent(ref context); - var scopeInfo = context.ValidScopeInfoFromStack; - - if (scopeInfo is ClassInfo classInfo) - classInfo.Events.Add(eventInfo); - else - throw new Exception($"Not supported location for event {eventInfo.Name} at line {tokenizer.CurrentLine}. Place it in the class to use API bindings for it."); - } - else if (string.Equals(token.Value, ApiTokens.InjectCppCode, StringComparison.Ordinal)) - { - var injectCppCodeInfo = ParseInjectCppCode(ref context); - fileInfo.AddChild(injectCppCodeInfo); - } - else if (string.Equals(token.Value, ApiTokens.Interface, StringComparison.Ordinal)) - { - if (!(context.ScopeInfo is FileInfo)) - throw new NotImplementedException("TODO: add support for nested interfaces in scripting API"); - - var interfaceInfo = ParseInterface(ref context); - scopeType = interfaceInfo; - context.ScopeInfo.AddChild(scopeType); - context.CurrentAccessLevel = AccessLevel.Public; - } - else if (string.Equals(token.Value, ApiTokens.AutoSerialization, StringComparison.Ordinal)) - { - if (context.ScopeInfo is ClassInfo classInfo) - classInfo.IsAutoSerialization = true; - else if (context.ScopeInfo is StructureInfo structureInfo) - structureInfo.IsAutoSerialization = true; - else - throw new Exception($"Not supported location for {ApiTokens.AutoSerialization} at line {tokenizer.CurrentLine}. Place it in the class or structure that uses API bindings."); - } - } - - // Track access level inside class - if (context.ScopeInfo != null && token.Type == TokenType.Colon && prevToken != null && prevToken.Type == TokenType.Identifier) - { - if (string.Equals(prevToken.Value, "public", StringComparison.Ordinal)) - { - context.CurrentAccessLevel = AccessLevel.Public; - } - else if (string.Equals(prevToken.Value, "protected", StringComparison.Ordinal)) - { - context.CurrentAccessLevel = AccessLevel.Protected; - } - else if (string.Equals(prevToken.Value, "private", StringComparison.Ordinal)) - { - context.CurrentAccessLevel = AccessLevel.Private; - } - } - - // Handle preprocessor blocks - if (token.Type == TokenType.Preprocessor) - { - token = tokenizer.NextToken(); - switch (token.Value) - { - case "define": - { - token = tokenizer.NextToken(); - var name = token.Value; - var value = string.Empty; - token = tokenizer.NextToken(true); - while (token.Type != TokenType.Newline) - { - value += token.Value; - token = tokenizer.NextToken(true); - } - value = value.Trim(); - context.PreprocessorDefines[name] = value; - break; - } - case "if": - { - // Parse condition - var condition = string.Empty; - token = tokenizer.NextToken(true); - while (token.Type != TokenType.Newline) - { - var tokenValue = token.Value.Trim(); - if (tokenValue.Length == 0) - { - token = tokenizer.NextToken(true); - continue; - } - - // Very simple defines processing - tokenValue = ReplacePreProcessorDefines(tokenValue, context.PreprocessorDefines.Keys); - tokenValue = ReplacePreProcessorDefines(tokenValue, moduleOptions.PublicDefinitions); - tokenValue = ReplacePreProcessorDefines(tokenValue, moduleOptions.PrivateDefinitions); - tokenValue = ReplacePreProcessorDefines(tokenValue, moduleOptions.CompileEnv.PreprocessorDefinitions); - tokenValue = tokenValue.Replace("false", "0"); - tokenValue = tokenValue.Replace("true", "1"); - tokenValue = tokenValue.Replace("||", "|"); - if (tokenValue.Length != 0 && tokenValue != "1" && tokenValue != "0" && tokenValue != "|") - tokenValue = "0"; - - condition += tokenValue; - token = tokenizer.NextToken(true); - } - - // Filter condition - condition = condition.Replace("1|1", "1"); - condition = condition.Replace("1|0", "1"); - condition = condition.Replace("0|1", "1"); - - // Skip chunk of code of condition fails - if (condition != "1") - { - ParsePreprocessorIf(fileInfo, tokenizer, ref token); - } - - break; - } - case "ifdef": - { - // Parse condition - var define = string.Empty; - token = tokenizer.NextToken(true); - while (token.Type != TokenType.Newline) - { - define += token.Value; - token = tokenizer.NextToken(true); - } - - // Check condition - define = define.Trim(); - if (!context.PreprocessorDefines.ContainsKey(define) && !moduleOptions.CompileEnv.PreprocessorDefinitions.Contains(define)) - { - ParsePreprocessorIf(fileInfo, tokenizer, ref token); - } - - break; - } - } - } - - // Scope tracking - if (token.Type == TokenType.LeftCurlyBrace) - { - context.ScopeTypeStack.Push(scopeType); - context.ScopeInfo = context.ScopeTypeStack.Peek(); - scopeType = null; - } - else if (token.Type == TokenType.RightCurlyBrace) - { - context.ScopeTypeStack.Pop(); - if (context.ScopeTypeStack.Count == 0) - throw new Exception($"Mismatch of the {{}} braces pair in file '{fileInfo.Name}' at line {tokenizer.CurrentLine}."); - context.ScopeInfo = context.ScopeTypeStack.Peek(); - if (context.ScopeInfo is FileInfo) - context.CurrentAccessLevel = AccessLevel.Public; - } - - prevToken = token; - } - } - catch (Exception ex) - { - Log.Error($"Failed to parse '{fileInfo.Name}' file to generate bindings."); - Log.Exception(ex); - throw; - } + }); } // Initialize API - moduleInfo.Init(buildData); + using (new ProfileEventScope("Init")) + { + moduleInfo.Init(buildData); + } return moduleInfo; } + private static void ParseModuleInnerAsync(ModuleInfo moduleInfo, BuildOptions moduleOptions, List headerFiles, int workIndex) + { + // Find and load files with API tags + bool hasApi = false; + string headerFileContents = File.ReadAllText(headerFiles[workIndex]); + for (int j = 0; j < ApiTokens.SearchTags.Length; j++) + { + if (headerFileContents.Contains(ApiTokens.SearchTags[j])) + { + hasApi = true; + break; + } + } + if (!hasApi) + return; + + // Process header file to generate the module API code reflection + var fileInfo = new FileInfo + { + Parent = null, + Children = new List(), + Name = headerFiles[workIndex], + Namespace = moduleInfo.Name, + }; + lock (moduleInfo) + { + moduleInfo.AddChild(fileInfo); + } + + try + { + // Tokenize the source + var tokenizer = new Tokenizer(); + tokenizer.Tokenize(headerFileContents); + + // Init the context + var context = new ParsingContext + { + File = fileInfo, + Tokenizer = tokenizer, + ScopeInfo = null, + CurrentAccessLevel = AccessLevel.Public, + ScopeTypeStack = new Stack(), + ScopeAccessStack = new Stack(), + PreprocessorDefines = new Dictionary(), + }; + context.EnterScope(fileInfo); + + // Process the source code + ApiTypeInfo scopeType = null; + Token prevToken = null; + while (true) + { + // Move to the next token + var token = tokenizer.NextToken(); + if (token == null) + continue; + if (token.Type == TokenType.EndOfFile) + break; + + // Parse API_.. tags in source code + if (token.Type == TokenType.Identifier && token.Value.StartsWith("API_", StringComparison.Ordinal)) + { + if (string.Equals(token.Value, ApiTokens.Class, StringComparison.Ordinal)) + { + if (!(context.ScopeInfo is FileInfo)) + throw new NotImplementedException("TODO: add support for nested classes in scripting API"); + + var classInfo = ParseClass(ref context); + scopeType = classInfo; + context.ScopeInfo.AddChild(scopeType); + context.CurrentAccessLevel = AccessLevel.Public; + } + else if (string.Equals(token.Value, ApiTokens.Property, StringComparison.Ordinal)) + { + var propertyInfo = ParseProperty(ref context); + } + else if (string.Equals(token.Value, ApiTokens.Function, StringComparison.Ordinal)) + { + var functionInfo = ParseFunction(ref context); + + if (context.ScopeInfo is ClassInfo classInfo) + classInfo.Functions.Add(functionInfo); + else if (context.ScopeInfo is StructureInfo structureInfo) + structureInfo.Functions.Add(functionInfo); + else + throw new Exception($"Not supported free-function {functionInfo.Name} at line {tokenizer.CurrentLine}. Place it in the class to use API bindings for it."); + } + else if (string.Equals(token.Value, ApiTokens.Enum, StringComparison.Ordinal)) + { + var enumInfo = ParseEnum(ref context); + context.ScopeInfo.AddChild(enumInfo); + } + else if (string.Equals(token.Value, ApiTokens.Struct, StringComparison.Ordinal)) + { + var structureInfo = ParseStructure(ref context); + scopeType = structureInfo; + context.ScopeInfo.AddChild(scopeType); + context.CurrentAccessLevel = AccessLevel.Public; + } + else if (string.Equals(token.Value, ApiTokens.Field, StringComparison.Ordinal)) + { + var fieldInfo = ParseField(ref context); + var scopeInfo = context.ValidScopeInfoFromStack; + + if (scopeInfo is ClassInfo classInfo) + classInfo.Fields.Add(fieldInfo); + else if (scopeInfo is StructureInfo structureInfo) + structureInfo.Fields.Add(fieldInfo); + else + throw new Exception($"Not supported location for field {fieldInfo.Name} at line {tokenizer.CurrentLine}. Place it in the class or structure to use API bindings for it."); + } + else if (string.Equals(token.Value, ApiTokens.Event, StringComparison.Ordinal)) + { + var eventInfo = ParseEvent(ref context); + var scopeInfo = context.ValidScopeInfoFromStack; + + if (scopeInfo is ClassInfo classInfo) + classInfo.Events.Add(eventInfo); + else + throw new Exception($"Not supported location for event {eventInfo.Name} at line {tokenizer.CurrentLine}. Place it in the class to use API bindings for it."); + } + else if (string.Equals(token.Value, ApiTokens.InjectCppCode, StringComparison.Ordinal)) + { + var injectCppCodeInfo = ParseInjectCppCode(ref context); + fileInfo.AddChild(injectCppCodeInfo); + } + else if (string.Equals(token.Value, ApiTokens.Interface, StringComparison.Ordinal)) + { + if (!(context.ScopeInfo is FileInfo)) + throw new NotImplementedException("TODO: add support for nested interfaces in scripting API"); + + var interfaceInfo = ParseInterface(ref context); + scopeType = interfaceInfo; + context.ScopeInfo.AddChild(scopeType); + context.CurrentAccessLevel = AccessLevel.Public; + } + else if (string.Equals(token.Value, ApiTokens.AutoSerialization, StringComparison.Ordinal)) + { + if (context.ScopeInfo is ClassInfo classInfo) + classInfo.IsAutoSerialization = true; + else if (context.ScopeInfo is StructureInfo structureInfo) + structureInfo.IsAutoSerialization = true; + else + throw new Exception($"Not supported location for {ApiTokens.AutoSerialization} at line {tokenizer.CurrentLine}. Place it in the class or structure that uses API bindings."); + } + } + + // Track access level inside class + if (context.ScopeInfo != null && token.Type == TokenType.Colon && prevToken != null && prevToken.Type == TokenType.Identifier) + { + if (string.Equals(prevToken.Value, "public", StringComparison.Ordinal)) + { + context.CurrentAccessLevel = AccessLevel.Public; + } + else if (string.Equals(prevToken.Value, "protected", StringComparison.Ordinal)) + { + context.CurrentAccessLevel = AccessLevel.Protected; + } + else if (string.Equals(prevToken.Value, "private", StringComparison.Ordinal)) + { + context.CurrentAccessLevel = AccessLevel.Private; + } + } + + // Handle preprocessor blocks + if (token.Type == TokenType.Preprocessor) + { + token = tokenizer.NextToken(); + switch (token.Value) + { + case "define": + { + token = tokenizer.NextToken(); + var name = token.Value; + var value = string.Empty; + token = tokenizer.NextToken(true); + while (token.Type != TokenType.Newline) + { + value += token.Value; + token = tokenizer.NextToken(true); + } + value = value.Trim(); + context.PreprocessorDefines[name] = value; + break; + } + case "if": + { + // Parse condition + var condition = string.Empty; + token = tokenizer.NextToken(true); + while (token.Type != TokenType.Newline) + { + var tokenValue = token.Value.Trim(); + if (tokenValue.Length == 0) + { + token = tokenizer.NextToken(true); + continue; + } + + // Very simple defines processing + tokenValue = ReplacePreProcessorDefines(tokenValue, context.PreprocessorDefines.Keys); + tokenValue = ReplacePreProcessorDefines(tokenValue, moduleOptions.PublicDefinitions); + tokenValue = ReplacePreProcessorDefines(tokenValue, moduleOptions.PrivateDefinitions); + tokenValue = ReplacePreProcessorDefines(tokenValue, moduleOptions.CompileEnv.PreprocessorDefinitions); + tokenValue = tokenValue.Replace("false", "0"); + tokenValue = tokenValue.Replace("true", "1"); + tokenValue = tokenValue.Replace("||", "|"); + if (tokenValue.Length != 0 && tokenValue != "1" && tokenValue != "0" && tokenValue != "|") + tokenValue = "0"; + + condition += tokenValue; + token = tokenizer.NextToken(true); + } + + // Filter condition + condition = condition.Replace("1|1", "1"); + condition = condition.Replace("1|0", "1"); + condition = condition.Replace("0|1", "1"); + + // Skip chunk of code of condition fails + if (condition != "1") + { + ParsePreprocessorIf(fileInfo, tokenizer, ref token); + } + + break; + } + case "ifdef": + { + // Parse condition + var define = string.Empty; + token = tokenizer.NextToken(true); + while (token.Type != TokenType.Newline) + { + define += token.Value; + token = tokenizer.NextToken(true); + } + + // Check condition + define = define.Trim(); + if (!context.PreprocessorDefines.ContainsKey(define) && !moduleOptions.CompileEnv.PreprocessorDefinitions.Contains(define)) + { + ParsePreprocessorIf(fileInfo, tokenizer, ref token); + } + + break; + } + } + } + + // Scope tracking + if (token.Type == TokenType.LeftCurlyBrace) + { + context.ScopeTypeStack.Push(scopeType); + context.ScopeInfo = context.ScopeTypeStack.Peek(); + scopeType = null; + } + else if (token.Type == TokenType.RightCurlyBrace) + { + context.ScopeTypeStack.Pop(); + if (context.ScopeTypeStack.Count == 0) + throw new Exception($"Mismatch of the {{}} braces pair in file '{fileInfo.Name}' at line {tokenizer.CurrentLine}."); + context.ScopeInfo = context.ScopeTypeStack.Peek(); + if (context.ScopeInfo is FileInfo) + context.CurrentAccessLevel = AccessLevel.Public; + } + + prevToken = token; + } + } + catch (Exception ex) + { + Log.Error($"Failed to parse '{fileInfo.Name}' file to generate bindings."); + Log.Exception(ex); + throw; + } + } + private static string ReplacePreProcessorDefines(string text, IEnumerable defines) { foreach (var define in defines) @@ -594,7 +599,6 @@ namespace Flax.Build.Bindings { foreach (var fieldInfo in structureInfo.Fields) { - // TODO: support bit-fields in structure fields if (fieldInfo.Type.IsBitField) throw new NotImplementedException($"TODO: support bit-fields in structure fields (found field {fieldInfo} in structure {structureInfo.Name})"); diff --git a/Source/Tools/Flax.Build/Bindings/FileInfo.cs b/Source/Tools/Flax.Build/Bindings/FileInfo.cs index 25e1e1944..6c4b4dcfe 100644 --- a/Source/Tools/Flax.Build/Bindings/FileInfo.cs +++ b/Source/Tools/Flax.Build/Bindings/FileInfo.cs @@ -7,7 +7,7 @@ namespace Flax.Build.Bindings /// /// The native file information for bindings generator. /// - public class FileInfo : ApiTypeInfo, IComparable + public class FileInfo : ApiTypeInfo, IComparable, IComparable { public override void AddChild(ApiTypeInfo apiTypeInfo) { @@ -26,5 +26,12 @@ namespace Flax.Build.Bindings { return System.IO.Path.GetFileName(Name); } + + public int CompareTo(object obj) + { + if (obj is ApiTypeInfo apiTypeInfo) + return Name.CompareTo(apiTypeInfo.Name); + return 0; + } } } diff --git a/Source/Tools/Flax.Build/Bindings/ModuleInfo.cs b/Source/Tools/Flax.Build/Bindings/ModuleInfo.cs index 6626145a9..7bb6d1608 100644 --- a/Source/Tools/Flax.Build/Bindings/ModuleInfo.cs +++ b/Source/Tools/Flax.Build/Bindings/ModuleInfo.cs @@ -13,5 +13,14 @@ namespace Flax.Build.Bindings { return "module " + Name; } + + /// + public override void Init(Builder.BuildData buildData) + { + base.Init(buildData); + + // Sort module files to prevent bindings rebuild due to order changes (list might be created in async) + Children.Sort(); + } } }