Skip to content

Commit 95470ee

Browse files
dalexsotoCopilot
andauthored
[sharpie] Add --deepsplit option to split bindings into one file per source header (#24883)
Add a new --deepsplit CLI option that generates one .cs file per source header instead of a single ApiDefinition.cs. This makes it easier to navigate large framework bindings and map generated C# types back to their original Objective-C headers. The implementation adds a DeepSplitMassager that extracts the source header filename from each declaration's Clang annotation via Cursor.TryGetPresumedLoc, creates a DocumentSyntaxTree per unique header basename, and distributes declarations accordingly. Struct and enum declarations are consolidated in StructsAndEnums.cs. Declarations without source location info fall back to ApiDefinition.cs. Example: binding FBSDKCoreKit with --deepsplit produces 194 files (FBSDKAccessToken.cs, FBSDKGraphRequest.cs, etc.) instead of a single 7,688-line ApiDefinition.cs. Fixes #23024. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c568db9 commit 95470ee

File tree

4 files changed

+235
-1
lines changed

4 files changed

+235
-1
lines changed

tests/sharpie/Sharpie.Bind.Tests/ObjectiveCClass.cs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,4 +627,130 @@ interface GoodClass {
627627
""";
628628
bindings.AssertSuccess (expectedBindings);
629629
}
630+
631+
[Test]
632+
public void DeepSplit_SplitsPerHeader ()
633+
{
634+
// Verify that --deepsplit creates one .cs file per source header.
635+
var binder = new BindTool ();
636+
var tmpdir = Cache.CreateTemporaryDirectory ();
637+
var headersDir = Path.Combine (tmpdir, "headers");
638+
Directory.CreateDirectory (headersDir);
639+
640+
File.WriteAllText (Path.Combine (headersDir, "ClassA.h"),
641+
"""
642+
@interface ClassA {
643+
}
644+
@property int valueA;
645+
@end
646+
""");
647+
648+
File.WriteAllText (Path.Combine (headersDir, "ClassB.h"),
649+
"""
650+
@interface ClassB {
651+
}
652+
@property int valueB;
653+
@end
654+
""");
655+
656+
var mainHeader = Path.Combine (tmpdir, "main.h");
657+
File.WriteAllText (mainHeader, $"#import \"{Path.Combine (headersDir, "ClassA.h")}\"\n#import \"{Path.Combine (headersDir, "ClassB.h")}\"\n");
658+
659+
binder.SourceFile = mainHeader;
660+
binder.DirectoriesInScope.Add (headersDir);
661+
binder.OutputDirectory = tmpdir;
662+
binder.DeepSplit = true;
663+
Configuration.IgnoreIfIgnoredPlatform (binder.Platform);
664+
binder.PlatformAssembly = Extensions.GetPlatformAssemblyPath (binder.Platform);
665+
binder.ClangResourceDirectory = Extensions.GetClangResourceDirectory ();
666+
var bindings = binder.BindInOrOut ();
667+
bindings.AssertSuccess (null);
668+
669+
Assert.That (bindings.AdditionalFiles.ContainsKey ("ClassA.cs"), Is.True, "Should have ClassA.cs");
670+
Assert.That (bindings.AdditionalFiles.ContainsKey ("ClassB.cs"), Is.True, "Should have ClassB.cs");
671+
Assert.That (bindings.AdditionalFiles ["ClassA.cs"], Does.Contain ("ClassA"), "ClassA.cs should contain ClassA");
672+
Assert.That (bindings.AdditionalFiles ["ClassA.cs"], Does.Not.Contain ("ClassB"), "ClassA.cs should not contain ClassB");
673+
Assert.That (bindings.AdditionalFiles ["ClassB.cs"], Does.Contain ("ClassB"), "ClassB.cs should contain ClassB");
674+
Assert.That (bindings.AdditionalFiles ["ClassB.cs"], Does.Not.Contain ("ClassA"), "ClassB.cs should not contain ClassA");
675+
}
676+
677+
[Test]
678+
public void DeepSplit_StructsAndEnumsSeparate ()
679+
{
680+
// Verify that structs and enums go into StructsAndEnums.cs even in deepsplit mode.
681+
var binder = new BindTool ();
682+
var tmpdir = Cache.CreateTemporaryDirectory ();
683+
var headersDir = Path.Combine (tmpdir, "headers");
684+
Directory.CreateDirectory (headersDir);
685+
686+
File.WriteAllText (Path.Combine (headersDir, "Widget.h"),
687+
"""
688+
struct WidgetSize {
689+
int width;
690+
int height;
691+
};
692+
@interface Widget {
693+
}
694+
@property int tag;
695+
@end
696+
""");
697+
698+
var mainHeader = Path.Combine (tmpdir, "main.h");
699+
File.WriteAllText (mainHeader, $"#import \"{Path.Combine (headersDir, "Widget.h")}\"\n");
700+
701+
binder.SourceFile = mainHeader;
702+
binder.DirectoriesInScope.Add (headersDir);
703+
binder.OutputDirectory = tmpdir;
704+
binder.DeepSplit = true;
705+
Configuration.IgnoreIfIgnoredPlatform (binder.Platform);
706+
binder.PlatformAssembly = Extensions.GetPlatformAssemblyPath (binder.Platform);
707+
binder.ClangResourceDirectory = Extensions.GetClangResourceDirectory ();
708+
var bindings = binder.BindInOrOut ();
709+
bindings.AssertSuccess (null);
710+
711+
Assert.That (bindings.AdditionalFiles.ContainsKey ("Widget.cs"), Is.True, "Should have Widget.cs for the interface");
712+
Assert.That (bindings.AdditionalFiles.ContainsKey ("StructsAndEnums.cs"), Is.True, "Should have StructsAndEnums.cs for the struct");
713+
Assert.That (bindings.AdditionalFiles ["Widget.cs"], Does.Contain ("Widget"), "Widget.cs should contain Widget interface");
714+
Assert.That (bindings.AdditionalFiles ["Widget.cs"], Does.Not.Contain ("WidgetSize"), "Widget.cs should not contain the struct");
715+
Assert.That (bindings.AdditionalFiles ["StructsAndEnums.cs"], Does.Contain ("WidgetSize"), "StructsAndEnums.cs should contain the struct");
716+
}
717+
718+
[Test]
719+
public void DeepSplit_MultipleClassesInOneHeader ()
720+
{
721+
// Verify that multiple classes from the same header go into the same .cs file.
722+
var binder = new BindTool ();
723+
var tmpdir = Cache.CreateTemporaryDirectory ();
724+
var headersDir = Path.Combine (tmpdir, "headers");
725+
Directory.CreateDirectory (headersDir);
726+
727+
File.WriteAllText (Path.Combine (headersDir, "Models.h"),
728+
"""
729+
@interface Person {
730+
}
731+
@property int age;
732+
@end
733+
@interface Car {
734+
}
735+
@property int speed;
736+
@end
737+
""");
738+
739+
var mainHeader = Path.Combine (tmpdir, "main.h");
740+
File.WriteAllText (mainHeader, $"#import \"{Path.Combine (headersDir, "Models.h")}\"\n");
741+
742+
binder.SourceFile = mainHeader;
743+
binder.DirectoriesInScope.Add (headersDir);
744+
binder.OutputDirectory = tmpdir;
745+
binder.DeepSplit = true;
746+
Configuration.IgnoreIfIgnoredPlatform (binder.Platform);
747+
binder.PlatformAssembly = Extensions.GetPlatformAssemblyPath (binder.Platform);
748+
binder.ClangResourceDirectory = Extensions.GetClangResourceDirectory ();
749+
var bindings = binder.BindInOrOut ();
750+
bindings.AssertSuccess (null);
751+
752+
Assert.That (bindings.AdditionalFiles.ContainsKey ("Models.cs"), Is.True, "Should have Models.cs");
753+
Assert.That (bindings.AdditionalFiles ["Models.cs"], Does.Contain ("Person"), "Models.cs should contain Person");
754+
Assert.That (bindings.AdditionalFiles ["Models.cs"], Does.Contain ("Car"), "Models.cs should contain Car");
755+
}
630756
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using ICSharpCode.NRefactory.CSharp;
5+
6+
namespace Sharpie.Bind.Massagers;
7+
8+
/// <summary>
9+
/// Splits the generated binding into one .cs file per source header file.
10+
/// Struct/enum declarations go into a separate StructsAndEnums.cs file.
11+
/// </summary>
12+
[RegisterBefore (typeof (GenerateUsingStatementsMassager))]
13+
public sealed class DeepSplitMassager : Massager<DeepSplitMassager> {
14+
readonly Dictionary<string, DocumentSyntaxTree> documentsByHeader = new (StringComparer.OrdinalIgnoreCase);
15+
readonly DocumentSyntaxTree structsAndEnums = new DocumentSyntaxTree ("StructsAndEnums.cs");
16+
17+
public DeepSplitMassager (ObjectiveCBinder binder)
18+
: base (binder)
19+
{
20+
}
21+
22+
DocumentSyntaxTree GetOrCreateDocument (string headerFileName)
23+
{
24+
var baseName = Path.GetFileNameWithoutExtension (headerFileName);
25+
var key = baseName.ToLowerInvariant ();
26+
27+
if (!documentsByHeader.TryGetValue (key, out var doc)) {
28+
doc = new DocumentSyntaxTree (baseName + ".cs");
29+
documentsByHeader [key] = doc;
30+
}
31+
return doc;
32+
}
33+
34+
string? GetSourceHeaderName (AstNode node)
35+
{
36+
// Walk annotations to find the linked Clang declaration and its source location
37+
foreach (var annotation in node.Annotations) {
38+
if (annotation is Cursor cursor) {
39+
if (cursor.TryGetPresumedLoc (out var loc) && loc.HasValue && !string.IsNullOrEmpty (loc.Value.FileName))
40+
return Path.GetFileName (loc.Value.FileName);
41+
}
42+
}
43+
return null;
44+
}
45+
46+
public override void VisitTypeDeclaration (TypeDeclaration typeDeclaration)
47+
{
48+
if (HasVisited (typeDeclaration))
49+
return;
50+
51+
MarkVisited (typeDeclaration);
52+
typeDeclaration.Remove ();
53+
54+
if (typeDeclaration.ClassType == ClassType.Interface) {
55+
var headerName = GetSourceHeaderName (typeDeclaration);
56+
if (headerName is not null) {
57+
GetOrCreateDocument (headerName).Members.Add (typeDeclaration);
58+
} else {
59+
GetOrCreateDocument ("ApiDefinition").Members.Add (typeDeclaration);
60+
}
61+
} else {
62+
structsAndEnums.Members.Add (typeDeclaration);
63+
}
64+
}
65+
66+
public override void VisitDelegateDeclaration (DelegateDeclaration delegateDeclaration)
67+
{
68+
if (HasVisited (delegateDeclaration))
69+
return;
70+
71+
MarkVisited (delegateDeclaration);
72+
delegateDeclaration.Remove ();
73+
74+
var headerName = GetSourceHeaderName (delegateDeclaration);
75+
if (headerName is not null) {
76+
GetOrCreateDocument (headerName).Members.Add (delegateDeclaration);
77+
} else {
78+
GetOrCreateDocument ("ApiDefinition").Members.Add (delegateDeclaration);
79+
}
80+
}
81+
82+
public override void VisitSyntaxTree (SyntaxTree syntaxTree)
83+
{
84+
base.VisitSyntaxTree (syntaxTree);
85+
86+
if (syntaxTree.Members.Count > 0) {
87+
Console.Error.WriteLine (syntaxTree);
88+
throw new Exception ("original SyntaxTree should be empty");
89+
}
90+
91+
// Add documents sorted by filename for deterministic output
92+
foreach (var doc in documentsByHeader.OrderBy (kv => kv.Key)) {
93+
if (doc.Value.Members.Count > 0)
94+
syntaxTree.Members.Add (doc.Value);
95+
}
96+
97+
if (structsAndEnums.Members.Count > 0)
98+
syntaxTree.Members.Add (structsAndEnums);
99+
}
100+
}

tools/sharpie/Sharpie.Bind/ObjectiveCBinder.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public string Sdk {
3434
public List<string> ClangArguments = new List<string> ();
3535
public bool? EnableModules { get; set; }
3636
public bool SplitDocuments { get; set; } = true;
37+
public bool DeepSplit { get; set; }
3738

3839
public string ApiDefinitionName { get; set; } = "ApiDefinition.cs";
3940
public string StructsAndEnumsName { get; set; } = "StructsAndEnums.cs";
@@ -249,6 +250,10 @@ protected virtual bool AddArguments (List<string> args)
249250
args.Add ("--nosplit");
250251
}
251252

253+
if (DeepSplit) {
254+
args.Add ("--deepsplit");
255+
}
256+
252257
if (EnableModules.HasValue) {
253258
if (EnableModules.Value)
254259
args.Add ("--modules=true");
@@ -468,7 +473,9 @@ BindingResult BindImpl ()
468473
var massagerNs = new NamespaceMassager (this, Namespace);
469474
massager.RegisterMassager (massagerNs);
470475
}
471-
if (SplitDocuments)
476+
if (DeepSplit)
477+
massager.RegisterMassager (new DeepSplitMassager (this));
478+
else if (SplitDocuments)
472479
massager.RegisterMassager (new SyntaxTreeSplitterMassager (this));
473480
foreach (var m in Massagers) {
474481
if (m.Enable) {

tools/sharpie/Sharpie.Bind/Tools.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public static int Bind (string [] arguments)
4444
{ "n|namespace=", "Namespace under which to generate the binding. The default is to use the framework's name as the namespace.", v => binder.Namespace = v },
4545
{ "m|massage=", "Register (+ prefix) or exclude (- prefix) a massager by name.", v => binder.AddMassager (v) },
4646
{ "nosplit", "Do not split the generated binding into multiple files.", v => binder.SplitDocuments = false },
47+
{ "deepsplit", "Split the generated binding into one file per source header.", v => binder.DeepSplit = true },
4748
};
4849

4950
os.EndOfParsingArguments.Clear ();

0 commit comments

Comments
 (0)