Skip to content

Commit

Permalink
[One .NET] *.runtimeconfig.json support (#6009)
Browse files Browse the repository at this point in the history
Context: https://github.com/dotnet/runtime/blob/d95bfea59ed7d19e9b1db096c9d332005989296b/docs/design/mono/mobile-runtimeconfig-json.md

.NET allows including a [`runtimeconfig.json`][0] file in an
application, which can be used to control runtime configuration
options, provide "backing data" to [`AppContext.GetData()`][1],
control configuration values, and more.

Update the Xamarin.Android-for-.NET 6 build process to call the new
[`<RuntimeConfigParser/>` task][2], which will process the
`runtimeconfig.json` files into an efficient binary blob, which will
be added to the `.apk` as `assemblies/rc.bin`.

During process startup, `assemblies/rc.bin` will be proved to
`monovm_runtimeconfig_initialize()`, which will parse `rc.bin`.

Certain `runtimeconfig.json` properties are *reserved*: if they're
specified within `runtimeconfig.json`, then the
`<RuntimeConfigParser/>` task will throw an `ArgumentException`, and
the build will fail.  The reserved properties are controlled by the
`@(_RuntimeConfigReservedProperties)` item group, and are reserved
because the Xamarin.Android runtime needs to control the value of
these properties for proper app execution, e.g. `PINVOKE_OVERRIDE`
(cf84e1b, 0cd890b).

Co-authored-by: Marek Habersack <grendel@twistedcode.net>

[0]: https://docs.microsoft.com/en-us/dotnet/core/run-time-config/#runtimeconfigjson
[1]: https://docs.microsoft.com/en-us/dotnet/api/system.appcontext.getdata?view=net-5.0
[2]: https://github.com/dotnet/runtime/blob/01b7e73cd378145264a7cb7a09365b41ed42b240/src/tasks/RuntimeConfigParser/RuntimeConfigParser.cs
  • Loading branch information
jonathanpeppers committed Jun 14, 2021
1 parent 5da92bd commit a022f5d
Show file tree
Hide file tree
Showing 20 changed files with 195 additions and 10 deletions.
Expand Up @@ -23,5 +23,6 @@ This file is imported *after* the Microsoft.NET.Sdk/Sdk.targets.
<Import Project="Microsoft.Android.Sdk.ILLink.targets" />
<Import Project="Microsoft.Android.Sdk.ProjectCapabilities.targets" />
<Import Project="Microsoft.Android.Sdk.Publish.targets" />
<Import Project="Microsoft.Android.Sdk.RuntimeConfig.targets" />
<Import Project="Microsoft.Android.Sdk.Tooling.targets" />
</Project>
Expand Up @@ -12,7 +12,6 @@
<AndroidHttpClientHandlerType Condition=" '$(AndroidHttpClientHandlerType)' == '' ">Xamarin.Android.Net.AndroidClientHandler</AndroidHttpClientHandlerType>
<AndroidUseIntermediateDesignerFile Condition=" '$(AndroidUseIntermediateDesignerFile)' == '' ">true</AndroidUseIntermediateDesignerFile>
<GenerateDependencyFile Condition=" '$(GenerateDependencyFile)' == '' ">false</GenerateDependencyFile>
<GenerateRuntimeConfigurationFiles Condition=" '$(GenerateRuntimeConfigurationFiles)' == '' ">false</GenerateRuntimeConfigurationFiles>
<CopyLocalLockFileAssemblies Condition=" '$(CopyLocalLockFileAssemblies)' == '' ">false</CopyLocalLockFileAssemblies>
<ComputeNETCoreBuildOutputFiles Condition=" '$(ComputeNETCoreBuildOutputFiles)' == '' ">false</ComputeNETCoreBuildOutputFiles>
<!-- jar2xml is not supported -->
Expand Down
@@ -0,0 +1,50 @@
<!--
***********************************************************************************************
Microsoft.Android.Sdk.RuntimeConfig.targets
MSBuild logic related to *.runtimeconfig.json files.
See: https://github.com/dotnet/runtime/blob/b13715b6984889a709ba29ea8a1961db469f8805/src/mono/nuget/Microsoft.NET.Runtime.RuntimeConfigParser.Task/README.md
***********************************************************************************************
-->
<Project>

<Import Sdk="Microsoft.NET.Runtime.RuntimeConfigParser.Task" Project="Sdk.props" />

<PropertyGroup>
<!-- HACK: workaround https://github.com/dotnet/runtime/issues/53811 -->
<RuntimeConfigParserTasksAssemblyPath>$([System.IO.Path]::GetDirectoryName($(RuntimeConfigParserTasksAssemblyPath)))/net6.0/$([System.IO.Path]::GetFileName($(RuntimeConfigParserTasksAssemblyPath)))</RuntimeConfigParserTasksAssemblyPath>
<_BinaryRuntimeConfigPath>$(IntermediateOutputPath)$(ProjectRuntimeConfigFileName).bin</_BinaryRuntimeConfigPath>
</PropertyGroup>

<ItemGroup>
<!--
See: https://docs.microsoft.com/en-us/dotnet/core/tutorials/netcore-hosting#step-3-%2D-prepare-runtime-properties
These properties shouldn't be used in Xamarin.Android apps as there are no directories the runtime can search,
everything related to assemblies or shared libraries must go through the Xamarin.Android native runtime.
-->
<_RuntimeConfigReservedProperties Include="TRUSTED_PLATFORM_ASSEMBLIES"/>
<_RuntimeConfigReservedProperties Include="APP_PATHS"/>
<_RuntimeConfigReservedProperties Include="APP_NI_PATHS"/>
<_RuntimeConfigReservedProperties Include="NATIVE_DLL_SEARCH_DIRECTORIES"/>
<_RuntimeConfigReservedProperties Include="PLATFORM_RESOURCE_ROOTS"/>
<_RuntimeConfigReservedProperties Include="PINVOKE_OVERRIDE"/>
</ItemGroup>

<Target Name="_ParseRuntimeConfigFiles"
AfterTargets="GenerateBuildRuntimeConfigurationFiles"
Condition=" '$(GenerateRuntimeConfigurationFiles)' == 'true' "
Inputs="$(ProjectRuntimeConfigFilePath)"
Outputs="$(_BinaryRuntimeConfigPath)">
<RuntimeConfigParserTask
RuntimeConfigFile="$(ProjectRuntimeConfigFilePath)"
OutputFile="$(_BinaryRuntimeConfigPath)"
RuntimeConfigReservedProperties="@(_RuntimeConfigReservedProperties)"
/>
<ItemGroup>
<FileWrites Include="$(_BinaryRuntimeConfigPath)" />
</ItemGroup>
</Target>

</Project>
6 changes: 6 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Tasks/BuildApk.cs
Expand Up @@ -89,6 +89,8 @@ public class BuildApk : AndroidTask

public string CheckedBuild { get; set; }

public string RuntimeConfigBinFilePath { get; set; }

[Required]
public string ProjectFullPath { get; set; }

Expand Down Expand Up @@ -190,6 +192,10 @@ void ExecuteWithAbi (string [] supportedAbis, string apkInputPath, string apkOut
}
}

