Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[dotnet] preliminary cut for class-redirector #17951

Merged
merged 4 commits into from
Apr 3, 2023

Conversation

stephen-hawley
Copy link
Contributor

@stephen-hawley stephen-hawley commented Mar 30, 2023

This is the preliminary version of class-redirector addressing issue #16671

What it does:

  • reads in an XML configuration file that will have been generated by the static registrar.
  • writes static fields into a class named after each ObjC class backed by the equivalent C# class
  • writes an initialization method body that initialized each field based on the static registrar.
  • in the process it also makes a map from the C# class to a FieldDefinition for the class_ptr
    For each class:
  • removes the class_ptr field
  • removes the initialization code for that field from that .cctor
  • removes that .cctor if it can
  • replaces all read references to the class_ptr to a read reference to the appropriate FieldDefinition

Tests:
right now there are tests that test the XML reading and writing
There will be future tests that test the class rewriting writ small before we release it on our code base

As it is, this will not affect our code if its merged in

To do:
need to modify the static registrar to generate the XML file
need to invoke this after the static registrar has run
make it localizable

What you should consider in this PR:

  • any boneheaded mistakes I made in style/spelling
  • overall structure
  • argument processing
  • error checking

@stephen-hawley stephen-hawley added the not-notes-worthy Ignore for release notes label Mar 30, 2023
@github-actions
Copy link
Contributor

⚠️ Your code has been reformatted. ⚠️

If this is not desired, add the actions-disable-autoformat label, and revert the reformatting commit.

If files unrelated to your change were modified, try reverting the reformatting commit + merging with the target branch (and push those changes).

Copy link
Member

@mandel-macaque mandel-macaque left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of them are nits except for the choice of net6 over net7, why is that?

Comment on lines 10 to 12
var map = new CSToObjCMap ();
map.Add ("Foo", new ObjCNameIndex ("Bar", 2));
map.Add ("Baz", new ObjCNameIndex ("Zed", 3));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because you are inheriting from the dictionary class, you can use a dict initializer:

Suggested change
var map = new CSToObjCMap ();
map.Add ("Foo", new ObjCNameIndex ("Bar", 2));
map.Add ("Baz", new ObjCNameIndex ("Zed", 3));
var map = new CSToObjCMap () {
["Foo"] = new ObjCNameIndex ("Bar", 2),
["Baz"] = new ObjCNameIndex ("Zed", 3),
};

</Element>
</CSToObjCMap>";

var reader = new StringReader (text);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var reader = new StringReader (text);
using var reader = new StringReader (text);

