Skip to content

Commit

Permalink
[linker] Remove non-bitcode compatible code, and show a warning.
Browse files Browse the repository at this point in the history
Remove code not currently compatible with bitcode and replace it with an
exception instead (otherwise we'll assert at runtime).

Also show a warning when we detect this.

This is quite helpful when looking at watch device test runs to filter out
failures we already know about.

This fixes point #2 in xamarin#4763.
  • Loading branch information
rolfbjarne committed Feb 5, 2019
1 parent 510bb0b commit be9355c
Show file tree
Hide file tree
Showing 10 changed files with 303 additions and 2 deletions.
6 changes: 6 additions & 0 deletions docs/website/mtouch-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1787,6 +1787,12 @@ Mixed-mode assemblies can not be processed by the linker.

See https://msdn.microsoft.com/en-us/library/x0w2664k.aspx for more information on mixed-mode assemblies.

### MT2105: The method {method} contains a '{handlerType}' exception clause, which is currently not supported when compiling for bitcode. This method will throw an exception if called.

Currently Xamarin.iOS does not support the 'filter' exception clauses when
compiling to bitcode. Any methods containing such code will throw a
NotSupportedException exception.

## MT3xxx: AOT error messages

<!--
Expand Down
44 changes: 44 additions & 0 deletions docs/website/optimizations.md
Original file line number Diff line number Diff line change
Expand Up @@ -604,3 +604,47 @@ It is always enabled by default (as long as the static registrar is enabled).

The default behavior can be overridden by passing
`--optimize=[+|-]static-delegate-to-block-lookup` to mtouch/mmp.

## Remove unsupported IL for bitcode

Removes unsupported IL for bitcode, and replaces it with a `NotSupportedException`.

There are certain types of IL that Xamarin.iOS doesn't support when compiling
to bitcode. This optimization will replace the unsupported IL with an
`NotSupportedException`, and will emit a warning at build time.

This ensures that any unsupported IL will be detected at runtime even when not
compiling to bitcode (in particular it will mean that the behavior between
Debug and Release device builds is identical, since Debug builds do not
compile to bitcode, while Release builds do).

This optimization will change the following code:

```csharp
void Method ()
{
try {
throw new Exception ("FilterMe");
} catch (Exception e) when (e.Message == "FilterMe") {
Console.WriteLine ("filtered");
}
}
```

into the following:

```csharp
void Method ()
{
throw new NotSupportedException ("This method contains IL not supported when compiled to bitcode.");
}
```

This optimization does not require the linker to be enabled, it will process
all assemblies, even those not linked.

It is only applicable to watchOS, and then it's enabled by default when
building for device.

The default behavior can be overridden by passing
`--optimize=[+|-]remove-unsupported-il-for-bitcode` to mtouch/mmp.
70 changes: 70 additions & 0 deletions tests/linker/ios/link sdk/BitcodeTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

using ObjCRuntime;

using NUnit.Framework;

namespace LinkSdk {
[TestFixture]
public class BitcodeTest {
[Test]
public void FilterClauseTest ()
{
var supported = true;
#if __WATCHOS__
if (Runtime.Arch == Arch.DEVICE)
supported = false;
#endif
if (supported) {
Assert.AreEqual (0, FilterClause (), "Filter me");
} else {
Assert.Throws<NotSupportedException> (() => FilterClause (), "Filter me not supported");
}
}

// A method with a filter clause
// mtouch will show a warning for this method (MT2105) when building for watchOS device. This is expected.
static int FilterClause ()
{
try {
throw new Exception ("FilterMe");
} catch (Exception e) when (e.Message == "FilterMe") {
return 0;
} catch {
return 1;
}
}

[Test]
public void FaultClauseTest ()
{
// First assert that the method that is supposed to have a fault clause actually has a fault clause.
// This is somewhat complicated because it's not the method we call that has the fault clause, but a generated method in a generated class.
// This is because we only have an indirect way of making csc produce a fault clause
var enumeratorType = GetType ().GetNestedTypes (BindingFlags.NonPublic).First ((v) => v.Name.Contains ($"<{nameof (FaultClause)}>"));
var method = enumeratorType.GetMethod ("MoveNext", BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
var body = method.GetMethodBody ();
Assert.IsTrue (body.ExceptionHandlingClauses.Any ((v) => v.Flags == ExceptionHandlingClauseOptions.Fault), "Any fault clauses");

// Then assert that the method can be called successfully.
var rv = FaultClause ().ToArray ();
Assert.AreEqual (1, rv.Count (), "Count");
Assert.AreEqual (1, rv [0], "Item 1");

}

// The C# compiler uses fault clauses for 'using' blocks inside iterators.
static IEnumerable<int> FaultClause ()
{
using (var d = new Disposable ()) {
yield return 1;
}
}
class Disposable : IDisposable {
void IDisposable.Dispose () { }
}
}
}
1 change: 1 addition & 0 deletions tests/linker/ios/link sdk/link sdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@
<Compile Include="..\..\CommonLinkSdkTest.cs">
<Link>CommonLinkSdkTest.cs</Link>
</Compile>
<Compile Include="BitcodeTest.cs" />
</ItemGroup>
<ItemGroup>
<InterfaceDefinition Include="LaunchScreen.storyboard" Condition="'$(TargetFrameworkIdentifier)' != 'Xamarin.WatchOS'" />
Expand Down
34 changes: 34 additions & 0 deletions tests/mtouch/MTouch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2910,6 +2910,40 @@ public void MT1216 ()
}
}

[Test]
public void MT2105 ()
{

using (var ext = new MTouchTool ()) {
var code = @"
class TestClass {
// A method with a filter clause
static int FilterClause ()
{
try {
throw new System.Exception (""FilterMe"");
} catch (System.Exception e) when (e.Message == ""FilterMe"") {
return 0;
} catch {
return 1;
}
}
}
";
ext.Profile = Profile.watchOS;
ext.Linker = MTouchLinker.LinkSdk;
ext.CreateTemporaryWatchKitExtension (extraCode: code, extraArg: "/debug");
ext.CreateTemporaryDirectory ();
ext.WarnAsError = new int [] { 2105 };
ext.AssertExecuteFailure (MTouchAction.BuildDev);
ext.AssertError (2105, "The method TestClass.FilterClause contains a 'Filter' exception clause, which is currently not supported when compiling for bitcode. This method will throw an exception if called.", "testApp.cs", 9);

ext.Optimize = new string [] { "remove-unsupported-il-for-bitcode" };
ext.AssertExecuteFailure (MTouchAction.BuildSim);
ext.AssertError (2105, "The method TestClass.FilterClause contains a 'Filter' exception clause, which is currently not supported when compiling for bitcode. This method will throw an exception if called.", "testApp.cs", 9);
}
}

[Test]
public void MT5211 ()
{
Expand Down
7 changes: 5 additions & 2 deletions tests/mtouch/MTouchTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -621,7 +621,7 @@ public void WidgetPerformUpdate (Action<NCUpdateResult> completionHandler)
File.WriteAllText (plist_path, info_plist);
}

public void CreateTemporaryWatchKitExtension (string code = null)
public void CreateTemporaryWatchKitExtension (string code = null, string extraCode = null, string extraArg = "")
{
string testDir;
if (RootAssembly == null) {
Expand All @@ -641,9 +641,12 @@ protected NotificationController (System.IntPtr handle) : base (handle) {}
}";
}

if (extraCode != null)
code += extraCode;

AppPath = app;
Extension = true;
RootAssembly = MTouch.CompileTestAppLibrary (testDir, code: code, profile: Profile);
RootAssembly = MTouch.CompileTestAppLibrary (testDir, code: code, extraArg: extraArg, profile: Profile);

File.WriteAllText (Path.Combine (app, "Info.plist"), @"<?xml version=""1.0"" encoding=""UTF-8""?>
<!DOCTYPE plist PUBLIC ""-//Apple//DTD PLIST 1.0//EN"" ""http://www.apple.com/DTDs/PropertyList-1.0.dtd"">
Expand Down
28 changes: 28 additions & 0 deletions tools/common/Optimizations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public class Optimizations
"", // dummy value to make indices match up between XM and XI
#else
"trim-architectures",
#endif
#if MONOTOUCH
"remove-unsupported-il-for-bitcode",
#else
"", // dummy value to make indices match up between XM and XI
#endif
};

Expand All @@ -44,8 +49,10 @@ enum Opt
StaticBlockToDelegateLookup,
RemoveDynamicRegistrar,
TrimArchitectures,
RemoveUnsupportedILForBitcode,
}

bool? all;
bool? [] values;

public bool? RemoveUIThreadChecks {
Expand Down Expand Up @@ -97,6 +104,13 @@ public bool? TrimArchitectures {
set { values [(int) Opt.TrimArchitectures] = value; }
}

#if MONOTOUCH
public bool? RemoveUnsupportedILForBitcode {
get { return values [(int) Opt.RemoveUnsupportedILForBitcode]; }
set { values [(int) Opt.RemoveUnsupportedILForBitcode] = value; }
}
#endif

public Optimizations ()
{
values = new bool? [opt_names.Length];
Expand Down Expand Up @@ -220,6 +234,19 @@ public void Initialize (Application app)
TrimArchitectures = !app.EnableDebug;
#endif

#if MONOTOUCH
if (!RemoveUnsupportedILForBitcode.HasValue) {
// By default enabled for watchOS device builds.
RemoveUnsupportedILForBitcode = app.Platform == Utils.ApplePlatform.WatchOS && app.IsDeviceBuild;
} else if (RemoveUnsupportedILForBitcode.Value) {
if (app.Platform != Utils.ApplePlatform.WatchOS) {
if (!all.HasValue) // Don't show this warning if it was enabled with --optimize=all
ErrorHelper.Warning (2003, $"Option '--optimize={opt_names [(int) Opt.RemoveUnsupportedILForBitcode]}' will be ignored since it's only applicable to watchOS.");
RemoveUnsupportedILForBitcode = false;
}
}
#endif

if (Driver.Verbosity > 3)
Driver.Log (4, "Enabled optimizations: {0}", string.Join (", ", values.Select ((v, idx) => v == true ? opt_names [idx] : string.Empty).Where ((v) => !string.IsNullOrEmpty (v))));
}
Expand Down Expand Up @@ -254,6 +281,7 @@ void ParseOption (string option)
}