if (!String.IsNullOrEmpty (RuntimeConfigBinFilePath) && File.Exists (RuntimeConfigBinFilePath)) {
AddFileToArchiveIfNewer (apk, RuntimeConfigBinFilePath, $"{AssembliesPath}rc.bin", compressionMethod: UncompressedMethod);
}

int count = 0;
foreach (var file in files) {
var item = Path.Combine (file.archivePath.Replace (Path.DirectorySeparatorChar, '/'));
Expand Down
Expand Up @@ -59,6 +59,7 @@ public class GeneratePackageManagerJava : AndroidTask
[Required]
public bool InstantRunEnabled { get; set; }

public string RuntimeConfigBinFilePath { get; set; }
public string BoundExceptionType { get; set; }

public string PackageNamingPolicy { get; set; }
Expand Down Expand Up @@ -261,6 +262,7 @@ void AddEnvironment ()
throw new InvalidOperationException ($"Unsupported BoundExceptionType value '{BoundExceptionType}'");
}

bool haveRuntimeConfigBlob = !String.IsNullOrEmpty (RuntimeConfigBinFilePath) && File.Exists (RuntimeConfigBinFilePath);
var appConfState = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal<ApplicationConfigTaskState> (ApplicationConfigTaskState.RegisterTaskObjectKey, RegisteredTaskObjectLifetime.Build);
foreach (string abi in SupportedAbis) {
NativeAssemblerTargetProvider asmTargetProvider = GetAssemblyTargetProvider (abi);
Expand All @@ -279,6 +281,7 @@ void AddEnvironment ()
BoundExceptionType = boundExceptionType,
InstantRunEnabled = InstantRunEnabled,
JniAddNativeMethodRegistrationAttributePresent = appConfState != null ? appConfState.JniAddNativeMethodRegistrationAttributePresent : false,
HaveRuntimeConfigBlob = haveRuntimeConfigBlob,
};

