Skip to content

Commit

Permalink
[mbr] Add Apple sample (#50740)
Browse files Browse the repository at this point in the history
* [mbr] Add an apple sample

   Works on iOS simulator and Mac Catalyst

* delete unused DeltaHelper code

   Now that System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate is in the BCL officially, I don't need the reflection-based invoke code anymore
  • Loading branch information
lambdageek authored Apr 6, 2021
1 parent 9089931 commit 281be9e
Show file tree
Hide file tree
Showing 9 changed files with 324 additions and 41 deletions.
41 changes: 1 addition & 40 deletions src/mono/sample/mbr/DeltaHelper/DeltaHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,9 @@

namespace MonoDelta {
public class DeltaHelper {
private static Action<Assembly, byte[], byte[], byte[]> _updateMethod;

private static Action<Assembly, byte[], byte[], byte[]> UpdateMethod => _updateMethod ?? InitUpdateMethod();

private static Action<Assembly, byte[], byte[], byte[]> InitUpdateMethod ()
{
var monoType = typeof(System.Reflection.Metadata.AssemblyExtensions);
const string methodName = "ApplyUpdate";
var mi = monoType.GetMethod (methodName, BindingFlags.Public | BindingFlags.Static);
if (mi == null)
throw new Exception ($"Couldn't get {methodName} from {monoType.FullName}");
_updateMethod = MakeUpdateMethod (mi); //Delegate.CreateDelegate (typeof(Action<Assembly, byte[], byte[], byte[]>), mi) as Action<Assembly, byte[], byte[], byte[]>;
return _updateMethod;
}

private static Action<Assembly, byte[], byte[], byte[]> MakeUpdateMethod (MethodInfo applyUpdate)
{
// Make
// void ApplyUpdateArray (Assembly a, byte[] dmeta, byte[] dil, byte[] dpdb)
// {
// ApplyUpdate (a, (ReadOnlySpan<byte>)dmeta, (ReadOnlySpan<byte>)dil, (ReadOnlySpan<byte>)dpdb);
// }
var dm = new DynamicMethod ("CallApplyUpdate", typeof(void), new Type[] { typeof(Assembly), typeof(byte[]), typeof(byte[]), typeof(byte[])}, typeof (DeltaHelper).Module);
var ilg = dm.GetILGenerator ();
var conv = typeof(ReadOnlySpan<byte>).GetMethod("op_Implicit", new Type[] {typeof(byte[])});

ilg.Emit (OpCodes.Ldarg_0);
ilg.Emit (OpCodes.Ldarg_1);
ilg.Emit (OpCodes.Call, conv);
ilg.Emit (OpCodes.Ldarg_2);
ilg.Emit (OpCodes.Call, conv);
ilg.Emit (OpCodes.Ldarg_3);
ilg.Emit (OpCodes.Call, conv);
ilg.Emit (OpCodes.Call, applyUpdate);
ilg.Emit (OpCodes.Ret);

return dm.CreateDelegate(typeof(Action<Assembly, byte[], byte[], byte[]>)) as Action<Assembly, byte[], byte[], byte[]>;
}

private static void LoadMetadataUpdate (Assembly assm, byte[] dmeta_data, byte[] dil_data, byte[] dpdb_data)
{
UpdateMethod (assm, dmeta_data, dil_data, dpdb_data);
System.Reflection.Metadata.AssemblyExtensions.ApplyUpdate (assm, dmeta_data, dil_data, dpdb_data);
}

DeltaHelper () { }
Expand Down
27 changes: 26 additions & 1 deletion src/mono/sample/mbr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,19 @@ For WebAssembly:

Make sure `EMSDK_PATH` is set (see [workflow](../../../../docs/workflow/building/libraries/webassembly-instructions.md))
```console
build.sh --os browser /p:MonoMetadataUpdate=true
build.sh --os browser
```

For Apple targets:

```console
build.sh --os MacCatalyst -s Mono+Libs
```

or

```console
build.sh --os iOSSimulator -s Mono+Libs
```

## Running
Expand All @@ -50,3 +62,16 @@ make CONFIG=Debug && make CONFIG=Debug run
```

Then go to http://localhost:8000/ and click the button once or twice (the example has 2 updates prebuilt)

For Apple targets:

for ios simulator
```
make run-sim
```

for Mac Catalyst

```
make run-catalyst
```
95 changes: 95 additions & 0 deletions src/mono/sample/mbr/apple/AppleDelta.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<OutputPath>bin</OutputPath>
<TargetFramework>$(NetCoreAppToolCurrent)</TargetFramework>
<MicrosoftNetCoreAppRuntimePackDir>$(ArtifactsBinDir)microsoft.netcore.app.runtime.$(TargetOS.ToLower())-$(TargetArchitecture)\$(Configuration)\runtimes\$(TargetOS.ToLower())-$(TargetArchitecture)\</MicrosoftNetCoreAppRuntimePackDir>
<EnableTargetingPackDownload>false</EnableTargetingPackDownload>
<RuntimeIdentifier>$(TargetOS.ToLower())-$(TargetArchitecture)</RuntimeIdentifier>
<DefineConstants Condition="'$(ArchiveTests)' == 'true'">$(DefineConstants);CI_TEST</DefineConstants>
<MonoForceInterpreter>true</MonoForceInterpreter>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
</PropertyGroup>

<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="ChangeablePart.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DeltaHelper\DeltaHelper.csproj" />
</ItemGroup>

<PropertyGroup>
<DeltaScript>deltascript.json</DeltaScript>
<DeltaCount>1</DeltaCount>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetOS)' == 'MacCatalyst'">
<DevTeamProvisioning Condition="'$(TargetOS)' == 'MacCatalyst' and '$(DevTeamProvisioning)' == ''">-</DevTeamProvisioning>
</PropertyGroup>

<!-- Redirect 'dotnet publish' to in-tree runtime pack -->
<Target Name="TrickRuntimePackLocation" AfterTargets="ProcessFrameworkReferences">
<ItemGroup>
<RuntimePack>
<PackageDirectory>$(ArtifactsBinDir)microsoft.netcore.app.runtime.$(RuntimeIdentifier)\$(Configuration)</PackageDirectory>
</RuntimePack>
</ItemGroup>
<Message Text="Packaged ID: %(RuntimePack.PackageDirectory)" Importance="high" />
</Target>

<Import Project="$(RepoTasksDir)AotCompilerTask\MonoAOTCompiler.props" />
<UsingTask TaskName="AppleAppBuilderTask"
AssemblyFile="$(AppleAppBuilderTasksAssemblyPath)" />

<Target Name="BuildAppBundle" AfterTargets="CopyFilesToPublishDirectory">
<PropertyGroup>
<AppDir>$(MSBuildThisFileDirectory)$(PublishDir)\app</AppDir>
<IosSimulator Condition="'$(TargetsiOSSimulator)' == 'true'">iPhone 11</IosSimulator>
<Optimized Condition="'$(Configuration)' == 'Release'">True</Optimized>
<RunAOTCompilation Condition="('$(TargetsMacCatalyst)' == 'false' and '$(IosSimulator)' == '') or '$(ForceAOT)' == 'true'">true</RunAOTCompilation>
</PropertyGroup>

<Error Condition="'$(TargetOS)' == ''" Text="The TargetOS property must be set outside the project file" />

<RemoveDir Directories="$(AppDir)" />

<ItemGroup>
<BundleAssemblies Condition="'$(RunAOTCompilation)' != 'true'" Include="$(MSBuildThisFileDirectory)$(PublishDir)\*.dll" />
</ItemGroup>

<AppleAppBuilderTask
TargetOS="$(TargetOS)"
Arch="$(TargetArchitecture)"
ProjectName="AppleDelta"
MonoRuntimeHeaders="$(MicrosoftNetCoreAppRuntimePackDir)native\include\mono-2.0"
Assemblies="@(BundleAssemblies)"
NativeMainSource="$(MSBuildThisFileDirectory)\main.m"
MainLibraryFileName="AppleDelta.dll"
GenerateXcodeProject="True"
BuildAppBundle="True"
DevTeamProvisioning="$(DevTeamProvisioning)"
OutputDirectory="$(AppDir)"
Optimized="$(Optimized)"
ForceAOT="$(ForceAOT)"
ForceInterpreter="$(MonoForceInterpreter)"
AppDir="$(MSBuildThisFileDirectory)$(PublishDir)">
<Output TaskParameter="AppBundlePath" PropertyName="AppBundlePath" />
<Output TaskParameter="XcodeProjectPath" PropertyName="XcodeProjectPath" />
</AppleAppBuilderTask>

<Message Importance="High" Text="Xcode: $(XcodeProjectPath)"/>
<Message Importance="High" Text="App: $(AppBundlePath)"/>

<Exec Condition="'$(TargetOS)' == 'iOSSimulator'" Command="dotnet xharness apple run --app=$(AppBundlePath) --targets=ios-simulator-64 --output-directory=/tmp/out" />

<!-- run on MacCatalyst -->
<Exec Condition="'$(TargetOS)' == 'MacCatalyst'" Command="dotnet xharness apple run --app=$(AppBundlePath) --targets=maccatalyst --output-directory=/tmp/out" />

</Target>

<!-- Set RoslynILDiffFullPath property to the path of roslynildiff -->
<Import Project="..\DeltaHelper.targets" />

</Project>
9 changes: 9 additions & 0 deletions src/mono/sample/mbr/apple/ChangeablePart.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

public class ChangeablePart
{
public static int UpdateCounter (ref int counter)
{
return ++counter;
}
}
9 changes: 9 additions & 0 deletions src/mono/sample/mbr/apple/ChangeablePart_v1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System;

public class ChangeablePart
{
public static int UpdateCounter (ref int counter)
{
return --counter;
}
}
14 changes: 14 additions & 0 deletions src/mono/sample/mbr/apple/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
CONFIG=Debug
MONO_ARCH=x64
DOTNET := ../../../../../dotnet.sh

run-sim:
$(DOTNET) publish -c $(CONFIG) /p:TargetOS=iOSSimulator /p:TargetArchitecture=$(MONO_ARCH) \
/p:UseLLVM=False /p:ForceAOT=False /p:MonoForceInterpreter=true

run-catalyst:
$(DOTNET) publish -c $(CONFIG) /p:TargetOS=MacCatalyst /p:TargetArchitecture=$(MONO_ARCH) \
/p:UseLLVM=False /p:ForceAOT=False /p:MonoForceInterpreter=true

clean:
rm -rf bin
59 changes: 59 additions & 0 deletions src/mono/sample/mbr/apple/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.InteropServices;

public static class Program
{
// Defined in main.m
[DllImport("__Internal")]
private static extern void ios_set_text(string value);

[DllImport("__Internal")]
unsafe private static extern void ios_register_button_click(delegate* unmanaged<void> callback);

[DllImport("__Internal")]
unsafe private static extern void ios_register_applyupdate_click(delegate* unmanaged<void> callback);

private static int counter = 0;

// Called by native code, see main.m
[UnmanagedCallersOnly]
private static void OnButtonClick()
{
ios_set_text("OnButtonClick! #" + ChangeablePart.UpdateCounter (ref counter));
}

[UnmanagedCallersOnly]
private static void OnApplyUpdateClick()
{
deltaHelper.Update (typeof(ChangeablePart).Assembly);
}

static MonoDelta.DeltaHelper deltaHelper;

public static async Task Main(string[] args)
{
unsafe {
// Register a managed callback (will be called by UIButton, see main.m)
delegate* unmanaged<void> unmanagedPtr = &OnButtonClick;
ios_register_button_click(unmanagedPtr);
delegate* unmanaged<void> unmanagedPtr2 = &OnApplyUpdateClick;
ios_register_applyupdate_click(unmanagedPtr2);
}
deltaHelper = MonoDelta.DeltaHelper.Make();
const string msg = "Hello World!\n.NET 5.0";
for (int i = 0; i < msg.Length; i++)
{
// a kind of an animation
ios_set_text(msg.Substring(0, i + 1));
await Task.Delay(100);
}

Console.WriteLine("Done!");
await Task.Delay(-1);
}
}
5 changes: 5 additions & 0 deletions src/mono/sample/mbr/apple/deltascript.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"changes": [
{"document": "ChangeablePart.cs", "update": "ChangeablePart_v1.cs"},
]
}
106 changes: 106 additions & 0 deletions src/mono/sample/mbr/apple/main.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#include <stdlib.h>

#import <UIKit/UIKit.h>
#import "runtime.h"

@interface ViewController : UIViewController
@end

@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) ViewController *controller;
@end

@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.controller = [[ViewController alloc] initWithNibName:nil bundle:nil];
self.window.rootViewController = self.controller;
[self.window makeKeyAndVisible];
return YES;
}
@end

UILabel *label;
void (*clickHandlerPtr)(void);
void (*clickHandlerApplyUpdatePtr)(void);

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

label = [[UILabel alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
label.textColor = [UIColor greenColor];
label.font = [UIFont boldSystemFontOfSize: 30];
label.numberOfLines = 2;
label.textAlignment = NSTextAlignmentCenter;
label.text = @"Hello, wire me up!\n(dllimport ios_set_text)";
[self.view addSubview:label];

UIButton *button = [UIButton buttonWithType:UIButtonTypeInfoDark];
[button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
[button setFrame:CGRectMake(50, 250, 200, 50)];
[button setTitle:@"Click me (wire me up)" forState:UIControlStateNormal];
[button setExclusiveTouch:YES];
[self.view addSubview:button];

UIButton *apply_button = [UIButton buttonWithType:UIButtonTypeInfoDark];
[apply_button addTarget:self action:@selector(applyUpdateButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
[apply_button setFrame:CGRectMake(50, 300, 200, 50)];
[apply_button setTitle:@"ApplyUpdate" forState:UIControlStateNormal];
[apply_button setExclusiveTouch:YES];
[self.view addSubview:apply_button];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
mono_ios_runtime_init ();
});
}
-(void) buttonClicked:(UIButton*)sender
{
if (clickHandlerPtr)
clickHandlerPtr();
}

-(void) applyUpdateButtonClicked:(UIButton*)sender
{
if (clickHandlerApplyUpdatePtr)
clickHandlerApplyUpdatePtr();
}

@end

// called from C# sample
void
ios_register_button_click (void* ptr)
{
clickHandlerPtr = ptr;
}

// called from C# sample
void
ios_register_applyupdate_click (void* ptr)
{
clickHandlerApplyUpdatePtr = ptr;
}

// called from C# sample
void
ios_set_text (const char* value)
{
NSString* nsstr = [NSString stringWithUTF8String:strdup(value)];
dispatch_async(dispatch_get_main_queue(), ^{
label.text = nsstr;
});
}

int main(int argc, char * argv[]) {
@autoreleasepool {
setenv("DOTNET_MODIFIABLE_ASSEMBLIES", "Debug", 1);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

0 comments on commit 281be9e

Please sign in to comment.