Adding HelloiOS sample for NativeAOT (#82249)
authorIvan Povazan <55002338+ivanpovazan@users.noreply.github.com>
Fri, 24 Feb 2023 16:42:55 +0000 (17:42 +0100)
committerGitHub <noreply@github.com>
Fri, 24 Feb 2023 16:42:55 +0000 (17:42 +0100)
- The application is intended to be used by developers who work on enabling NativeAOT on iOS-like platforms and can serve as PoC
- Currently still relies on dotnet/runtime internals and requires local builds instead of package references

src/mono/sample/iOS-NativeAOT/Makefile [new file with mode: 0644]
src/mono/sample/iOS-NativeAOT/Program.csproj [new file with mode: 0644]
src/mono/sample/iOS-NativeAOT/README.md [new file with mode: 0644]
src/mono/sample/iOS/Program.cs
src/tasks/AppleAppBuilder/AppleAppBuilder.cs
src/tasks/AppleAppBuilder/Templates/CMakeLists.txt.template
src/tasks/AppleAppBuilder/Templates/main-simple.m
src/tasks/AppleAppBuilder/Templates/runtime.m
src/tasks/AppleAppBuilder/Templates/util.h [new file with mode: 0644]
src/tasks/AppleAppBuilder/Templates/util.m [new file with mode: 0644]
src/tasks/AppleAppBuilder/Xcode.cs

diff --git a/src/mono/sample/iOS-NativeAOT/Makefile b/src/mono/sample/iOS-NativeAOT/Makefile
new file mode 100644 (file)
index 0000000..e1e295a
--- /dev/null
@@ -0,0 +1,50 @@
+.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:
+       $(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:DeployAndRun=$(DEPLOY_AND_RUN) \
+       -p:StripDebugSymbols=$(STRIP_DEBUG_SYMBOLS) \
+       -bl
+
+clean:
+       rm -rf obj bin
\ No newline at end of file
diff --git a/src/mono/sample/iOS-NativeAOT/Program.csproj b/src/mono/sample/iOS-NativeAOT/Program.csproj
new file mode 100644 (file)
index 0000000..aa08743
--- /dev/null
@@ -0,0 +1,95 @@
+<Project Sdk="Microsoft.NET.Sdk">
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <OutputPath>bin</OutputPath>
+    <IntermediateOutputPath>$(MSBuildThisFileDirectory)/obj/</IntermediateOutputPath>
+    <TargetFramework>$(NetCoreAppCurrent)</TargetFramework>
+    <TargetOS Condition="'$(TargetOS)' == ''">ios</TargetOS>
+    <TargetOS Condition="'$(TargetsiOSSimulator)' == 'true'">iossimulator</TargetOS>
+    <DeployAndRun Condition="'$(DeployAndRun)' == ''">true</DeployAndRun>
+    <RuntimeIdentifier>$(TargetOS)-$(TargetArchitecture)</RuntimeIdentifier>
+    <AppName>HelloiOS</AppName>
+    <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'">
+    <DevTeamProvisioning Condition="'$(DevTeamProvisioning)' == ''">adhoc</DevTeamProvisioning>
+    <EnableAppSandbox Condition="'$(EnableAppSandbox)' == ''">false</EnableAppSandbox>
+  </PropertyGroup>
+
+  <Import Project="$(CoreClrProjectRoot)nativeaot\BuildIntegration\Microsoft.NETCore.Native.targets" />
+  <UsingTask TaskName="AppleAppBuilderTask"
+             AssemblyFile="$(AppleAppBuilderTasksAssemblyPath)" />
+
+  <!-- FIXME: Once we set up builing appropriate runtime package for iOS-like platforms the following properties should be removed -->
+  <Target Name="ConfigureIlcPathsForiOSCrossCompilation">
+      <PropertyGroup>
+        <IlcPath>$([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'artifacts', 'bin', 'coreclr', '$(HostOS).$(BuildArchitecture).$(CoreCLRConfiguration)', 'ilc'))</IlcPath>
+        <IlcToolsPath>$(IlcPath)</IlcToolsPath>
+        <IlcSdkPath>$(CoreCLRAotSdkDir)</IlcSdkPath>
+        <IlcFrameworkPath>$(LibrariesAllBinArtifactsPath)</IlcFrameworkPath>
+        <IlcFrameworkNativePath>$(LibrariesAllBinArtifactsPath)</IlcFrameworkNativePath>
+    </PropertyGroup>
+  </Target>
+
+  <Target Name="BuildAppBundle"
+    AfterTargets="Build"
+    DependsOnTargets="ConfigureIlcPathsForiOSCrossCompilation;SetupProperties;ComputeIlcCompileInputs;IlcCompile">
+
+    <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>
\ No newline at end of file
diff --git a/src/mono/sample/iOS-NativeAOT/README.md b/src/mono/sample/iOS-NativeAOT/README.md
new file mode 100644 (file)
index 0000000..fe6fcb1
--- /dev/null
@@ -0,0 +1,79 @@
+# NativeAOT iOS sample app
+
+## Description
+
+This sample application is intended to be used by developers who work on enabling NativeAOT on iOS-like platforms and can serve as PoC for verifying support for the following systems:
+- 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.
+
+## Limitations
+
+The application is **_currently_** relying on the following:
+1. Internal dependencies - locally building the internals is required as runtime and tools nuget packages are still not being produced
+2. Invariant globalization - `System.Globalization.Native` is currently not being built as part of NativeAOT framework for iOS-like platforms
+3. No publish targets - the SDK and MSBuild integration is still not complete
+
+## How to build and test
+
+### 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
+
+#### Simulator
+
+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
+```
+
+#### Device
+
+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=A1B2C3D4E5; make hello-app TARGET_OS=ios DEPLOY_AND_RUN=true
+```
+Assuming `A1B2C3D4E5` is a valid team ID.
+
+#### One-liner
+
+On a clean dotnet/runtime checkout, from this directory, run:
+
+``` bash
+export DevTeamProvisioning=A1B2C3D4E5; make world BUILD_CONFIG=Release TARGET_OS=ios DEPLOY_AND_RUN=true
+```
+
+This command will build everything necessary to run and deploy the application on an iOS device.
+
+### Custom builds
+
+Check the `Makefile` for individual list of targets and variables to customize the build.
index 603ff2e..1c9839a 100644 (file)
@@ -35,7 +35,7 @@ public static class Program
             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
index 6182f97..785c406 100644 (file)
@@ -171,6 +171,11 @@ public class AppleAppBuilderTask : Task
     /// </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)
@@ -267,6 +272,11 @@ public class AppleAppBuilderTask : Task
             }
         }
 
+        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.");
index aba0797..3053a7e 100644 (file)
@@ -11,6 +11,8 @@ add_executable(
     %ProjectName%
     %MainSource%
     ${APP_RESOURCES}
+    util.h
+    util.m
 )
 
 if(NOT %UseNativeAOTRuntime%)
@@ -73,5 +75,12 @@ target_link_libraries(
     "-lz"
     "-lc++"
     "-liconv"
-%NativeLibrariesToLink%
+    %NativeLibrariesToLink%
 )
+
+if(%UseNativeAOTRuntime%)
+target_link_libraries(
+    %ProjectName%
+    "-Wl,-u,_NativeAOT_StaticInitialization"
+)
+endif()
\ No newline at end of file
index 270bf23..f34d4c9 100644 (file)
@@ -5,7 +5,8 @@
 #if !USE_NATIVE_AOT
 #import "runtime.h"
 #else
-extern void* NativeAOT_StaticInitialization();
+#import <os/log.h>
+#import "util.h"
 extern int __managed__Main(int argc, char* argv[]);
 #endif
 
@@ -57,8 +58,12 @@ void (*clickHandlerPtr)(void);
 #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);
+        os_log_info (OS_LOG_DEFAULT, EXIT_CODE_TAG ": %d", ret_val);
+        exit (ret_val);
 #endif
     });
 }
index 1d0cd6f..9cc3879 100644 (file)
 #include <stdlib.h>
 #include <stdio.h>
 
+#import "util.h"
+
 static char *bundle_path;
 
 #define APPLE_RUNTIME_IDENTIFIER "//%APPLE_RUNTIME_IDENTIFIER%"
 
 #define RUNTIMECONFIG_BIN_FILE "runtimeconfig.bin"
 
-// XHarness is looking for this tag in app's output to determine the exit code
-#define EXIT_CODE_TAG "DOTNET.APP_EXIT_CODE"
-
 const char *
 get_bundle_path (void)
 {
@@ -254,14 +253,8 @@ mono_ios_runtime_init (void)
     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;
 
@@ -373,5 +366,7 @@ mono_ios_runtime_init (void)
 
     mono_jit_cleanup (domain);
 
+    free_managed_args (&managed_argv, argi);
+
     exit (res);
 }
diff --git a/src/tasks/AppleAppBuilder/Templates/util.h b/src/tasks/AppleAppBuilder/Templates/util.h
new file mode 100644 (file)
index 0000000..c81697a
--- /dev/null
@@ -0,0 +1,13 @@
+// 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
+
+// XHarness is looking for this tag in app's output to determine the exit code
+#define EXIT_CODE_TAG "DOTNET.APP_EXIT_CODE"
+
+size_t get_managed_args (char*** managed_args_array);
+void free_managed_args (char*** managed_args_array, size_t array_size);
+
+#endif /* util_h */
diff --git a/src/tasks/AppleAppBuilder/Templates/util.m b/src/tasks/AppleAppBuilder/Templates/util.m
new file mode 100644 (file)
index 0000000..b80b56d
--- /dev/null
@@ -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);
+        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);
+    }
+}
index ea6e0dd..4561df0 100644 (file)
@@ -456,6 +456,9 @@ internal sealed class Xcode
                     .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;
     }