using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) {
Expand Down
Expand Up @@ -86,6 +86,7 @@ public void CheckIncludedAssemblies ()
new [] {
"Java.Interop.dll",
"Mono.Android.dll",
"rc.bin",
"System.Private.CoreLib.dll",
"System.Runtime.dll",
"System.Linq.dll",
Expand Down
Expand Up @@ -25,13 +25,14 @@ public sealed class ApplicationConfig
public bool broken_exception_transitions;
public bool instant_run_enabled;
public bool jni_add_native_method_registration_attribute_present;
public bool have_runtime_config_blob;
public byte bound_stream_io_exception_type;
public uint package_naming_policy;
public uint environment_variable_count;
public uint system_property_count;
public string android_package_name;
};
const uint ApplicationConfigFieldCount = 12;
const uint ApplicationConfigFieldCount = 13;

static readonly object ndkInitLock = new object ();
static readonly char[] readElfFieldSeparator = new [] { ' ', '\t' };
Expand Down Expand Up @@ -150,27 +151,32 @@ static ApplicationConfig ReadApplicationConfig (string envFile)
ret.jni_add_native_method_registration_attribute_present = ConvertFieldToBool ("jni_add_native_method_registration_attribute_present", envFile, i, field [1]);
break;

case 7: // bound_stream_io_exception_type: byte / .byte
case 7: // have_runtime_config_blob: bool / .byte
AssertFieldType (envFile, ".byte", field [0], i);
ret.have_runtime_config_blob = ConvertFieldToBool ("have_runtime_config_blob", envFile, i, field [1]);
break;

case 8: // bound_stream_io_exception_type: byte / .byte
AssertFieldType (envFile, ".byte", field [0], i);
ret.bound_stream_io_exception_type = ConvertFieldToByte ("bound_stream_io_exception_type", envFile, i, field [1]);
break;

case 8: // package_naming_policy: uint32_t / .word | .long
case 9: // package_naming_policy: uint32_t / .word | .long
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile}:{i}': {field [0]}");
ret.package_naming_policy = ConvertFieldToUInt32 ("package_naming_policy", envFile, i, field [1]);
break;

case 9: // environment_variable_count: uint32_t / .word | .long
case 10: // environment_variable_count: uint32_t / .word | .long
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile}:{i}': {field [0]}");
ret.environment_variable_count = ConvertFieldToUInt32 ("environment_variable_count", envFile, i, field [1]);
break;

case 10: // system_property_count: uint32_t / .word | .long
case 11: // system_property_count: uint32_t / .word | .long
Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected uint32_t field type in '{envFile}:{i}': {field [0]}");
ret.system_property_count = ConvertFieldToUInt32 ("system_property_count", envFile, i, field [1]);
break;