Comment on lines 65 to 70
if (map.TryGetValue ("Foo", out var nameIndex)) {
Assert.That (nameIndex.ObjCName, Is.EqualTo ("Bar"), "no bar name");
Assert.That (nameIndex.MapIndex, Is.EqualTo (2), "no bar index");
} else {
Assert.Fail ("no Foo value");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can make things a little cleaner with the following:

Suggested change
if (map.TryGetValue ("Foo", out var nameIndex)) {
Assert.That (nameIndex.ObjCName, Is.EqualTo ("Bar"), "no bar name");
Assert.That (nameIndex.MapIndex, Is.EqualTo (2), "no bar index");
} else {
Assert.Fail ("no Foo value");
}
Assert.True(map.TryGetValue ("Foo", out var nameIndex));
Assert.That (nameIndex.ObjCName, Is.EqualTo ("Bar"), "no bar name");
Assert.That (nameIndex.MapIndex, Is.EqualTo (2), "no bar index");

Comment on lines 72 to 77
if (map.TryGetValue ("Baz", out var nameIndex1)) {
Assert.That (nameIndex1.ObjCName, Is.EqualTo ("Zed"), "no bar name");
Assert.That (nameIndex1.MapIndex, Is.EqualTo (3), "no bar index");
} else {
Assert.Fail ("no Foo value");
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment:

Suggested change
if (map.TryGetValue ("Baz", out var nameIndex1)) {
Assert.That (nameIndex1.ObjCName, Is.EqualTo ("Zed"), "no bar name");
Assert.That (nameIndex1.MapIndex, Is.EqualTo (3), "no bar index");
} else {
Assert.Fail ("no Foo value");
}
Assert.True(map.TryGetValue ("Baz", out var nameIndex1));
Assert.That (nameIndex1.ObjCName, Is.EqualTo ("Zed"), "no bar name");
Assert.That (nameIndex1.MapIndex, Is.EqualTo (3), "no bar index");

static bool DirectoryIsWritable (string path)
{
var info = new DirectoryInfo (path);
return (info.Attributes & FileAttributes.ReadOnly) == 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines 140 to 142
if (classMap.TryGetValue (cl.FullName, out var classPtrField)) {
PatchClassPtrUsage (cl, classPtrField!);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use net7 TryGetValue should have the attribute NotNullWhen in the newer versions and the '!' should not be needed. Mentioned before, but why net6?

Comment on lines 186 to 197
var i = 0;
Instruction? stsfld = null;
while (i < il.Body.Instructions.Count) {
var instr = il.Body.Instructions [i];
// look for
// stsfld class_ptr
if (instr.OpCode == OpCodes.Stsfld && instr.Operand == class_ptr) {
stsfld = instr;
break;
}
i++;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use a for loop, why the use of a while loop? I am a person that makes mistakes, the for loop should help not to forget the index update:

Suggested change
var i = 0;
Instruction? stsfld = null;
while (i < il.Body.Instructions.Count) {
var instr = il.Body.Instructions [i];
// look for
// stsfld class_ptr
if (instr.OpCode == OpCodes.Stsfld && instr.Operand == class_ptr) {
stsfld = instr;
break;
}
i++;
}
int i;
Instruction? stsfld = null;
for (i=0; i < il.Body.Instructions.Count; i++) {
var instr = il.Body.Instructions [i];
// look for
// stsfld class_ptr
if (instr.OpCode == OpCodes.Stsfld && instr.Operand == class_ptr) {
stsfld = instr;
break;
}
}

You can still access the 'i' variable as you do later in the code. We ca even be little more evil (I do not like this option):

Suggested change
var i = 0;
Instruction? stsfld = null;
while (i < il.Body.Instructions.Count) {
var instr = il.Body.Instructions [i];
// look for
// stsfld class_ptr
if (instr.OpCode == OpCodes.Stsfld && instr.Operand == class_ptr) {
stsfld = instr;
break;
}
i++;
}
int i = 0;
Instruction? stsfld = null;
for (; i < il.Body.Instructions.Count; i++) {
var instr = il.Body.Instructions [i];
// look for
// stsfld class_ptr
if (instr.OpCode == OpCodes.Stsfld && instr.Operand == class_ptr) {
stsfld = instr;
break;
}
}

Comment on lines +246 to +247
var instr = il.Body.Instructions [index]!;
var operand = instr.Operand?.ToString () ?? "";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for the '!' here, just fwd the '?' correct?

Suggested change
var instr = il.Body.Instructions [index]!;
var operand = instr.Operand?.ToString () ?? "";
var instr = il.Body.Instructions [index];
var operand = instr?.Operand?.ToString () ?? "";

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree here. When I use the ! I'm trying to say clearly "this cannot be and should never be null and I'm so confident that I will bet the stability of my code on that assertion" whereas the ?. says "well, if it's not there, empty string is OK, which it, in fact, is not. Every array element should have an instruction. Some instructions may or may not have operands.

Comment on lines +255 to +256
var instr = il.Body.Instructions [index]!;
return instr.OpCode == OpCodes.Ldstr;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment, no need to do the ! which is usually code smell regarding to nullability. We can do:

Suggested change
var instr = il.Body.Instructions [index]!;
return instr.OpCode == OpCodes.Ldstr;
var instr = il.Body.Instructions [index];
return instr?.OpCode == OpCodes.Ldstr;

Afaik OpCode is a ref class, so you are no dealing with a Nullable situation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, Cecil is written with prior to nullable and Instruction is possible to be null (but should never be null in this situation, but the analyzer doesn't know that). In this case, the bad smell is in the analyzer in working with legacy code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, but you can move the ! to be a ? and the analyser won't complain and the output will be valid.

Comment on lines +32 to +35
foreach (var elem in elements) {
if (elem.Key is not null && elem.Value is not null)
map.Add (elem.Key, elem.Value);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we add this filter for the key/value in the LINQ expression with a where clause?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started out writing a LINQ expression and it was ugly and not as readable as I liked. This was much more readable. I could have done elements.Where (el => el.Key is not null && elem.Value is not null) but Dictionary doesn't have AddRange so I still need a for loop, or I'd have to do:

elements.Where (el => el.Key is not null && el.Value is not null).ForEach (el => map.Add(el.Key, el.Value));

And now I've reduced it to one line and just made it harder to read (IMHO) and harder to debug.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2
Copy link
Collaborator

✅ API diff for current PR / commit

Legacy Xamarin (No breaking changes)
  • iOS (no change detected)
  • tvOS (no change detected)
  • watchOS (no change detected)
  • macOS (no change detected)
NET (empty diffs)
  • iOS: (empty diff detected)
  • tvOS: (empty diff detected)
  • MacCatalyst: (empty diff detected)
  • macOS: (empty diff detected)

✅ API diff vs stable

Legacy Xamarin (No breaking changes)
.NET (No breaking changes)
Legacy Xamarin (stable) vs .NET

✅ Generator diff

Generator diff is empty

Pipeline on Agent
Hash: cc9a08274209aaaa1973da69f02a08fc4774cbbc [PR build]

@vs-mobiletools-engineering-service2
Copy link
Collaborator

💻 [CI Build] Windows Integration Tests passed 💻

All Windows Integration Tests passed.

Pipeline on Agent
Hash: cc9a08274209aaaa1973da69f02a08fc4774cbbc [PR build]

@vs-mobiletools-engineering-service2
Copy link
Collaborator

📚 [PR Build] Artifacts 📚

Packages generated

View packages

Pipeline on Agent xambot-1199.Ventura
Hash: cc9a08274209aaaa1973da69f02a08fc4774cbbc [PR build]

@vs-mobiletools-engineering-service2
Copy link
Collaborator

💻 [PR Build] Tests on macOS M1 - Mac Ventura (13.0) passed 💻

All tests on macOS M1 - Mac Ventura (13.0) passed.

Pipeline on Agent
Hash: cc9a08274209aaaa1973da69f02a08fc4774cbbc [PR build]

@vs-mobiletools-engineering-service2
Copy link
Collaborator

💻 [PR Build] Tests on macOS M1 - Mac Big Sur (11.5) passed 💻

All tests on macOS M1 - Mac Big Sur (11.5) passed.

Pipeline on Agent
Hash: cc9a08274209aaaa1973da69f02a08fc4774cbbc [PR build]

@vs-mobiletools-engineering-service2
Copy link
Collaborator

🚀 [CI Build] Test results 🚀

Test results

✅ All tests passed on VSTS: simulator tests.

🎉 All 225 tests passed 🎉

Tests counts

✅ bcl: All 69 tests passed. Html Report (VSDrops) Download
✅ cecil: All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests: All 1 tests passed. Html Report (VSDrops) Download
✅ fsharp: All 7 tests passed. Html Report (VSDrops) Download
✅ framework: All 8 tests passed. Html Report (VSDrops) Download
✅ generator: All 2 tests passed. Html Report (VSDrops) Download
✅ interdependent_binding_projects: All 7 tests passed. Html Report (VSDrops) Download
✅ install_source: All 1 tests passed. Html Report (VSDrops) Download
✅ introspection: All 8 tests passed. Html Report (VSDrops) Download
✅ linker: All 65 tests passed. Html Report (VSDrops) Download
✅ mac_binding_project: All 1 tests passed. Html Report (VSDrops) Download
✅ mmp: All 2 tests passed. Html Report (VSDrops) Download
✅ mononative: All 12 tests passed. Html Report (VSDrops) Download
✅ monotouch: All 25 tests passed. Html Report (VSDrops) Download
✅ msbuild: All 2 tests passed. Html Report (VSDrops) Download
✅ mtouch: All 1 tests passed. Html Report (VSDrops) Download
✅ xammac: All 3 tests passed. Html Report (VSDrops) Download
✅ xcframework: All 8 tests passed. Html Report (VSDrops) Download
✅ xtro: All 2 tests passed. Html Report (VSDrops) Download

Pipeline on Agent
Hash: cc9a08274209aaaa1973da69f02a08fc4774cbbc [PR build]

@stephen-hawley stephen-hawley merged commit bef5d47 into xamarin:main Apr 3, 2023
@stephen-hawley stephen-hawley deleted the class-redirector branch April 3, 2023 16:37
static string [] xamarinDlls = new string [] {
"Microsoft.iOS.dll",
"Microsoft.macOS.dll",
"Microsoft.tvOS.dll",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Microsoft.MacCatalyst.dll?

if (index < 0)
return false;
var instr = il.Body.Instructions [index]!;
var operand = instr.Operand?.ToString () ?? "";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're calling ToString on every instruction, which is unnecessary. You only need to call ToString on call instructions, which would be much more performant. This would mean moving the instr.OpCode == OpCodes.Call check to before the ToString call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
not-notes-worthy Ignore for release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants