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

Adding HelloiOS sample for NativeAOT #82249

Merged
merged 10 commits into from
Feb 24, 2023
51 changes: 51 additions & 0 deletions src/mono/sample/iOS-NativeAOT/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
.DEFAULT_GOAL := all

TOP=../../../../

BUILD_CONFIG?=Debug
TARGET_ARCH?=$(shell . $(TOP)eng/native/init-os-and-arch.sh && echo $${arch})
TARGET_OS?=iossimulator
DEPLOY_AND_RUN?=false
STRIP_DEBUG_SYMBOLS?=false

REPO_DIR=$(realpath $(TOP))
TASKS_DIR=$(REPO_DIR)/src/tasks
DOTNET=$(REPO_DIR)/dotnet.sh
BUILD_SCRIPT=$(REPO_DIR)/build.sh

world: build-deps all

# build all dependencies: runtime, nativeAot and all the libs
build-deps: build-runtime-ilc build-libs-all

# building for host
build-runtime-ilc:
$(BUILD_SCRIPT) clr+clr.aot -c $(BUILD_CONFIG)

build-ilc:
kotlarmilos marked this conversation as resolved.
Show resolved Hide resolved
$(BUILD_SCRIPT) clr.aot -c $(BUILD_CONFIG)

# building for target platform
build-libs-all:
$(BUILD_SCRIPT) clr.nativeaotruntime+clr.nativeaotlibs+libs -c $(BUILD_CONFIG) -os $(TARGET_OS) -arch $(TARGET_ARCH)

build-libs-nativeaot:
$(BUILD_SCRIPT) clr.nativeaotruntime+clr.nativeaotlibs -c $(BUILD_CONFIG) -os $(TARGET_OS) -arch $(TARGET_ARCH)

all: appbuilder hello-app

appbuilder:
$(DOTNET) build -c $(BUILD_CONFIG) $(TASKS_DIR)/AppleAppBuilder/AppleAppBuilder.csproj

hello-app: clean
$(DOTNET) \
build -c $(BUILD_CONFIG) \
/p:TargetOS=$(TARGET_OS) \
/p:TargetArchitecture=$(TARGET_ARCH) \
/p:RepoDir=$(REPO_DIR) \
/p:DeployAndRun=$(DEPLOY_AND_RUN) \
/p:StripDebugSymbols=$(STRIP_DEBUG_SYMBOLS) \
-bl
kotlarmilos marked this conversation as resolved.
Show resolved Hide resolved

clean:
rm -rf obj bin
96 changes: 96 additions & 0 deletions src/mono/sample/iOS-NativeAOT/Program.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<OutputPath>bin</OutputPath>
<IntermediateOutputPath>$(MSBuildThisFileDirectory)/obj/</IntermediateOutputPath>
<TargetFramework>net7.0</TargetFramework>
akoeplinger marked this conversation as resolved.
Show resolved Hide resolved
<TargetOS Condition="'$(TargetOS)' == ''">ios</TargetOS>
<TargetOS Condition="'$(TargetsiOSSimulator)' == 'true'">iossimulator</TargetOS>
<DeployAndRun Condition="'$(DeployAndRun)' == ''">true</DeployAndRun>
<RuntimeIdentifier>$(TargetOS)-$(TargetArchitecture)</RuntimeIdentifier>
<AppName>HelloiOS</AppName>
kotlarmilos marked this conversation as resolved.
Show resolved Hide resolved
<StripDebugSymbols Condition="'$(StripDebugSymbols)' == ''">false</StripDebugSymbols>
<!-- NativeAOT-specific props -->
<NativeLib>static</NativeLib>
<CustomNativeMain>true</CustomNativeMain>
<UseNativeAOTRuntime Condition="'$(UseNativeAOTRuntime)' == ''">true</UseNativeAOTRuntime>
<!-- FIXME: Once we support building System.Globalization.Native and icu, should be removed -->
<InvariantGlobalization>true</InvariantGlobalization>
<!-- FIXME: We do not use publish targets yet, but we need to create a publish directory -->
<PublishDir Condition="'$(PublishDir)' == ''">$(OutputPath)/publish</PublishDir>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\iOS\Program.cs" Link="Program.cs" />
<DirectPInvoke Include="__Internal" />
</ItemGroup>

<PropertyGroup Condition="'$(TargetOS)' == 'maccatalyst'">
ivanpovazan marked this conversation as resolved.
Show resolved Hide resolved
<DevTeamProvisioning Condition="'$(TargetOS)' == 'maccatalyst' and '$(DevTeamProvisioning)' == ''">adhoc</DevTeamProvisioning>
kotlarmilos marked this conversation as resolved.
Show resolved Hide resolved
<EnableAppSandbox Condition="'$(EnableAppSandbox)' == ''">false</EnableAppSandbox>
</PropertyGroup>

<Import Project="$(MSBuildThisFileDirectory)../../../coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets" />
akoeplinger marked this conversation as resolved.
Show resolved Hide resolved
<UsingTask TaskName="AppleAppBuilderTask"
AssemblyFile="$(AppleAppBuilderTasksAssemblyPath)" />

<!-- FIXME: Once we set up builing appropriate runtime package for iOS-like platforms the following properties should be removed -->
<PropertyGroup>
<NativeAotFrameworkLibsDir>$(RepoDir)/artifacts/bin/runtime/net8.0-$(TargetOS)-$(Configuration)-$(TargetArchitecture)/</NativeAotFrameworkLibsDir>
akoeplinger marked this conversation as resolved.
Show resolved Hide resolved
<NativeAotRuntimeLibsDir>$(RepoDir)/artifacts/bin/coreclr/$(TargetOS).$(TargetArchitecture).$(Configuration)/</NativeAotRuntimeLibsDir>
<IlcHostArchitecture>$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString().ToLowerInvariant)</IlcHostArchitecture>
<IlcPath>$(RepoDir)/artifacts/bin/coreclr/osx.$(IlcHostArchitecture).$(Configuration)/ilc/</IlcPath>
<IlcToolsPath>$(IlcPath)</IlcToolsPath>
<IlcSdkPath>$(NativeAotRuntimeLibsDir)/aotsdk/</IlcSdkPath>
<IlcFrameworkPath>$(NativeAotFrameworkLibsDir)</IlcFrameworkPath>
<IlcFrameworkNativePath>$(NativeAotFrameworkLibsDir)</IlcFrameworkNativePath>
</PropertyGroup>

<Target Name="BuildAppBundle"
AfterTargets="Build"
DependsOnTargets="SetupProperties;ComputeIlcCompileInputs;IlcCompile">
kotlarmilos marked this conversation as resolved.
Show resolved Hide resolved

<PropertyGroup>
<AppDir>$(MSBuildThisFileDirectory)$(PublishDir)\app</AppDir>
<IosSimulator Condition="'$(TargetsiOSSimulator)' == 'true'">iPhone 11</IosSimulator>
<Optimized Condition="'$(Configuration)' == 'Release'">True</Optimized>
</PropertyGroup>

<ItemGroup>
<NativeLibrary Include="%(ManagedBinary.IlcOutputFile)" />
</ItemGroup>

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

<AppleAppBuilderTask
UseNativeAOTRuntime="$(UseNativeAOTRuntime)"
NativeDependencies="@(NativeLibrary)"
TargetOS="$(TargetOS)"
Arch="$(TargetArchitecture)"
ProjectName="$(AppName)"
Assemblies="@(BundleAssemblies)"
GenerateXcodeProject="True"
BuildAppBundle="True"
DevTeamProvisioning="$(DevTeamProvisioning)"
OutputDirectory="$(AppDir)"
Optimized="$(Optimized)"
InvariantGlobalization="$(InvariantGlobalization)"
StripSymbolTable="$(StripDebugSymbols)"
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)"/>

</Target>

<Target Name="RunAppBundle"
AfterTargets="BuildAppBundle"
Condition="'$(DeployAndRun)' == 'true'">
<Exec Condition="'$(TargetOS)' == 'iossimulator'" Command="dotnet xharness apple run --app=$(AppBundlePath) --targets=ios-simulator-64 --output-directory=/tmp/out" />
<Exec Condition="'$(TargetOS)' == 'ios'" Command="dotnet xharness apple run --app=$(AppBundlePath) --targets=ios-device --output-directory=/tmp/out" />
</Target>

</Project>
55 changes: 55 additions & 0 deletions src/mono/sample/iOS-NativeAOT/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# NativeAOT iOS sample app
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is it under mono folder?

Copy link
Member Author

@ivanpovazan ivanpovazan Feb 20, 2023

Choose a reason for hiding this comment

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

I agree the location is not ideal as I wrote in the description, I wanted to avoid interrupting the repo tree and think that until we have more complete support with NativeAOT it might stay like this, as these samples are generally used as PoC. Later, once we have more complete NativeAOT on iOS support, we should move the sample into a tests directory where it should become a functional test. Nevertheless, I am open for suggestions where to place the application.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

@filipnavara filipnavara Feb 20, 2023

Choose a reason for hiding this comment

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

There's also https://github.com/dotnet/runtime/tree/main/src/samples which may be better than the mono location. I assume the desire is to keep it in the same repository to allow quick iteration of AppleAppBuilder changes.

Copy link
Member Author

Choose a reason for hiding this comment

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

@MichalStrehovsky what do you think?

Copy link
Member

Choose a reason for hiding this comment

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

I don't have a strong opinion. If the sample is not building/testing, it will go stale and stop working quickly. This sample is digging into too many unstable internals to be placed in the dotnet/samples repo.

I don't know about the src/samples directory. It looks like it's something we imported from runtimelab. It's unclear if intentionally; can't find any docs for what it is. If I try to dotnet build the project in it, it doesn't seem to build.

It's not clear to me who the target audience for the sample is. I don't have an opinion on where it should be.

Copy link
Member Author

@ivanpovazan ivanpovazan Feb 21, 2023

Choose a reason for hiding this comment

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

It's not clear to me who the target audience for the sample is.

The target audience, for now, are developers contributing to NativeAOT on iOS work and the sample serves as a verification method to locally test a change (or series of changes).

On the other hand, the sample is also a "proof" that with the current state of dotnet/runtime main, one can build an iOS-like application targeting different platforms with NativeAOT.

Once we have runtime packages ready, they will be referenced instead of referencing internals explicitly.

Copy link
Member

Choose a reason for hiding this comment

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

What I was trying to say is that if this is not building/running, it will go stale and might as well live in a personal repo of one of the devs. It will likely get regularly broken now or in the future. If it's building and running, it would qualify as a test and would go to src/tests. I'm not firm on any of this but I was asked for an opinion :)

Copy link
Member Author

Choose a reason for hiding this comment

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

I understand, and I am grateful for your input! :)
I was just describing the reasoning behind it.
Another thing I forgot the mention, is that the Mono sample, living in: https://github.com/dotnet/runtime/tree/main/src/mono/sample/iOS, is built regularly in size/performance regression tracking, so was thinking that in the future the same approach can be used for NativeAOT as well. This would prevent the sample to go stale.

Copy link
Member

Choose a reason for hiding this comment

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

We had very similar discussion when src/mono/sample was added initially. The problem is that we have overloaded meaning of samples.

  • Product samples that customers can copy&paste and they just work. These samples live in dotnet/samples repo. Samples under src/mono/sample or src/samples are not that. They depend on the rest of the repo, internal details and they are not generally something we would like to see replicated in uncontrolled way.
  • Basic apps that people working on the dotnet/runtime repo find convenient for ad-hoc testing and that are sometimes used for other purposes, like perf testing. They are simple end-to-end tests more than anything else. We use multiple conventions for these projects. src\coreclr\tools\aot\ILCompiler\repro is another example of this kind of convenience project. I think it is confusing that we call these samples.


This sample application should be used as PoC for verifying that NativeAOT can be used for targeting iOS-like platforms:
- ios
- iossimulator
- tvos
- tvossimulator
- maccatalyst

The sample shares the source code with the Mono sample specified at: `../iOS/Program.cs` and in general should have the same behavior as MonoAOT.