case 11: // android_package_name: string / [pointer type]
case 12: // android_package_name: string / [pointer type]
Assert.IsTrue (expectedPointerTypes.Contains (field [0]), $"Unexpected pointer field type in '{envFile}:{i}': {field [0]}");
pointers.Add (field [1].Trim ());
break;
Expand Down
Expand Up @@ -412,6 +412,7 @@ public void DotNetBuild (string runtimeIdentifiers, bool isRelease)
"es",
$"{proj.ProjectName}.dll",
$"{proj.ProjectName}.pdb",
$"{proj.ProjectName}.runtimeconfig.json",
$"{proj.ProjectName}.xml",
};
CollectionAssert.AreEqual (expectedFiles, files, $"Expected: {string.Join (";", expectedFiles)}\n Found: {string.Join (";", files)}");
Expand Down Expand Up @@ -574,7 +575,7 @@ public abstract class Foo<TVirtualView, TNativeView> : AbstractViewHandler<TVirt
where TNativeView : Android.Views.View
#else
where TNativeView : class
#endif
#endif
{
protected Foo (PropertyMapper mapper) : base(mapper)
{
Expand Down
Expand Up @@ -22,6 +22,7 @@ class ApplicationConfigNativeAssemblyGenerator : NativeAssemblyGenerator
public global::Android.Runtime.BoundExceptionType BoundExceptionType { get; set; }
public bool InstantRunEnabled { get; set; }
public bool JniAddNativeMethodRegistrationAttributePresent { get; set; }
public bool HaveRuntimeConfigBlob { get; set; }

public PackageNamingPolicy PackageNamingPolicy { get; set; }

Expand Down Expand Up @@ -70,6 +71,9 @@ protected override void WriteSymbols (StreamWriter output)
WriteCommentLine (output, "jni_add_native_method_registration_attribute_present");
size += WriteData (output, JniAddNativeMethodRegistrationAttributePresent);
WriteCommentLine (output, "have_runtime_config_blob");
size += WriteData (output, HaveRuntimeConfigBlob);
WriteCommentLine (output, "bound_exception_type");
size += WriteData (output, (byte)BoundExceptionType);
Expand Down
Expand Up @@ -1567,6 +1567,7 @@ because xbuild doesn't support framework reference assemblies.
PackageNamingPolicy="$(AndroidPackageNamingPolicy)"
BoundExceptionType="$(AndroidBoundExceptionType)"
InstantRunEnabled="$(_InstantRunEnabled)"
RuntimeConfigBinFilePath="$(_BinaryRuntimeConfigPath)"
>
<Output TaskParameter="BuildId" PropertyName="_XamarinBuildId" />
</GeneratePackageManagerJava>
Expand Down Expand Up @@ -2036,7 +2037,8 @@ because xbuild doesn't support framework reference assemblies.
UncompressedFileExtensions="$(AndroidStoreUncompressedFileExtensions)"
ProjectFullPath="$(MSBuildProjectFullPath)"
IncludeWrapSh="$(AndroidIncludeWrapSh)"
CheckedBuild="$(_AndroidCheckedBuild)">
CheckedBuild="$(_AndroidCheckedBuild)"
RuntimeConfigBinFilePath="$(_BinaryRuntimeConfigPath)">
<Output TaskParameter="OutputFiles" ItemName="ApkFiles" />
</BuildApk>
<BuildBaseAppBundle
Expand Down Expand Up @@ -2067,7 +2069,8 @@ because xbuild doesn't support framework reference assemblies.
UncompressedFileExtensions="$(AndroidStoreUncompressedFileExtensions)"
ProjectFullPath="$(MSBuildProjectFullPath)"
IncludeWrapSh="$(AndroidIncludeWrapSh)"
CheckedBuild="$(_AndroidCheckedBuild)">
CheckedBuild="$(_AndroidCheckedBuild)"
RuntimeConfigBinFilePath="$(_BinaryRuntimeConfigPath)">
<Output TaskParameter="OutputFiles" ItemName="BaseZipFile" />
</BuildBaseAppBundle>
<BuildAppBundle
Expand Down
1 change: 1 addition & 0 deletions src/monodroid/jni/application_dso_stub.cc
Expand Up @@ -42,6 +42,7 @@ ApplicationConfig application_config = {
/*.broken_exception_transitions =*/ false,
/*.instant_run_enabled =*/ false,
/*.jni_add_native_method_registration_attribute_present =*/ false,
/*.have_runtime_config_blob =*/ false,
/*.bound_exception_type =*/ 0, // System
/*.package_naming_policy =*/ 0,
/*.environment_variable_count =*/ 0,
Expand Down
13 changes: 13 additions & 0 deletions src/monodroid/jni/embedded-assemblies-zip.cc
Expand Up @@ -51,6 +51,9 @@ EmbeddedAssemblies::zip_load_entries (int fd, const char *apk_name, monodroid_sh
}

dynamic_local_string<SENSIBLE_PATH_MAX> entry_name;
#if defined (NET6)
bool runtime_config_blob_found = false;
#endif // def NET6

// clang-tidy claims we have a leak in the loop:
//
Expand Down Expand Up @@ -87,6 +90,16 @@ EmbeddedAssemblies::zip_load_entries (int fd, const char *apk_name, monodroid_sh
if (strncmp (prefix, file_name, prefix_len) != 0)
continue;

#if defined (NET6)
if (application_config.have_runtime_config_blob && !runtime_config_blob_found) {
if (utils.ends_with (file_name, SharedConstants::RUNTIME_CONFIG_BLOB_NAME)) {
runtime_config_blob_found = true;
runtime_config_blob_mmap = md_mmap_apk_file (fd, data_offset, file_size, file_name, apk_name);
continue;
}
}
#endif // def NET6

// assemblies must be 4-byte aligned, or Bad Things happen
if ((data_offset & 0x3) != 0) {
log_fatal (LOG_ASSEMBLY, "Assembly '%s' is located at bad offset %lu within the .apk\n", file_name, data_offset);
Expand Down
20 changes: 20 additions & 0 deletions src/monodroid/jni/embedded-assemblies.hh
Expand Up @@ -3,6 +3,7 @@
#define INC_MONODROID_EMBEDDED_ASSEMBLIES_H

#include <cstring>
#include <limits>
#include <mono/metadata/object.h>
#include <mono/metadata/assembly.h>

Expand All @@ -12,6 +13,7 @@

#include "strings.hh"
#include "xamarin-app.hh"
#include "cpp-util.hh"

struct TypeMapHeader;

Expand Down Expand Up @@ -71,6 +73,21 @@ namespace xamarin::android::internal {

void set_assemblies_prefix (const char *prefix);

#if defined (NET6)
void get_runtime_config_blob (const char *& area, uint32_t& size) const
{
area = static_cast<char*>(runtime_config_blob_mmap.area);

abort_unless (runtime_config_blob_mmap.size < std::numeric_limits<uint32_t>::max (), "Runtime config binary blob size exceeds %u bytes", std::numeric_limits<uint32_t>::max ());
size = static_cast<uint32_t>(runtime_config_blob_mmap.size);
}

bool have_runtime_config_blob () const
{
return application_config.have_runtime_config_blob && runtime_config_blob_mmap.area != nullptr;
}
#endif

private:
const char* typemap_managed_to_java (MonoType *type, MonoClass *klass, const uint8_t *mvid);
MonoReflectionType* typemap_java_to_managed (const char *java_type_name);
Expand Down Expand Up @@ -140,6 +157,9 @@ namespace xamarin::android::internal {
size_t type_map_count;
#endif // DEBUG || !ANDROID
const char *assemblies_prefix_override = nullptr;
#if defined (NET6)
md_mmap_info runtime_config_blob_mmap{};
#endif // def NET6
};
}

Expand Down
2 changes: 2 additions & 0 deletions src/monodroid/jni/monodroid-glue-internal.hh
Expand Up @@ -204,6 +204,7 @@ namespace xamarin::android::internal
static const char* get_my_location (bool remove_file_name = true);
#endif // defined(WINDOWS) || defined(APPLE_OS_X)
#if defined (NET6)
static void cleanup_runtime_config (MonovmRuntimeConfigArguments *args, void *user_data);
static void* load_library_entry (std::string const& library_name, std::string const& entrypoint_name, pinvoke_api_map_ptr api_map);
static void* fetch_or_create_pinvoke_map_entry (std::string const& library_name, std::string const& entrypoint_name, pinvoke_api_map_ptr api_map, bool need_lock);
static void* monodroid_pinvoke_override (const char *library_name, const char *entrypoint_name);
Expand Down Expand Up @@ -305,6 +306,7 @@ namespace xamarin::android::internal
static pinvoke_api_map xa_pinvoke_map;
static pinvoke_library_map other_pinvoke_map;
static MonoCoreRuntimeProperties monovm_core_properties;
MonovmRuntimeConfigArguments runtime_config_args;
#else // def NET6
static std::mutex api_init_lock;
static void *api_dso_handle;
Expand Down
31 changes: 31 additions & 0 deletions src/monodroid/jni/monodroid-glue.cc
Expand Up @@ -843,6 +843,20 @@ MonodroidRuntime::mono_runtime_init ([[maybe_unused]] dynamic_local_string<PROPE
#endif
}

#if defined (NET6)
void
MonodroidRuntime::cleanup_runtime_config (MonovmRuntimeConfigArguments *args, [[maybe_unused]] void *user_data)
{
if (args == nullptr || args->kind != 1 || args->runtimeconfig.data.data == nullptr) {
return;
}

#if !defined (WINDOWS)
munmap (static_cast<void*>(const_cast<char*>(args->runtimeconfig.data.data)), args->runtimeconfig.data.data_len);
#endif // ndef WINDOWS
}
#endif // def NET6

MonoDomain*
MonodroidRuntime::create_domain (JNIEnv *env, jstring_array_wrapper &runtimeApks, bool is_root_domain)
{
Expand All @@ -855,6 +869,23 @@ MonodroidRuntime::create_domain (JNIEnv *env, jstring_array_wrapper &runtimeApks

gather_bundled_assemblies (runtimeApks, &user_assemblies_count);

#if defined (NET6)
timing_period blob_time;
if (XA_UNLIKELY (utils.should_log (LOG_TIMING)))
blob_time.mark_start ();

if (embeddedAssemblies.have_runtime_config_blob ()) {
runtime_config_args.kind = 1;
embeddedAssemblies.get_runtime_config_blob (runtime_config_args.runtimeconfig.data.data, runtime_config_args.runtimeconfig.data.data_len);
monovm_runtimeconfig_initialize (&runtime_config_args, cleanup_runtime_config, nullptr);
}

if (XA_UNLIKELY (utils.should_log (LOG_TIMING))) {
blob_time.mark_end ();
Timing::info (blob_time, "Register runtimeconfig binary blob");
}
#endif // def NET6

if (!have_mono_mkbundle_init && user_assemblies_count == 0 && androidSystem.count_override_assemblies () == 0 && !is_running_on_desktop) {
log_fatal (LOG_DEFAULT, "No assemblies found in '%s' or '%s'. Assuming this is part of Fast Deployment. Exiting...",
androidSystem.get_override_dir (0),
Expand Down
4 changes: 4 additions & 0 deletions src/monodroid/jni/shared-constants.hh
Expand Up @@ -15,6 +15,10 @@ namespace xamarin::android::internal
class SharedConstants
{
public:
#if defined (NET6)
static constexpr char RUNTIME_CONFIG_BLOB_NAME[] = "rc.bin";
#endif // def NET6

#if defined (ANDROID) || defined (__linux__) || defined (__linux)
static constexpr char MONO_SGEN_SO[] = "libmonosgen-2.0.so";
static constexpr char MONO_SGEN_ARCH_SO[] = "libmonosgen-" __BITNESS__ "-2.0.so";
Expand Down
1 change: 1 addition & 0 deletions src/monodroid/jni/xamarin-app.hh
Expand Up @@ -105,6 +105,7 @@ struct ApplicationConfig
bool broken_exception_transitions;
bool instant_run_enabled;
bool jni_add_native_method_registration_attribute_present;
bool have_runtime_config_blob;
uint8_t bound_exception_type;
uint32_t package_naming_policy;
uint32_t environment_variable_count;
Expand Down

0 comments on commit a022f5d

Please sign in to comment.