if (opt == "all") {
all = enabled;
for (int i = 0; i < values.Length; i++) {
if (!string.IsNullOrEmpty (opt_names [i]))
values [i] = enabled;
Expand Down
108 changes: 108 additions & 0 deletions tools/linker/MonoTouch.Tuner/RemoveBitcodeIncompatibleCodeStep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2012-2014, 2016 Xamarin Inc. All rights reserved.
using System.Collections.Generic;
using Mono.Tuner;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Linker.Steps;

using Xamarin.Bundler;
using Xamarin.Linker;
using Mono.Linker;
using System;

namespace MonoTouch.Tuner {

public class RemoveBitcodeIncompatibleCodeStep : BaseStep {

LinkerOptions Options;
MethodDefinition nse_ctor_def;
Dictionary<ModuleDefinition, MethodReference> nse_ctors;

public RemoveBitcodeIncompatibleCodeStep (LinkerOptions options)
{
Options = options;
}

protected override void ProcessAssembly (AssemblyDefinition assembly)
{
foreach (var type in assembly.MainModule.Types)
ProcessType (type);
}

void ProcessType (TypeDefinition type)
{
if (type.HasNestedTypes) {
foreach (var nestedType in type.NestedTypes)
ProcessType (nestedType);
}

if (type.HasMethods) {
foreach (var method in type.Methods)
ProcessMethod (method);
}

}

void ProcessMethod (MethodDefinition method)
{
if (!method.HasBody)
return;

var body = method.Body;
if (!body.HasExceptionHandlers)
return;

var anyFilterClauses = false;
foreach (var eh in body.ExceptionHandlers) {
if (eh.HandlerType == ExceptionHandlerType.Filter) {
anyFilterClauses = true;
ErrorHelper.Show (ErrorHelper.CreateWarning (Options.Application, 2105, method, $"The method {method.DeclaringType.FullName}.{method.Name} contains a '{eh.HandlerType}' exception clause, which is currently not supported when compiling for bitcode. This method will throw an exception if called."));
break;
}
}
if (!anyFilterClauses)
return;

body = new MethodBody (method);
var il = body.GetILProcessor ();
il.Emit (OpCodes.Ldstr, "This method contains IL not supported when compiled to bitcode.");
if (nse_ctor_def == null) {
var corlib = Context.GetAssembly ("mscorlib");
var nse = corlib.MainModule.GetType ("System", "NotSupportedException");
foreach (var ctor in nse.GetConstructors ()) {
if (!ctor.HasParameters)
continue;
var parameters = ctor.Parameters;
if (parameters.Count != 1)
continue;
if (!parameters [0].ParameterType.Is ("System", "String"))
continue;
nse_ctor_def = ctor;
break;
}
nse_ctors = new Dictionary<ModuleDefinition, MethodReference> ();
}
MethodReference nse_ctor;
if (!nse_ctors.TryGetValue (method.Module, out nse_ctor)) {
nse_ctors [method.Module] = nse_ctor = method.Module.ImportReference (nse_ctor_def);

// We're processing all assemblies, not linked assemblies, so
// make sure we're saving any changes to non-linked assemblies as well.
var assembly = method.Module.Assembly;
var action = Annotations.GetAction (assembly);
switch (action) {
case AssemblyAction.Link:
case AssemblyAction.Save:
break;
default:
Annotations.SetAction (assembly, AssemblyAction.Save);
break;
}
}
il.Emit (OpCodes.Newobj, nse_ctor);
il.Emit (OpCodes.Throw);
method.Body = body;

}
}
}
4 changes: 4 additions & 0 deletions tools/mtouch/Tuning.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ static Pipeline CreatePipeline (LinkerOptions options)
// We need to store the Field attribute in annotations, since it may end up removed.
pipeline.Append (new ProcessExportedFields ());

// We need to remove incompatible bitcode for all assemblies, not only the linked assemblies.
if (options.Application.Optimizations.RemoveUnsupportedILForBitcode == true)
pipeline.AppendStep (new RemoveBitcodeIncompatibleCodeStep (options));

if (options.LinkMode != LinkMode.None) {
pipeline.Append (new CoreTypeMapStep ());

Expand Down
3 changes: 3 additions & 0 deletions tools/mtouch/mtouch.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,9 @@
<Compile Include="..\..\external\mono\external\linker\linker\Linker\TypeNameParser.cs">
<Link>Linker\TypeNameParser.cs</Link>
</Compile>
<Compile Include="..\linker\MonoTouch.Tuner\RemoveBitcodeIncompatibleCodeStep.cs">
<Link>MonoTouch.Tuner\RemoveBitcodeIncompatibleCodeStep.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<Reference Include="System.Core" />
Expand Down

0 comments on commit be9355c

Please sign in to comment.