## How to build

### Building for the first time

When building for the first time (on a clean checkout) run from this directory the following `make` command:
``` bash
make world
```
This will first build all required runtime components and dependencies, after which it will build the sample app and bundle it into an application bundle.
By default the build will use `Debug` build configuration and target `iossimulator`.
To change this behavior, specify the desired setting in the following way:
``` bash
make world BUILD_CONFIG=Release TARGET_OS=ios
```

### To avoid building all the dependencies

For future builds, you can run just:
``` bash
make
```
which will skip building all the runtime dependencies, assuming those have been already properly built, and build the MSBuild task used for bundling the application and the application it self.

For convenience, it is also possible to rebuild only the application it self with:
``` bash
make hello-app
```

### Deploy and run

To test the application on a simulator include the following in your make command `DEPLOY_AND_RUN=true` e.g.,:
``` bash
make hello-app DEPLOY_AND_RUN=true
```

To test the application on a device, a provisioning profile needs to be specified.
This can be achieved by defining `DevTeamProvisioning` environment variable with a valid team ID (see [developer.apple.com/account/#/membership](https://developer.apple.com/account/#/membership), scroll down to `Team ID`) for example:
``` bash
export DevTeamProvisioning=H1A2B3C4D5; make hello-app TARGET_OS=ios DEPLOY_AND_RUN=true
```

### Custom builds

Check the `Makefile` for individual list of targets and variables to customize the build.
2 changes: 1 addition & 1 deletion src/mono/sample/iOS/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public static async Task Main(string[] args)
delegate* unmanaged<void> unmanagedPtr = &OnButtonClick;
ios_register_button_click(unmanagedPtr);
}
const string msg = "Hello World!\n.NET 5.0";
const string msg = "Hello World!\n.NET 8.0";
for (int i = 0; i < msg.Length; i++)
{
// a kind of an animation
Expand Down
10 changes: 10 additions & 0 deletions src/tasks/AppleAppBuilder/AppleAppBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ public string TargetOS
/// </summary>
public bool UseNativeAOTRuntime { get; set; }

/// <summary>
/// Extra native dependencies to link into the app
/// </summary>
public string[] NativeDependencies { get; set; } = Array.Empty<string>();

public void ValidateRuntimeSelection()
{
if (UseNativeAOTRuntime)
Expand Down Expand Up @@ -267,6 +272,11 @@ public override bool Execute()
}
}

foreach (var nativeDependency in NativeDependencies)
{
assemblerFilesToLink.Add(nativeDependency);
}

if (!ForceInterpreter && (isDevice || ForceAOT) && (assemblerFiles.Count == 0 && !UseNativeAOTRuntime))
{
throw new InvalidOperationException("Need list of AOT files for device builds.");
Expand Down
11 changes: 10 additions & 1 deletion src/tasks/AppleAppBuilder/Templates/CMakeLists.txt.template
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ add_executable(
%ProjectName%
%MainSource%
${APP_RESOURCES}
util.h
util.m
)

if(NOT %UseNativeAOTRuntime%)
Expand Down Expand Up @@ -73,5 +75,12 @@ target_link_libraries(
"-lz"
"-lc++"
"-liconv"
%NativeLibrariesToLink%
%NativeLibrariesToLink%
)

if(%UseNativeAOTRuntime%)
target_link_libraries(
%ProjectName%
"-Wl,-u,_NativeAOT_StaticInitialization"
)
endif()
8 changes: 5 additions & 3 deletions src/tasks/AppleAppBuilder/Templates/main-simple.m
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
#if !USE_NATIVE_AOT
#import "runtime.h"
#else
extern void* NativeAOT_StaticInitialization();
#import "util.h"
extern int __managed__Main(int argc, char* argv[]);
#endif

Expand Down Expand Up @@ -57,8 +57,10 @@ - (void)viewDidLoad {
#if INVARIANT_GLOBALIZATION
setenv ("DOTNET_SYSTEM_GLOBALIZATION_INVARIANT", "1", TRUE);
#endif
NativeAOT_StaticInitialization();
int ret_val = __managed__Main(0, NULL);
char **managed_argv;
int managed_argc = get_managed_args (&managed_argv);
int ret_val = __managed__Main (managed_argc, managed_argv);
free_managed_args (&managed_argv, managed_argc);
akoeplinger marked this conversation as resolved.
Show resolved Hide resolved
#endif
});
}
Expand Down
14 changes: 6 additions & 8 deletions src/tasks/AppleAppBuilder/Templates/runtime.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
#include <stdlib.h>
#include <stdio.h>

