diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index baa7f2e..2d1b059 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -35,7 +35,7 @@ jobs:
sudo apt-get install gcc-i686-linux-gnu gcc-x86-64-linux-gnu gcc-aarch64-linux-gnu llvm-14 clang-14
- name: "Setup .NET"
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'
@@ -44,7 +44,7 @@ jobs:
dotnet test '${{ github.workspace }}/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/c2ffi.Tests.EndToEnd.Extract.csproj' --nologo --verbosity minimal --configuration Release
- name: "Upload generated FFI files"
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: "ffi-${{ matrix.platform.name }}"
path: |
@@ -63,26 +63,26 @@ jobs:
uses: actions/checkout@v4
- name: "Setup .NET"
- uses: actions/setup-dotnet@v3
+ uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.x'
- name: "Download generated FFI files: windows"
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
name: "ffi-windows"
path: |
${{ github.workspace }}/src/c/tests
- name: "Download generated FFI files: linux"
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
name: "ffi-linux"
path: |
${{ github.workspace }}/src/c/tests
- name: "Download generated FFI files: macos"
- uses: actions/download-artifact@v3
+ uses: actions/download-artifact@v4
with:
name: "ffi-macos"
path: |
@@ -92,3 +92,10 @@ jobs:
run: |
dotnet test '${{ github.workspace }}/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/c2ffi.Tests.EndToEnd.Merge.csproj' --nologo --verbosity minimal --configuration Release
+ - name: "Upload generated FFI files"
+ uses: actions/upload-artifact@v4
+ with:
+ name: "ffi-x"
+ path: |
+ ${{ github.workspace }}/src/c/tests/**/ffi-x/*.json
+
diff --git a/.gitignore b/.gitignore
index 9965f5f..b3433c8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ bin/
obj/
/artifacts/
src/c/tests/**/ffi/*.json
+src/c/tests/**/ffi-x/*.json
diff --git a/src/cs/c2json.sln b/src/cs/c2ffi.sln
similarity index 100%
rename from src/cs/c2json.sln
rename to src/cs/c2ffi.sln
diff --git a/src/cs/production/c2ffi.Data/Nodes/CMacroObject.cs b/src/cs/production/c2ffi.Data/Nodes/CMacroObject.cs
index 62f6687..375b9a3 100644
--- a/src/cs/production/c2ffi.Data/Nodes/CMacroObject.cs
+++ b/src/cs/production/c2ffi.Data/Nodes/CMacroObject.cs
@@ -45,6 +45,16 @@ public override bool Equals(CNode? other)
return TypeInfo.Equals(other2.TypeInfo) && Value == other2.Value;
}
+ public bool EqualsWithoutValue(CMacroObject other)
+ {
+ if (!base.Equals(other))
+ {
+ return false;
+ }
+
+ return TypeInfo.Equals(other.TypeInfo);
+ }
+
///
public override int GetHashCode()
{
diff --git a/src/cs/production/c2ffi.Tool/Commands/Merge/Input/MergeInputSanitizer.cs b/src/cs/production/c2ffi.Tool/Commands/Merge/Input/MergeInputSanitizer.cs
new file mode 100644
index 0000000..f05f0a3
--- /dev/null
+++ b/src/cs/production/c2ffi.Tool/Commands/Merge/Input/MergeInputSanitizer.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information.
+
+using System.Collections.Immutable;
+using System.IO.Abstractions;
+using bottlenoselabs.Common.Tools;
+using c2ffi.Tool.Commands.Merge.Input.Sanitized;
+using c2ffi.Tool.Commands.Merge.Input.Unsanitized;
+
+namespace c2ffi.Tool.Commands.Merge.Input;
+
+public sealed class MergeInputSanitizer
+{
+ private readonly IFileSystem _fileSystem;
+
+ public MergeInputSanitizer(IFileSystem fileSystem)
+ {
+ _fileSystem = fileSystem;
+ }
+
+ public MergeOptions Sanitize(UnsanitizedMergeOptions unsanitizedOptions)
+ {
+ var directoryPath = _fileSystem.Path.GetFullPath(unsanitizedOptions.InputDirectoryPath);
+ if (!_fileSystem.Directory.Exists(directoryPath))
+ {
+ throw new ToolInputSanitizationException($"The directory '{directoryPath}' does not exist.");
+ }
+
+ var filePaths = _fileSystem.Directory.GetFiles(directoryPath, "*.json").ToImmutableArray();
+
+ if (filePaths.IsDefaultOrEmpty)
+ {
+ throw new ToolInputSanitizationException($"The directory '{directoryPath}' does not contain any abstract syntax tree `.json` files.");
+ }
+
+ var outputFilePath = _fileSystem.Path.GetFullPath(unsanitizedOptions.OutputFilePath);
+
+ var result = new MergeOptions
+ {
+ OutputFilePath = outputFilePath,
+ InputFilePaths = filePaths
+ };
+
+ return result;
+ }
+}
diff --git a/src/cs/production/c2ffi.Tool/Commands/Merge/Input/Sanitized/MergeOptions.cs b/src/cs/production/c2ffi.Tool/Commands/Merge/Input/Sanitized/MergeOptions.cs
new file mode 100644
index 0000000..304c73c
--- /dev/null
+++ b/src/cs/production/c2ffi.Tool/Commands/Merge/Input/Sanitized/MergeOptions.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information.
+
+using System.Collections.Immutable;
+
+namespace c2ffi.Tool.Commands.Merge.Input.Sanitized;
+
+public class MergeOptions
+{
+ public ImmutableArray InputFilePaths { get; set; } = ImmutableArray.Empty;
+
+ public string OutputFilePath { get; set; } = string.Empty;
+}
diff --git a/src/cs/production/c2ffi.Tool/Commands/Merge/Input/Unsanitized/UnsanitizedMergeOptions.cs b/src/cs/production/c2ffi.Tool/Commands/Merge/Input/Unsanitized/UnsanitizedMergeOptions.cs
new file mode 100644
index 0000000..1a80806
--- /dev/null
+++ b/src/cs/production/c2ffi.Tool/Commands/Merge/Input/Unsanitized/UnsanitizedMergeOptions.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information.
+
+namespace c2ffi.Tool.Commands.Merge.Input.Unsanitized;
+
+// NOTE: This class is considered un-sanitized input; all strings and other types could be null.
+public class UnsanitizedMergeOptions
+{
+ public string InputDirectoryPath { get; set; } = string.Empty;
+
+ public string OutputFilePath { get; set; } = string.Empty;
+}
diff --git a/src/cs/production/c2ffi.Tool/Commands/Merge/MergeFfisCommand.cs b/src/cs/production/c2ffi.Tool/Commands/Merge/MergeFfisCommand.cs
index c4923ac..bd01586 100644
--- a/src/cs/production/c2ffi.Tool/Commands/Merge/MergeFfisCommand.cs
+++ b/src/cs/production/c2ffi.Tool/Commands/Merge/MergeFfisCommand.cs
@@ -2,23 +2,26 @@
// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information.
using System.CommandLine;
-using JetBrains.Annotations;
+using c2ffi.Tool.Commands.Merge.Input.Unsanitized;
namespace c2ffi.Tool.Commands.Merge;
-[UsedImplicitly]
-public class MergeFfisCommand : Command
+public sealed class MergeFfisCommand : Command
{
- public MergeFfisCommand()
+ private readonly MergeFfisTool _tool;
+
+ public MergeFfisCommand(MergeFfisTool tool)
: base(
"merge",
"Merge multiple target platform FFI (foreign function interface) `.json` files into a cross-platform FFI `.json` file.")
{
+ _tool = tool;
+
var directoryOption = new Option(
"--inputDirectoryPath", "The input directory where the multiple target platform FFI (foreign function interface) `.json` files are located.")
- {
- IsRequired = true
- };
+ {
+ IsRequired = true
+ };
AddOption(directoryOption);
var fileOption = new Option(
@@ -30,6 +33,6 @@ public MergeFfisCommand()
private void Main(string inputDirectoryPath, string outputFilePath)
{
- Console.WriteLine("Merge!");
+ _tool.Run(inputDirectoryPath, outputFilePath);
}
}
diff --git a/src/cs/production/c2ffi.Tool/Commands/Merge/MergeFfisTool.cs b/src/cs/production/c2ffi.Tool/Commands/Merge/MergeFfisTool.cs
new file mode 100644
index 0000000..8242676
--- /dev/null
+++ b/src/cs/production/c2ffi.Tool/Commands/Merge/MergeFfisTool.cs
@@ -0,0 +1,374 @@
+// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information.
+
+using System.Collections.Immutable;
+using System.IO.Abstractions;
+using c2ffi.Data;
+using c2ffi.Data.Nodes;
+using c2ffi.Data.Serialization;
+using c2ffi.Tool.Commands.Merge.Input;
+using c2ffi.Tool.Commands.Merge.Input.Sanitized;
+using c2ffi.Tool.Commands.Merge.Input.Unsanitized;
+using Microsoft.Extensions.Logging;
+
+namespace c2ffi.Tool.Commands.Merge;
+
+public sealed partial class MergeFfisTool
+{
+ private readonly ILogger _logger;
+ private readonly IFileSystem _fileSystem;
+ private readonly MergeInputSanitizer _mergeInputSanitizer;
+
+ private readonly List _enums = new();
+ private readonly List _variables = new();
+ private readonly List _opaqueTypes = new();
+ private readonly List _functions = new();
+ private readonly List _records = new();
+ private readonly List _functionPointers = new();
+ private readonly List _macroObjects = new();
+ private readonly List _typeAliases = new();
+ private readonly List _enumConstants = new();
+
+ private sealed class CNodeWithTargetPlatform
+ {
+ public readonly CNode Node;
+ public readonly TargetPlatform TargetPlatform;
+
+ public CNodeWithTargetPlatform(CNode node, TargetPlatform targetPlatform)
+ {
+ Node = node;
+ TargetPlatform = targetPlatform;
+ }
+ }
+
+ public MergeFfisTool(
+ ILogger logger,
+ IFileSystem fileSystem,
+ MergeInputSanitizer mergeInputSanitizer)
+ {
+ _logger = logger;
+ _fileSystem = fileSystem;
+ _mergeInputSanitizer = mergeInputSanitizer;
+ }
+
+ public void Run(string inputDirectoryPath, string outputFilePath)
+ {
+ var options = GetOptions(inputDirectoryPath, outputFilePath);
+ var platformFfis =
+ GetPlatformFfis(options.InputFilePaths);
+ var platforms = platformFfis.
+ Select(x => x.PlatformRequested).ToImmutableArray();
+ var platformNodesByName = GetPlatformNodesByName(platformFfis);
+ var ffi = CreateCrossPlatformFfi(platforms, platformNodesByName);
+
+ Json.WriteFfiCrossPlatform(_fileSystem, options.OutputFilePath, ffi);
+ LogWriteAbstractSyntaxTreeSuccess(string.Join(", ", platforms), options.OutputFilePath);
+ }
+
+ private MergeOptions GetOptions(string inputDirectoryPath, string outputFilePath)
+ {
+ var unsanitizedOptions = new UnsanitizedMergeOptions
+ {
+ InputDirectoryPath = inputDirectoryPath,
+ OutputFilePath = outputFilePath
+ };
+ return _mergeInputSanitizer.Sanitize(unsanitizedOptions);
+ }
+
+ private CFfiCrossPlatform CreateCrossPlatformFfi(
+ ImmutableArray platforms,
+ ImmutableSortedDictionary> platformNodesByName)
+ {
+ var result = new CFfiCrossPlatform();
+
+ foreach (var (name, nodes) in platformNodesByName)
+ {
+ BuildCrossPlatformNodes(platforms, nodes, name);
+ }
+
+ result.Platforms = platforms.Sort(
+ (a, b) =>
+ string.Compare(a.ClangTargetTriple, b.ClangTargetTriple, StringComparison.Ordinal));
+ result.Enums = _enums.ToImmutableDictionary(x => x.Name);
+ result.Variables = _variables.ToImmutableDictionary(x => x.Name);
+ result.OpaqueTypes = _opaqueTypes.ToImmutableDictionary(x => x.Name);
+ result.Functions = _functions.ToImmutableDictionary(x => x.Name);
+ result.Records = _records.ToImmutableDictionary(x => x.Name);
+ result.FunctionPointers = _functionPointers.ToImmutableDictionary(x => x.Name);
+ result.MacroObjects = _macroObjects.ToImmutableDictionary(x => x.Name);
+ result.TypeAliases = _typeAliases.ToImmutableDictionary(x => x.Name);
+ result.EnumConstants = _enumConstants.ToImmutableDictionary(x => x.Name);
+ return result;
+ }
+
+ private void AddCrossPlatformNode(CNodeWithTargetPlatform nodeWithTargetPlatform)
+ {
+ var node = nodeWithTargetPlatform.Node;
+
+ if (node is CNodeWithLocation nodeWithLocation)
+ {
+ nodeWithLocation.Location = null;
+ }
+
+ switch (node)
+ {
+ case CEnum @enum:
+ ClearLocationForTypeInfo(@enum.IntegerTypeInfo);
+ _enums.Add(@enum);
+ break;
+ case CVariable variable:
+ _variables.Add(variable);
+ break;
+ case COpaqueType opaqueType:
+ _opaqueTypes.Add(opaqueType);
+ break;
+ case CFunction function:
+ ClearLocationForTypeInfo(function.ReturnTypeInfo);
+ foreach (var parameter in function.Parameters)
+ {
+ parameter.Location = null;
+ ClearLocationForTypeInfo(parameter.TypeInfo);
+ }
+
+ _functions.Add(function);
+ break;
+ case CRecord record:
+ foreach (var field in record.Fields)
+ {
+ field.Location = null;
+ ClearLocationForTypeInfo(field.TypeInfo);
+ }
+
+ _records.Add(record);
+ break;
+ case CFunctionPointer functionPointer:
+ ClearLocationForTypeInfo(functionPointer.ReturnTypeInfo);
+ foreach (var parameter in functionPointer.Parameters)
+ {
+ ClearLocationForTypeInfo(parameter.TypeInfo);
+ }
+
+ _functionPointers.Add(functionPointer);
+ break;
+ case CMacroObject macroObject:
+ ClearLocationForTypeInfo(macroObject.TypeInfo);
+ _macroObjects.Add(macroObject);
+ break;
+ case CTypeAlias typeAlias:
+ ClearLocationForTypeInfo(typeAlias.UnderlyingTypeInfo);
+ _typeAliases.Add(typeAlias);
+ break;
+ case CEnumConstant enumConstant:
+ ClearLocationForTypeInfo(enumConstant.TypeInfo);
+ _enumConstants.Add(enumConstant);
+ break;
+ default:
+ throw new NotImplementedException($"Unknown node type '{node.GetType()}'");
+ }
+ }
+
+ private void ClearLocationForTypeInfo(CTypeInfo typeInfo)
+ {
+ var currentTypeInfo = typeInfo;
+ while (currentTypeInfo != null)
+ {
+ currentTypeInfo.Location = null;
+ currentTypeInfo = currentTypeInfo.InnerTypeInfo;
+ }
+ }
+
+ private void BuildCrossPlatformNodes(
+ ImmutableArray platforms,
+ ImmutableArray nodes,
+ string nodeName)
+ {
+ if (nodes.Length != platforms.Length)
+ {
+ var nodePlatforms = nodes.Select(x => x.TargetPlatform);
+ var missingNodePlatforms = platforms.Except(nodePlatforms);
+ var missingNodePlatformsString = string.Join(", ", missingNodePlatforms);
+ LogNodeNotCrossPlatform(nodeName, missingNodePlatformsString);
+ return;
+ }
+
+ if (nodes.Length == 1)
+ {
+ AddCrossPlatformNode(nodes[0]);
+ return;
+ }
+
+ var areAllEqual = true;
+ var firstNode = nodes[0].Node;
+ for (var i = 1; i < nodes.Length; i++)
+ {
+ var node = nodes[i].Node;
+
+ if (!node.Equals(firstNode))
+ {
+ if (node is CMacroObject nodeMacroObject && firstNode is CMacroObject firstNodeMacroObject)
+ {
+ if (nodeMacroObject.EqualsWithoutValue(firstNodeMacroObject))
+ {
+ areAllEqual = false;
+ break;
+ }
+ }
+
+ LogNodeNotEqual(nodeName);
+ areAllEqual = false;
+ break;
+ }
+ }
+
+ if (areAllEqual)
+ {
+ AddCrossPlatformNode(nodes[0]);
+ }
+ }
+
+ private ImmutableSortedDictionary> GetPlatformNodesByName(
+ ImmutableArray platformFfis)
+ {
+ var platformNodesByName = new Dictionary>();
+
+ foreach (var ffi in platformFfis)
+ {
+ AddPlatformFfi(ffi, platformNodesByName);
+ }
+
+ var result = platformNodesByName.
+ ToImmutableSortedDictionary(
+ x => x.Key,
+ y => y.Value.ToImmutableArray());
+ return result;
+ }
+
+ private void AddPlatformFfi(
+ CFfiTargetPlatform ffi,
+ Dictionary> platformNodesByName)
+ {
+ foreach (var (name, node) in ffi.Enums)
+ {
+ AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName);
+ }
+
+ foreach (var (name, node) in ffi.Functions)
+ {
+ AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName);
+ }
+
+ foreach (var (name, node) in ffi.Records)
+ {
+ AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName);
+ }
+
+ foreach (var (name, node) in ffi.Variables)
+ {
+ AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName);
+ }
+
+ foreach (var (name, node) in ffi.EnumConstants)
+ {
+ AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName);
+ }
+
+ foreach (var (name, node) in ffi.FunctionPointers)
+ {
+ AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName);
+ }
+
+ foreach (var (name, node) in ffi.MacroObjects)
+ {
+ AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName);
+ }
+
+ foreach (var (name, node) in ffi.OpaqueTypes)
+ {
+ AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName);
+ }
+
+ foreach (var (name, node) in ffi.TypeAliases)
+ {
+ AddPlatformNode(name, node, ffi.PlatformRequested, platformNodesByName);
+ }
+ }
+
+ private void AddPlatformNode(
+ string name,
+ CNode node,
+ TargetPlatform targetPlatform,
+ Dictionary> platformNodesByName)
+ {
+ if (!platformNodesByName.TryGetValue(name, out var platformNodes))
+ {
+ platformNodes = new List();
+ platformNodesByName.Add(name, platformNodes);
+ }
+
+ if (platformNodes.Count >= 1)
+ {
+ var previousNode = platformNodes[^1].Node;
+ if (node.NodeKind != previousNode.NodeKind)
+ {
+ var nodeActualKind = node.NodeKind.ToString();
+ var nodePlatform = targetPlatform.ToString();
+ var nodeExpectedKind = previousNode.NodeKind.ToString();
+ var nodePlatformExpectedKind = platformNodes[^1].TargetPlatform.ToString();
+ LogNodeNotSameKind(name, nodeActualKind, nodePlatform, nodeExpectedKind, nodePlatformExpectedKind);
+ return;
+ }
+ }
+
+ var nodeWithTargetPlatform = new CNodeWithTargetPlatform(node, targetPlatform);
+ platformNodes.Add(nodeWithTargetPlatform);
+ }
+
+ private ImmutableArray GetPlatformFfis(ImmutableArray inputFilePaths)
+ {
+ var builder = ImmutableArray.CreateBuilder();
+
+ foreach (var filePath in inputFilePaths)
+ {
+ CFfiTargetPlatform ffi;
+ try
+ {
+ ffi = Json.ReadFfiTargetPlatform(_fileSystem, filePath);
+ }
+#pragma warning disable CA1031
+ catch (Exception e)
+#pragma warning restore CA1031
+ {
+ LogFailedToLoadTargetPlatformAbstractSyntaxTree(e, filePath);
+ continue;
+ }
+
+ if (ffi.PlatformRequested.Equals(ffi.PlatformActual) && ffi.PlatformRequested.Equals(TargetPlatform.Unknown))
+ {
+ // Skip any FFI which is unknown platform.
+ // This can happen if the cross-platform FFI is placed in the same folder as the target-platform FFIs.
+ continue;
+ }
+
+ builder.Add(ffi);
+ }
+
+ return builder.ToImmutable();
+ }
+
+ [LoggerMessage(0, LogLevel.Warning, "The node '{NodeName}' is not cross-platform; there is no matching node for platforms: {MissingPlatformNames}")]
+ private partial void LogNodeNotCrossPlatform(string nodeName, string missingPlatformNames);
+
+ [LoggerMessage(1, LogLevel.Error, "The node '{NodeName}' of kind '{NodeActualKind}' for platform '{NodePlatform}' does not match the kind '{nodeExpectedKind}' for platform {NodePlatformExpectedKind}.")]
+ private partial void LogNodeNotSameKind(string nodeName, string nodeActualKind, string nodePlatform, string nodeExpectedKind, string nodePlatformExpectedKind);
+
+ [LoggerMessage(2, LogLevel.Error, "The node '{NodeName}' is not equal to all other platform nodes of the same name.")]
+ private partial void LogNodeNotEqual(string nodeName);
+
+ [LoggerMessage(3, LogLevel.Information, "Success. Merged FFIs for the target platforms '{TargetPlatformsString}': {FilePath}")]
+ private partial void LogWriteAbstractSyntaxTreeSuccess(
+ string targetPlatformsString,
+ string filePath);
+
+ [LoggerMessage(4, LogLevel.Error, "Failed to load platform FFI: {FilePath}")]
+ private partial void LogFailedToLoadTargetPlatformAbstractSyntaxTree(Exception e, string filePath);
+}
diff --git a/src/cs/production/c2ffi.Tool/Commands/Merge/Startup.cs b/src/cs/production/c2ffi.Tool/Commands/Merge/Startup.cs
index c0c04f4..42d456c 100644
--- a/src/cs/production/c2ffi.Tool/Commands/Merge/Startup.cs
+++ b/src/cs/production/c2ffi.Tool/Commands/Merge/Startup.cs
@@ -1,16 +1,17 @@
// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved.
// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information.
-using JetBrains.Annotations;
+using c2ffi.Tool.Commands.Merge.Input;
using Microsoft.Extensions.DependencyInjection;
namespace c2ffi.Tool.Commands.Merge;
-[UsedImplicitly]
public sealed class Startup : IDependencyInjectionStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
}
}
diff --git a/src/cs/production/c2ffi.Tool/Generated/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs b/src/cs/production/c2ffi.Tool/Generated/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs
index 86bd41d..e71bfeb 100644
--- a/src/cs/production/c2ffi.Tool/Generated/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs
+++ b/src/cs/production/c2ffi.Tool/Generated/Microsoft.Extensions.Logging.Generators/Microsoft.Extensions.Logging.Generators.LoggerMessageGenerator/LoggerMessage.g.cs
@@ -420,4 +420,70 @@ private partial void LogFailure()
}
}
}
+}
+namespace c2ffi.Tool.Commands.Merge
+{
+ partial class MergeFfisTool
+ {
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")]
+ private static readonly global::System.Action __LogNodeNotCrossPlatformCallback =
+ global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Warning, new global::Microsoft.Extensions.Logging.EventId(0, nameof(LogNodeNotCrossPlatform)), "The node '{NodeName}' is not cross-platform; there is no matching node for platforms: {MissingPlatformNames}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")]
+ private partial void LogNodeNotCrossPlatform(global::System.String nodeName, global::System.String missingPlatformNames)
+ {
+ if (_logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Warning))
+ {
+ __LogNodeNotCrossPlatformCallback(_logger, nodeName, missingPlatformNames, null);
+ }
+ }
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")]
+ private static readonly global::System.Action __LogNodeNotSameKindCallback =
+ global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Error, new global::Microsoft.Extensions.Logging.EventId(1, nameof(LogNodeNotSameKind)), "The node '{NodeName}' of kind '{NodeActualKind}' for platform '{NodePlatform}' does not match the kind '{nodeExpectedKind}' for platform {NodePlatformExpectedKind}.", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")]
+ private partial void LogNodeNotSameKind(global::System.String nodeName, global::System.String nodeActualKind, global::System.String nodePlatform, global::System.String nodeExpectedKind, global::System.String nodePlatformExpectedKind)
+ {
+ if (_logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Error))
+ {
+ __LogNodeNotSameKindCallback(_logger, nodeName, nodeActualKind, nodePlatform, nodeExpectedKind, nodePlatformExpectedKind, null);
+ }
+ }
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")]
+ private static readonly global::System.Action __LogNodeNotEqualCallback =
+ global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Error, new global::Microsoft.Extensions.Logging.EventId(2, nameof(LogNodeNotEqual)), "The node '{NodeName}' is not equal to all other platform nodes of the same name.", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")]
+ private partial void LogNodeNotEqual(global::System.String nodeName)
+ {
+ if (_logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Error))
+ {
+ __LogNodeNotEqualCallback(_logger, nodeName, null);
+ }
+ }
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")]
+ private static readonly global::System.Action __LogWriteAbstractSyntaxTreeSuccessCallback =
+ global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Information, new global::Microsoft.Extensions.Logging.EventId(3, nameof(LogWriteAbstractSyntaxTreeSuccess)), "Success. Merged FFIs for the target platforms '{TargetPlatformsString}': {FilePath}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")]
+ private partial void LogWriteAbstractSyntaxTreeSuccess(global::System.String targetPlatformsString, global::System.String filePath)
+ {
+ if (_logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Information))
+ {
+ __LogWriteAbstractSyntaxTreeSuccessCallback(_logger, targetPlatformsString, filePath, null);
+ }
+ }
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")]
+ private static readonly global::System.Action __LogFailedToLoadTargetPlatformAbstractSyntaxTreeCallback =
+ global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Error, new global::Microsoft.Extensions.Logging.EventId(4, nameof(LogFailedToLoadTargetPlatformAbstractSyntaxTree)), "Failed to load platform FFI: {FilePath}", new global::Microsoft.Extensions.Logging.LogDefineOptions() { SkipEnabledCheck = true });
+
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "8.0.9.3103")]
+ private partial void LogFailedToLoadTargetPlatformAbstractSyntaxTree(global::System.Exception e, global::System.String filePath)
+ {
+ if (_logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Error))
+ {
+ __LogFailedToLoadTargetPlatformAbstractSyntaxTreeCallback(_logger, filePath, e);
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/ExtractFfiTest.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/ExtractFfiTest.cs
index 9ad5ec5..4a43a96 100644
--- a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/ExtractFfiTest.cs
+++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/ExtractFfiTest.cs
@@ -38,9 +38,9 @@ protected ExtractFfiTest()
_tool = services.GetService()!;
}
- public ImmutableArray GetFfis(string relativeConfigurationFilePath)
+ public ImmutableArray GetFfis(string configurationFilePath)
{
- var fullConfigurationFilePath = GetFullConfigurationFilePath(relativeConfigurationFilePath);
+ var fullConfigurationFilePath = _fileSystemHelper.GetFullFilePath(configurationFilePath);
var oldFilePaths = GetFfiFilePaths(fullConfigurationFilePath);
DeleteFiles(oldFilePaths);
RunTool(fullConfigurationFilePath);
@@ -48,22 +48,10 @@ public ImmutableArray GetFfis(string relativeConfigurationFilePath)
return ReadFfis(filePaths);
}
- private string GetFullConfigurationFilePath(string relativeConfigurationFilePath)
+ private ImmutableArray ReadFfis(IEnumerable filePaths)
{
- var rootDirectoryPath = _fileSystemHelper.GitRepositoryRootDirectoryPath;
- var result = _path.Combine(rootDirectoryPath, relativeConfigurationFilePath);
- if (!_file.Exists(result))
- {
- throw new InvalidOperationException($"Could not find the configuration file path '{relativeConfigurationFilePath}'.");
- }
-
- return result;
- }
-
- private ImmutableArray ReadFfis(IEnumerable ffiFilePaths)
- {
- var builder = ImmutableArray.CreateBuilder();
- foreach (var filePath in ffiFilePaths)
+ var builder = ImmutableArray.CreateBuilder();
+ foreach (var filePath in filePaths)
{
var ffi = ReadFfi(filePath);
builder.Add(ffi);
@@ -97,7 +85,7 @@ private void DeleteFiles(IEnumerable filePaths)
}
}
- private CTestFfi ReadFfi(string filePath)
+ private CTestFfiTargetPlatform ReadFfi(string filePath)
{
var ffi = Json.ReadFfiTargetPlatform(_fileSystem, filePath);
@@ -109,7 +97,7 @@ private CTestFfi ReadFfi(string filePath)
var functionPointers = CreateTestFunctionPointers(ffi);
var opaqueDataTypes = CreateTestOpaqueTypes(ffi);
- var result = new CTestFfi(
+ var result = new CTestFfiTargetPlatform(
ffi.PlatformRequested.ToString(),
ffi.PlatformActual.ToString(),
functions,
@@ -122,11 +110,11 @@ private CTestFfi ReadFfi(string filePath)
return result;
}
- private static ImmutableDictionary CreateTestFunctions(CFfiTargetPlatform ast)
+ private static ImmutableDictionary CreateTestFunctions(CFfiTargetPlatform ffi)
{
var builder = ImmutableDictionary.CreateBuilder();
- foreach (var function in ast.Functions.Values)
+ foreach (var function in ffi.Functions.Values)
{
var result = CreateTestFunction(function);
builder.Add(result.Name, result);
@@ -177,11 +165,11 @@ private static CTestFunctionParameter CreateTestFunctionParameter(CFunctionParam
return result;
}
- private static ImmutableDictionary CreateTestEnums(CFfiTargetPlatform ast)
+ private static ImmutableDictionary CreateTestEnums(CFfiTargetPlatform ffi)
{
var builder = ImmutableDictionary.CreateBuilder();
- foreach (var @enum in ast.Enums.Values)
+ foreach (var @enum in ffi.Enums.Values)
{
var result = CreateTestEnum(@enum);
builder.Add(result.Name, result);
@@ -226,11 +214,11 @@ private static CTestEnumValue CreateTestEnumValue(CEnumValue value)
return result;
}
- private static ImmutableDictionary CreateTestRecords(CFfiTargetPlatform ast)
+ private static ImmutableDictionary CreateTestRecords(CFfiTargetPlatform ffi)
{
var builder = ImmutableDictionary.CreateBuilder();
- foreach (var value in ast.Records.Values)
+ foreach (var value in ffi.Records.Values)
{
var result = CreateTestRecord(value);
builder.Add(result.Name, result);
@@ -282,11 +270,11 @@ private static CTestRecordField CreateTestRecordField(CRecordField value)
return result;
}
- private ImmutableDictionary CreateTestMacroObjects(CFfiTargetPlatform ast)
+ private ImmutableDictionary CreateTestMacroObjects(CFfiTargetPlatform ffi)
{
var builder = ImmutableDictionary.CreateBuilder();
- foreach (var value in ast.MacroObjects.Values)
+ foreach (var value in ffi.MacroObjects.Values)
{
var result = CreateMacroObject(value);
builder.Add(result.Name, result);
@@ -307,11 +295,11 @@ private CTestMacroObject CreateMacroObject(CMacroObject value)
return result;
}
- private ImmutableDictionary CreateTestTypeAliases(CFfiTargetPlatform ast)
+ private ImmutableDictionary CreateTestTypeAliases(CFfiTargetPlatform ffi)
{
var builder = ImmutableDictionary.CreateBuilder();
- foreach (var value in ast.TypeAliases.Values)
+ foreach (var value in ffi.TypeAliases.Values)
{
var result = CreateTestTypeAlias(value);
builder.Add(result.Name, result);
@@ -332,11 +320,11 @@ private CTestTypeAlias CreateTestTypeAlias(CTypeAlias value)
return result;
}
- private ImmutableDictionary CreateTestFunctionPointers(CFfiTargetPlatform ast)
+ private ImmutableDictionary CreateTestFunctionPointers(CFfiTargetPlatform ffi)
{
var builder = ImmutableDictionary.CreateBuilder();
- foreach (var value in ast.FunctionPointers.Values)
+ foreach (var value in ffi.FunctionPointers.Values)
{
var result = CreateTestFunctionPointer(value);
builder.Add(result.Name, result);
@@ -386,11 +374,11 @@ private static CTestFunctionPointerParameter CreateTestFunctionPointerParameter(
}
private static ImmutableDictionary CreateTestOpaqueTypes(
- CFfiTargetPlatform ast)
+ CFfiTargetPlatform ffi)
{
var builder = ImmutableDictionary.CreateBuilder();
- foreach (var value in ast.OpaqueTypes.Values)
+ foreach (var value in ffi.OpaqueTypes.Values)
{
var result = CreateTestOpaqueType(value);
builder.Add(result.Name, result);
diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int/Test.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int/Test.cs
index bfde7b6..8ae4338 100644
--- a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int/Test.cs
+++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int/Test.cs
@@ -18,13 +18,13 @@ public void FunctionExists()
$"src/c/tests/functions/{FunctionName}/config.json");
Assert.True(ffis.Length > 0);
- foreach (var ast in ffis)
+ foreach (var ffi in ffis)
{
- AstFunctionExists(ast);
+ FfiFunctionExists(ffi);
}
}
- private void AstFunctionExists(CTestFfi ast)
+ private void FfiFunctionExists(CTestFfiTargetPlatform ast)
{
var function = ast.GetFunction(FunctionName);
Assert.True(function.CallingConvention == "cdecl");
diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int_params_int/Test.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int_params_int/Test.cs
index 1c663cb..e918310 100644
--- a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int_params_int/Test.cs
+++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_int_params_int/Test.cs
@@ -18,15 +18,15 @@ public void FunctionExists()
$"src/c/tests/functions/{FunctionName}/config.json");
Assert.True(ffis.Length > 0);
- foreach (var ast in ffis)
+ foreach (var ffi in ffis)
{
- AstFunctionExists(ast);
+ FfiFunctionExists(ffi);
}
}
- private static void AstFunctionExists(CTestFfi ast)
+ private static void FfiFunctionExists(CTestFfiTargetPlatform ffi)
{
- var function = ast.GetFunction(FunctionName);
+ var function = ffi.GetFunction(FunctionName);
Assert.True(function.CallingConvention == "cdecl");
Assert.True(function.ReturnTypeName == "int");
diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_uint64_params_uint8_uint16_uint32/Test.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_uint64_params_uint8_uint16_uint32/Test.cs
index 068f827..fa5fad3 100644
--- a/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_uint64_params_uint8_uint16_uint32/Test.cs
+++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Extract/Functions/function_uint64_params_uint8_uint16_uint32/Test.cs
@@ -18,15 +18,15 @@ public void FunctionExists()
$"src/c/tests/functions/{FunctionName}/config.json");
Assert.True(ffis.Length > 0);
- foreach (var ast in ffis)
+ foreach (var ffi in ffis)
{
- AstFunctionExists(ast);
+ FfiFunctionExists(ffi);
}
}
- private static void AstFunctionExists(CTestFfi ast)
+ private static void FfiFunctionExists(CTestFfiTargetPlatform ffi)
{
- var function = ast.GetFunction(FunctionName);
+ var function = ffi.GetFunction(FunctionName);
Assert.True(function.CallingConvention == "cdecl");
Assert.True(function.ReturnTypeName == "uint64_t");
diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/MergeFfisTest.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/MergeFfisTest.cs
new file mode 100644
index 0000000..bfd6d4b
--- /dev/null
+++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/MergeFfisTest.cs
@@ -0,0 +1,376 @@
+// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information.
+
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.IO.Abstractions;
+using c2ffi.Data;
+using c2ffi.Data.Nodes;
+using c2ffi.Data.Serialization;
+using c2ffi.Tests.Library;
+using c2ffi.Tests.Library.Helpers;
+using c2ffi.Tests.Library.Models;
+using c2ffi.Tool.Commands.Merge;
+using JetBrains.Annotations;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace c2ffi.Tests.EndToEnd.Merge;
+
+[PublicAPI]
+[ExcludeFromCodeCoverage]
+public abstract class MergeFfisTest
+{
+ private readonly IFileSystem _fileSystem;
+ private readonly IPath _path;
+ private readonly IDirectory _directory;
+ private readonly IFile _file;
+ private readonly FileSystemHelper _fileSystemHelper;
+ private readonly MergeFfisTool _tool;
+
+ protected MergeFfisTest()
+ {
+ var services = TestHost.Services;
+ _fileSystem = services.GetService()!;
+ _path = _fileSystem.Path;
+ _directory = _fileSystem.Directory;
+ _file = _fileSystem.File;
+ _fileSystemHelper = services.GetService()!;
+ _tool = services.GetService()!;
+ }
+
+ public CTestFfiCrossPlatform GetFfi(string relativeInputDirectoryPath)
+ {
+ var fullInputDirectoryPath = _fileSystemHelper.GetFullDirectoryPath(relativeInputDirectoryPath);
+ var fullOutputFilePath = _fileSystem.Path.Combine(fullInputDirectoryPath, "../ffi-x/cross-platform.json");
+ RunTool(fullInputDirectoryPath, fullOutputFilePath);
+ return ReadFfi(fullOutputFilePath);
+ }
+
+ private void RunTool(string inputDirectoryPath, string outputFilePath)
+ {
+ _tool.Run(inputDirectoryPath, outputFilePath);
+ }
+
+ private IEnumerable GetFfiFilePaths(string configurationFilePath)
+ {
+ var directoryPath = _path.GetDirectoryName(configurationFilePath)!;
+ var ffiDirectoryPath = _path.Combine(directoryPath, "ffi");
+ if (!_directory.Exists(ffiDirectoryPath))
+ {
+ return Array.Empty();
+ }
+
+ return _directory.EnumerateFiles(ffiDirectoryPath);
+ }
+
+ private CTestFfiCrossPlatform ReadFfi(string filePath)
+ {
+ var ffi = Json.ReadFfiCrossPlatform(_fileSystem, filePath);
+
+ var functions = CreateTestFunctions(ffi);
+ var enums = CreateTestEnums(ffi);
+ var structs = CreateTestRecords(ffi);
+ var macroObjects = CreateTestMacroObjects(ffi);
+ var typeAliases = CreateTestTypeAliases(ffi);
+ var functionPointers = CreateTestFunctionPointers(ffi);
+ var opaqueDataTypes = CreateTestOpaqueTypes(ffi);
+
+ var result = new CTestFfiCrossPlatform(
+ functions,
+ enums,
+ structs,
+ macroObjects,
+ typeAliases,
+ functionPointers,
+ opaqueDataTypes);
+ return result;
+ }
+
+ private static ImmutableDictionary CreateTestFunctions(CFfiCrossPlatform ffi)
+ {
+ var builder = ImmutableDictionary.CreateBuilder();
+
+ foreach (var function in ffi.Functions.Values)
+ {
+ var result = CreateTestFunction(function);
+ builder.Add(result.Name, result);
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private static CTestFunction CreateTestFunction(CFunction value)
+ {
+ var parameters = CreateTestFunctionParameters(value.Parameters);
+
+ var result = new CTestFunction
+ {
+ Name = value.Name,
+#pragma warning disable CA1308
+ CallingConvention = value.CallingConvention.ToString().ToLowerInvariant(),
+#pragma warning restore CA1308
+ ReturnTypeName = value.ReturnTypeInfo.Name,
+ Parameters = parameters,
+ Comment = value.Comment
+ };
+ return result;
+ }
+
+ private static ImmutableArray CreateTestFunctionParameters(
+ ImmutableArray values)
+ {
+ var builder = ImmutableArray.CreateBuilder();
+
+ foreach (var value in values)
+ {
+ var result = CreateTestFunctionParameter(value);
+ builder.Add(result);
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private static CTestFunctionParameter CreateTestFunctionParameter(CFunctionParameter value)
+ {
+ var result = new CTestFunctionParameter
+ {
+ Name = value.Name,
+ TypeName = value.TypeInfo.Name
+ };
+
+ return result;
+ }
+
+ private static ImmutableDictionary CreateTestEnums(CFfiCrossPlatform ffi)
+ {
+ var builder = ImmutableDictionary.CreateBuilder();
+
+ foreach (var @enum in ffi.Enums.Values)
+ {
+ var result = CreateTestEnum(@enum);
+ builder.Add(result.Name, result);
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private static CTestEnum CreateTestEnum(CEnum value)
+ {
+ var values = CreateTestEnumValues(value.Values);
+
+ var result = new CTestEnum
+ {
+ Name = value.Name,
+ IntegerType = value.IntegerTypeInfo.Name,
+ Values = values
+ };
+ return result;
+ }
+
+ private static ImmutableArray CreateTestEnumValues(ImmutableArray values)
+ {
+ var builder = ImmutableArray.CreateBuilder();
+
+ foreach (var value in values)
+ {
+ var result = CreateTestEnumValue(value);
+ builder.Add(result);
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private static CTestEnumValue CreateTestEnumValue(CEnumValue value)
+ {
+ var result = new CTestEnumValue
+ {
+ Name = value.Name,
+ Value = value.Value
+ };
+ return result;
+ }
+
+ private static ImmutableDictionary CreateTestRecords(CFfiCrossPlatform ffi)
+ {
+ var builder = ImmutableDictionary.CreateBuilder();
+
+ foreach (var value in ffi.Records.Values)
+ {
+ var result = CreateTestRecord(value);
+ builder.Add(result.Name, result);
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private static CTestRecord CreateTestRecord(CRecord value)
+ {
+ var name = value.Name;
+ var fields = CreateTestRecordFields(value.Fields);
+
+ var result = new CTestRecord
+ {
+ Name = name,
+ SizeOf = value.SizeOf,
+ AlignOf = value.AlignOf,
+ Fields = fields,
+ IsUnion = false
+ };
+
+ return result;
+ }
+
+ private static ImmutableArray CreateTestRecordFields(ImmutableArray values)
+ {
+ var builder = ImmutableArray.CreateBuilder();
+
+ foreach (var value in values)
+ {
+ var result = CreateTestRecordField(value);
+ builder.Add(result);
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private static CTestRecordField CreateTestRecordField(CRecordField value)
+ {
+ var result = new CTestRecordField
+ {
+ Name = value.Name,
+ TypeName = value.TypeInfo.Name,
+ OffsetOf = value.OffsetOf,
+ SizeOf = value.TypeInfo.SizeOf
+ };
+
+ return result;
+ }
+
+ private ImmutableDictionary CreateTestMacroObjects(CFfiCrossPlatform ffi)
+ {
+ var builder = ImmutableDictionary.CreateBuilder();
+
+ foreach (var value in ffi.MacroObjects.Values)
+ {
+ var result = CreateMacroObject(value);
+ builder.Add(result.Name, result);
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private CTestMacroObject CreateMacroObject(CMacroObject value)
+ {
+ var result = new CTestMacroObject
+ {
+ Name = value.Name,
+ TypeName = value.TypeInfo.Name,
+ Value = value.Value
+ };
+
+ return result;
+ }
+
+ private ImmutableDictionary CreateTestTypeAliases(CFfiCrossPlatform ffi)
+ {
+ var builder = ImmutableDictionary.CreateBuilder();
+
+ foreach (var value in ffi.TypeAliases.Values)
+ {
+ var result = CreateTestTypeAlias(value);
+ builder.Add(result.Name, result);
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private CTestTypeAlias CreateTestTypeAlias(CTypeAlias value)
+ {
+ var result = new CTestTypeAlias
+ {
+ Name = value.Name,
+ UnderlyingName = value.UnderlyingTypeInfo.Name,
+ UnderlyingKind = value.UnderlyingTypeInfo.NodeKind.ToString()
+ };
+
+ return result;
+ }
+
+ private ImmutableDictionary CreateTestFunctionPointers(CFfiCrossPlatform ffi)
+ {
+ var builder = ImmutableDictionary.CreateBuilder();
+
+ foreach (var value in ffi.FunctionPointers.Values)
+ {
+ var result = CreateTestFunctionPointer(value);
+ builder.Add(result.Name, result);
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private CTestFunctionPointer CreateTestFunctionPointer(CFunctionPointer value)
+ {
+ var parameters = CreateTestFunctionPointerParameters(value.Parameters);
+
+ var result = new CTestFunctionPointer
+ {
+ Name = value.Name,
+ CallingConvention = "todo",
+ ReturnTypeName = value.ReturnTypeInfo.Name,
+ Parameters = parameters
+ };
+
+ return result;
+ }
+
+ private static ImmutableArray CreateTestFunctionPointerParameters(
+ ImmutableArray values)
+ {
+ var builder = ImmutableArray.CreateBuilder();
+
+ foreach (var value in values)
+ {
+ var result = CreateTestFunctionPointerParameter(value);
+ builder.Add(result);
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private static CTestFunctionPointerParameter CreateTestFunctionPointerParameter(CFunctionPointerParameter value)
+ {
+ var result = new CTestFunctionPointerParameter
+ {
+ Name = value.Name,
+ TypeName = value.TypeInfo.Name
+ };
+
+ return result;
+ }
+
+ private static ImmutableDictionary CreateTestOpaqueTypes(
+ CFfiCrossPlatform ffi)
+ {
+ var builder = ImmutableDictionary.CreateBuilder();
+
+ foreach (var value in ffi.OpaqueTypes.Values)
+ {
+ var result = CreateTestOpaqueType(value);
+ builder.Add(result.Name, result);
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private static CTestOpaqueType CreateTestOpaqueType(COpaqueType value)
+ {
+ var result = new CTestOpaqueType
+ {
+ Name = value.Name,
+ SizeOf = value.SizeOf
+ };
+
+ return result;
+ }
+}
diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/Test.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/Test.cs
deleted file mode 100644
index b64e61c..0000000
--- a/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/Test.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved.
-// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information.
-
-using System.IO.Abstractions;
-using c2ffi.Tests.Library;
-using c2ffi.Tests.Library.Helpers;
-using Microsoft.Extensions.DependencyInjection;
-using Xunit;
-using Xunit.Abstractions;
-
-namespace c2ffi.Tests.EndToEnd.Merge;
-
-public class Test
-{
- private readonly ITestOutputHelper _testOutputHelper;
- private readonly IFileSystem _fileSystem;
- private readonly FileSystemHelper _fileSystemHelper;
-
- public Test(ITestOutputHelper testOutputHelper)
- {
- _testOutputHelper = testOutputHelper;
- var services = TestHost.Services;
- _fileSystem = services.GetService()!;
- _fileSystemHelper = services.GetService()!;
- }
-
- [Fact]
- public void Exists()
- {
- var rootDirectoryPath = _fileSystemHelper.GitRepositoryRootDirectoryPath;
- var x = _fileSystem.Path.Combine(rootDirectoryPath, "src/c/tests/functions/function_int/ffi");
- var y = _fileSystem.DirectoryInfo.New(x);
- Assert.True(y.Exists);
- foreach (var z in y.EnumerateFiles())
- {
- _testOutputHelper.WriteLine(z.FullName);
- }
- }
-}
diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_int/Test.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_int/Test.cs
new file mode 100644
index 0000000..6a73153
--- /dev/null
+++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_int/Test.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information.
+
+using Xunit;
+
+#pragma warning disable CA1707
+
+namespace c2ffi.Tests.EndToEnd.Merge.function_int;
+
+public class Test : MergeFfisTest
+{
+ private const string FunctionName = "function_int";
+
+ [Fact]
+ public void FunctionExists()
+ {
+ var ffi = GetFfi(
+ $"src/c/tests/functions/{FunctionName}/ffi");
+
+ var function = ffi.GetFunction(FunctionName);
+ Assert.True(function.CallingConvention == "cdecl");
+ Assert.True(function.ReturnTypeName == "int");
+
+ Assert.True(function.Parameters.IsDefaultOrEmpty);
+ }
+}
diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_int_params_int/Test.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_int_params_int/Test.cs
new file mode 100644
index 0000000..c897509
--- /dev/null
+++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_int_params_int/Test.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information.
+
+using c2ffi.Tests.Library.Models;
+using Xunit;
+
+#pragma warning disable CA1707
+
+namespace c2ffi.Tests.EndToEnd.Merge.function_int_params_int;
+
+public class Test : MergeFfisTest
+{
+ private const string FunctionName = "function_int_params_int";
+
+ [Fact]
+ public void FunctionExists()
+ {
+ var ffi = GetFfi(
+ $"src/c/tests/functions/{FunctionName}/ffi");
+
+ var function = ffi.GetFunction(FunctionName);
+ Assert.True(function.CallingConvention == "cdecl");
+ Assert.True(function.ReturnTypeName == "int");
+
+ Assert.True(function.Parameters.Length == 1);
+ var parameter = function.Parameters[0];
+ Assert.True(parameter.Name == "a");
+ Assert.True(parameter.TypeName == "int");
+ }
+}
diff --git a/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_uint64_params_uint8_uint16_uint32/Test.cs b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_uint64_params_uint8_uint16_uint32/Test.cs
new file mode 100644
index 0000000..2403287
--- /dev/null
+++ b/src/cs/tests/c2ffi.Tests.EndToEnd.Merge/function_uint64_params_uint8_uint16_uint32/Test.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information.
+
+using c2ffi.Tests.Library.Models;
+using Xunit;
+
+#pragma warning disable CA1707
+
+namespace c2ffi.Tests.EndToEnd.Merge.function_uint64_params_uint8_uint16_uint32;
+
+public class Test : MergeFfisTest
+{
+ private const string FunctionName = "function_uint64_params_uint8_uint16_uint32";
+
+ [Fact]
+ public void FunctionExists()
+ {
+ var ffi = GetFfi(
+ $"src/c/tests/functions/{FunctionName}/ffi");
+
+ var function = ffi.GetFunction(FunctionName);
+ Assert.True(function.CallingConvention == "cdecl");
+ Assert.True(function.ReturnTypeName == "uint64_t");
+
+ Assert.True(function.Parameters.Length == 3);
+
+ var parameter1 = function.Parameters[0];
+ Assert.True(parameter1.Name == "a");
+ Assert.True(parameter1.TypeName == "uint8_t");
+
+ var parameter2 = function.Parameters[1];
+ Assert.True(parameter2.Name == "b");
+ Assert.True(parameter2.TypeName == "uint16_t");
+
+ var parameter3 = function.Parameters[2];
+ Assert.True(parameter3.Name == "c");
+ Assert.True(parameter3.TypeName == "uint32_t");
+ }
+}
diff --git a/src/cs/tests/c2ffi.Tests.Library/Helpers/FileSystemHelper.cs b/src/cs/tests/c2ffi.Tests.Library/Helpers/FileSystemHelper.cs
index 13bd457..9c02cd6 100644
--- a/src/cs/tests/c2ffi.Tests.Library/Helpers/FileSystemHelper.cs
+++ b/src/cs/tests/c2ffi.Tests.Library/Helpers/FileSystemHelper.cs
@@ -19,6 +19,31 @@ public FileSystemHelper(IFileSystem fileSystem)
_fileSystem = fileSystem;
}
+ public string GetFullFilePath(string relativeFilePath)
+ {
+ var rootDirectoryPath = GitRepositoryRootDirectoryPath;
+ var filePath = _fileSystem.Path.Combine(rootDirectoryPath, relativeFilePath);
+
+ if (!_fileSystem.File.Exists(filePath))
+ {
+ throw new InvalidOperationException($"Could not find file path: {filePath}");
+ }
+
+ return filePath;
+ }
+
+ public string GetFullDirectoryPath(string relativeDirectoryPath)
+ {
+ var rootDirectoryPath = GitRepositoryRootDirectoryPath;
+ var directoryPath = _fileSystem.Path.Combine(rootDirectoryPath, relativeDirectoryPath);
+ if (!_fileSystem.Directory.Exists(directoryPath))
+ {
+ throw new InvalidOperationException($"Could not find directory path: {relativeDirectoryPath}");
+ }
+
+ return directoryPath;
+ }
+
private string FindGitRepositoryRootDirectoryPath()
{
var baseDirectory = AppContext.BaseDirectory;
diff --git a/src/cs/tests/c2ffi.Tests.Library/Models/CTestFfiCrossPlatform.cs b/src/cs/tests/c2ffi.Tests.Library/Models/CTestFfiCrossPlatform.cs
new file mode 100644
index 0000000..47dd5bb
--- /dev/null
+++ b/src/cs/tests/c2ffi.Tests.Library/Models/CTestFfiCrossPlatform.cs
@@ -0,0 +1,257 @@
+// Copyright (c) Bottlenose Labs Inc. (https://github.com/bottlenoselabs). All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the Git repository root directory for full license information.
+
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using JetBrains.Annotations;
+using Xunit;
+
+namespace c2ffi.Tests.Library.Models;
+
+[PublicAPI]
+[ExcludeFromCodeCoverage]
+public sealed class CTestFfiCrossPlatform
+{
+ private readonly ImmutableDictionary _enums;
+ private readonly ImmutableDictionary _functions;
+ private readonly ImmutableDictionary _macroObjects;
+ private readonly ImmutableDictionary _records;
+ private readonly ImmutableDictionary _typeAliases;
+ private readonly ImmutableDictionary _functionPointers;
+ private readonly ImmutableDictionary _opaqueTypes;
+
+ private readonly ImmutableHashSet.Builder _namesTested;
+
+ public CTestFfiCrossPlatform(
+ ImmutableDictionary functions,
+ ImmutableDictionary enums,
+ ImmutableDictionary records,
+ ImmutableDictionary macroObjects,
+ ImmutableDictionary typeAliases,
+ ImmutableDictionary functionPointers,
+ ImmutableDictionary opaqueTypes)
+ {
+ _functions = functions;
+ _enums = enums;
+ _records = records;
+ _macroObjects = macroObjects;
+ _typeAliases = typeAliases;
+ _functionPointers = functionPointers;
+ _opaqueTypes = opaqueTypes;
+ _namesTested = ImmutableHashSet.CreateBuilder();
+
+ AssertPInvokePlatformNameFunction();
+ }
+
+ public void AssertNodesAreTested()
+ {
+ foreach (var value in _enums.Values)
+ {
+ Assert.True(_namesTested.Contains(value.Name), $"The C enum '{value.Name}' is not covered in a test!");
+ }
+
+ foreach (var value in _records.Values)
+ {
+ Assert.True(_namesTested.Contains(value.Name), $"The C record '{value.Name}' is not covered in a test!");
+ }
+
+ foreach (var value in _macroObjects.Values)
+ {
+ Assert.True(_namesTested.Contains(value.Name), $"The C macro object '{value.Name}' is not covered in a test!");
+ }
+
+ foreach (var value in _typeAliases.Values)
+ {
+ Assert.True(_namesTested.Contains(value.Name), $"The C type alias '{value.Name}' is not covered in a test!");
+ }
+
+ foreach (var value in _functionPointers.Values)
+ {
+ Assert.True(_namesTested.Contains(value.Name), $"The C function pointer '{value.Name}' is not covered in a test!");
+ }
+ }
+
+ public CTestFunction GetFunction(string name)
+ {
+ var exists = _functions.TryGetValue(name, out var value);
+ Assert.True(exists, $"The function '{name}' does not exist.");
+ _namesTested.Add(name);
+ return value!;
+ }
+
+ public CTestFunction? TryGetFunction(string name)
+ {
+ var exists = _functions.TryGetValue(name, out var value);
+ if (!exists)
+ {
+ return null;
+ }
+
+ _namesTested.Add(name);
+ return value;
+ }
+
+ public CTestEnum GetEnum(string name)
+ {
+ var exists = _enums.TryGetValue(name, out var value);
+ Assert.True(exists, $"The enum '{name}' does not exist.");
+ _namesTested.Add(name);
+ return value!;
+ }
+
+ public CTestEnum? TryGetEnum(string name)
+ {
+ var exists = _enums.TryGetValue(name, out var value);
+ if (!exists)
+ {
+ return null;
+ }
+
+ _namesTested.Add(name);
+ return value;
+ }
+
+ public CTestRecord GetRecord(string name)
+ {
+ var exists = _records.TryGetValue(name, out var value);
+ Assert.True(exists, $"The record '{name}' does not exist.");
+ _namesTested.Add(name);
+ AssertRecord(value!);
+ return value!;
+ }
+
+ public CTestRecord? TryGetRecord(string name)
+ {
+ var exists = _records.TryGetValue(name, out var value);
+ if (!exists)
+ {
+ return null;
+ }
+
+ _namesTested.Add(name);
+ AssertRecord(value!);
+ return value;
+ }
+
+ public CTestMacroObject GetMacroObject(string name)
+ {
+ var exists = _macroObjects.TryGetValue(name, out var value);
+ Assert.True(exists, $"The macro object '{name}' does not exist.");
+ _namesTested.Add(name);
+ return value!;
+ }
+
+ public CTestMacroObject? TryGetMacroObject(string name)
+ {
+ var exists = _macroObjects.TryGetValue(name, out var value);
+ if (!exists)
+ {
+ return null;
+ }
+
+ _namesTested.Add(name);
+ return value;
+ }
+
+ public CTestTypeAlias GetTypeAlias(string name)
+ {
+ var exists = _typeAliases.TryGetValue(name, out var value);
+ Assert.True(exists, $"The type alias '{name}' does not exist.");
+ _namesTested.Add(name);
+ return value!;
+ }
+
+ public CTestTypeAlias? TryGetTypeAlias(string name)
+ {
+ var exists = _typeAliases.TryGetValue(name, out var value);
+ if (!exists)
+ {
+ return null;
+ }
+
+ _namesTested.Add(name);
+ return value;
+ }
+
+ public CTestFunctionPointer GetFunctionPointer(string name)
+ {
+ var exists = _functionPointers.TryGetValue(name, out var value);
+ Assert.True(exists, $"The function pointer '{name}' does not exist.");
+ _namesTested.Add(name);
+ return value!;
+ }
+
+ public CTestFunctionPointer? TryGetFunctionPointer(string name)
+ {
+ var exists = _functionPointers.TryGetValue(name, out var value);
+ return exists ? value : null;
+ }
+
+ public CTestOpaqueType GetOpaqueType(string name)
+ {
+ var exists = _opaqueTypes.TryGetValue(name, out var value);
+ Assert.True(exists, $"The opaque type '{name}' does not exist.");
+ _namesTested.Add(name);
+ return value!;
+ }
+
+ public CTestOpaqueType? TryGetOpaqueType(string name)
+ {
+ var exists = _opaqueTypes.TryGetValue(name, out var value);
+ return exists ? value : null;
+ }
+
+ private void AssertRecord(CTestRecord record)
+ {
+ var namesLookup = new List();
+
+ foreach (var field in record.Fields)
+ {
+ AssertRecordField(record, field, namesLookup);
+ }
+
+ Assert.True(
+ record.AlignOf > 0,
+ $"C record '{record.Name}' does not have an alignment of which is positive.");
+
+ Assert.True(
+ record.SizeOf >= 0,
+ $"C record '{record.Name}' does not have an size of of which is positive or zero.");
+ }
+
+ private void AssertRecordField(CTestRecord record, CTestRecordField field, List namesLookup)
+ {
+ var recordKindName = record.IsUnion ? "union" : "struct";
+
+ Assert.False(
+ namesLookup.Contains(field.Name),
+ $"C {recordKindName} '{record.Name}' already has a field named `{field.Name}`.");
+ namesLookup.Add(field.Name);
+
+ Assert.True(
+ field.OffsetOf >= 0,
+ $"C {recordKindName} '{record.Name}' field '{field.Name}' does not have an offset of which is positive or zero.");
+ Assert.True(
+ field.SizeOf > 0,
+ $"C {recordKindName} '{record.Name}' field '{field.Name}' does not have a size of which is positive.");
+
+ if (record.IsUnion)
+ {
+ Assert.True(
+ field.OffsetOf == 0,
+ $"C union '{record.Name}' field '{field.Name}' does not have an offset of zero.");
+ Assert.True(
+ field.SizeOf == record.SizeOf,
+ $"C union '{record.Name}' field '{field.Name}' does not have a size that matches the union.");
+ }
+ }
+
+ private void AssertPInvokePlatformNameFunction()
+ {
+ var function = GetFunction("ffi_get_platform_name");
+ Assert.Equal("cdecl", function.CallingConvention);
+ Assert.Equal("const char *", function.ReturnTypeName);
+ Assert.Equal("// Returns the current platform name.", function.Comment);
+ Assert.True(function.Parameters.IsDefaultOrEmpty);
+ }
+}
diff --git a/src/cs/tests/c2ffi.Tests.Library/Models/CTestFfi.cs b/src/cs/tests/c2ffi.Tests.Library/Models/CTestFfiTargetPlatform.cs
similarity index 99%
rename from src/cs/tests/c2ffi.Tests.Library/Models/CTestFfi.cs
rename to src/cs/tests/c2ffi.Tests.Library/Models/CTestFfiTargetPlatform.cs
index 389d3d3..93d5c66 100644
--- a/src/cs/tests/c2ffi.Tests.Library/Models/CTestFfi.cs
+++ b/src/cs/tests/c2ffi.Tests.Library/Models/CTestFfiTargetPlatform.cs
@@ -10,7 +10,7 @@ namespace c2ffi.Tests.Library.Models;
[PublicAPI]
[ExcludeFromCodeCoverage]
-public sealed class CTestFfi
+public sealed class CTestFfiTargetPlatform
{
private readonly ImmutableDictionary _enums;
private readonly ImmutableDictionary _functions;
@@ -26,7 +26,7 @@ public sealed class CTestFfi
public string TargetPlatformActual { get; }
- public CTestFfi(
+ public CTestFfiTargetPlatform(
string targetPlatformRequested,
string targetPlatformActual,
ImmutableDictionary functions,