#import "util.h"

static char *bundle_path;

#define APPLE_RUNTIME_IDENTIFIER "//%APPLE_RUNTIME_IDENTIFIER%"
Expand Down Expand Up @@ -254,14 +256,8 @@
setenv ("DOTNET_DiagnosticPorts", DIAGNOSTIC_PORTS, true);
#endif

id args_array = [[NSProcessInfo processInfo] arguments];
assert ([args_array count] <= 128);
const char *managed_argv [128];
int argi;
for (argi = 0; argi < [args_array count]; argi++) {
NSString* arg = [args_array objectAtIndex: argi];
managed_argv[argi] = [arg UTF8String];
}
char **managed_argv;
int argi = get_managed_args (&managed_argv);

bool wait_for_debugger = FALSE;

Expand Down Expand Up @@ -373,5 +369,7 @@

mono_jit_cleanup (domain);

free_managed_args (&managed_argv, argi);

exit (res);
}
10 changes: 10 additions & 0 deletions src/tasks/AppleAppBuilder/Templates/util.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#ifndef util_h
#define util_h

size_t get_managed_args (char*** managed_args_array);
void free_managed_args (char*** managed_args_array, size_t array_size);

#endif /* util_h */
52 changes: 52 additions & 0 deletions src/tasks/AppleAppBuilder/Templates/util.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#import <Foundation/Foundation.h>

//---------------------------------------------------------------------------------------
//
// get_managed_args: converts arguments passed to the Objective-C program to their
// C-representation so they can be passed to the managed side.
// The caller is responsible for freeing up the allocated memory.
// This can be achieved by calling the accompanied 'free_managed_args' function.
//
// Arguments:
// * managed_args_array - pointer to array of strings to hold converted arguments.
//
// Return Value:
// int - number of arguments (size of the array of string)
//
size_t get_managed_args (char*** managed_args_array)
{
id args_array = [[NSProcessInfo processInfo] arguments];
size_t args_count = [args_array count];
assert (args_count <= 128);
*managed_args_array = (char**) malloc (sizeof(char**) * args_count);
size_t argi;
for (argi = 0; argi < args_count; argi++) {
NSString* arg = [args_array objectAtIndex: argi];
const char* cstring = [arg UTF8String];
size_t cstring_len = strlen(cstring) + 1;
(*managed_args_array)[argi] = (char*) malloc (sizeof(char*) * cstring_len);
kotlarmilos marked this conversation as resolved.
Show resolved Hide resolved
strcpy((*managed_args_array)[argi], cstring);
}
return argi;
}

//---------------------------------------------------------------------------------------
//
// free_managed_args: frees up the allocated memory for the program arguments.
//
// Arguments:
// * managed_args_array - pointer to array of strings which converted program arguments
// * array_size - number of arguments (size of the array of string)
//
void free_managed_args (char*** managed_args_array, size_t array_size)
{
if (*managed_args_array != NULL)
{
for (size_t i = 0; i < array_size; i++)
free((*managed_args_array)[i]);
free(*managed_args_array);
}
}
3 changes: 3 additions & 0 deletions src/tasks/AppleAppBuilder/Xcode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,9 @@ public string GenerateCMake(
.Replace("%EntryPointLibName%", Path.GetFileName(entryPointLib)));
}

File.WriteAllText(Path.Combine(binDir, "util.h"), Utils.GetEmbeddedResource("util.h"));
File.WriteAllText(Path.Combine(binDir, "util.m"), Utils.GetEmbeddedResource("util.m"));

return binDir;
}

Expand Down