Skip to content

Commit

Permalink
[mono] Implement public hot reload API (dotnet#48380)
Browse files Browse the repository at this point in the history
* [mono] Implement public hot reload API

Also add ApplyUpdateSdb that takes byte[] arguments for use with
mono/debugger-libs as its awkward to call a ROS<byte> method from the debugger.

Contributes to dotnet#45689

* Update src/mono/System.Private.CoreLib/src/System/Reflection/Metadata/AssemblyExtensions.cs

Co-authored-by: Marek Safar <[email protected]>

* Don't allow changes to System.Private.CoreLib

* formatting

* more formatting

* Add a TODO to remove ApplyUpdateSdb

* update DeltaHelper

* don't use ApplyUpdateSdb in DeltaHelper

* throw InvalidOpertionException for corlib changes with hot reload disabled

* Run the test on mono on all platoforms; CoreCLR windows only

* Use resource string

* Address review feedback

Co-authored-by: Marek Safar <[email protected]>
  • Loading branch information
lambdageek and marek-safar authored Feb 19, 2021
1 parent 2e2eccc commit 629ad50
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3679,4 +3679,7 @@
<data name="InvalidOperation_EditFailed" xml:space="preserve">
<value>The assembly update failed.</value>
</data>
</root>
<data name="NotSupported_MethodBodyReplacement" xml:space="preserve">
<value>Method body replacement not supported in this runtime.</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ class NonRuntimeAssembly : Assembly
}

[Fact]
[ActiveIssue("https://github.com/dotnet/runtime/issues/45689", platforms: TestPlatforms.AnyUnix, runtimes: TestRuntimes.Mono)]
[PlatformSpecific(TestPlatforms.Windows)]
[SkipOnMono("Not yet implemented on Mono")]
[ActiveIssue("https://github.com/dotnet/runtime/issues/45689", platforms: ~TestPlatforms.Windows, runtimes: TestRuntimes.CoreCLR)]
public static void ApplyUpdateInvalidParameters()
{
// Dummy delta arrays
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,11 +548,6 @@
<method signature="System.Void .ctor(System.Object)" />
</type>

<!-- Used by metadata update -->
<type fullname="System.Runtime.CompilerServices.RuntimeFeature">
<method name="LoadMetadataUpdate" />
</type>

<!-- domain.c: mono_defaults.marshal_class -->
<type fullname="System.Runtime.InteropServices.Marshal" preserve="fields" >
<!-- marshal.c (mono_marshal_get_struct_to_ptr) -->
Expand Down Expand Up @@ -637,5 +632,12 @@
<!-- marshal-ilgen.c:emit_invoke_call -->
<method signature="System.Void .ctor()" />
</type>

<!-- Used by metadata update -->
<!-- TODO: Remove this and add a debugger-libs command to invoke updates, see https://github.com/dotnet/runtime/issues/48458 -->
<type fullname="System.Reflection.Metadata.AssemblyExtensions">
<method name="ApplyUpdateSdb" />
</type>

</assembly>
</linker>
Original file line number Diff line number Diff line change
@@ -1,13 +1,68 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.CompilerServices;

namespace System.Reflection.Metadata
{
public static class AssemblyExtensions
{
[CLSCompliant(false)]
public static unsafe bool TryGetRawMetadata(this Assembly assembly, out byte* blob, out int length) => throw new NotImplementedException();

public static void ApplyUpdate(Assembly assembly, ReadOnlySpan<byte> metadataDelta, ReadOnlySpan<byte> ilDelta, ReadOnlySpan<byte> pdbDelta) => throw new NotImplementedException();
/// <summary>
/// Updates the specified assembly using the provided metadata, IL and PDB deltas.
/// </summary>
/// <remarks>
/// Currently executing methods will continue to use the existing IL. New executions of modified methods will
/// use the new IL. Different runtimes may have different limitations on what kinds of changes are supported,
/// and runtimes make no guarantees as to the state of the assembly and process if the delta includes
/// unsupported changes.
/// </remarks>
/// <param name="assembly">The assembly to update.</param>
/// <param name="metadataDelta">The metadata changes to be applied.</param>
/// <param name="ilDelta">The IL changes to be applied.</param>
/// <param name="pdbDelta">The PDB changes to be applied.</param>
/// <exception cref="ArgumentNullException">The assembly argument is null.</exception>
/// <exception cref="NotSupportedException">The update could not be applied.</exception>
public static void ApplyUpdate(Assembly assembly, ReadOnlySpan<byte> metadataDelta, ReadOnlySpan<byte> ilDelta, ReadOnlySpan<byte> pdbDelta)
{
if (assembly is not RuntimeAssembly runtimeAssembly)
{
if (assembly is null) throw new ArgumentNullException(nameof(assembly));
throw new ArgumentException(SR.Argument_MustBeRuntimeAssembly);
}

// System.Private.CoreLib is not editable
if (runtimeAssembly == typeof(AssemblyExtensions).Assembly)
throw new InvalidOperationException (SR.InvalidOperation_AssemblyNotEditable);

#if !FEATURE_METADATA_UPDATE
throw new NotSupportedException (SR.NotSupported_MethodBodyReplacement);
#else
unsafe
{
IntPtr monoAssembly = runtimeAssembly.GetUnderlyingNativeHandle ();
fixed (byte* metadataDeltaPtr = metadataDelta, ilDeltaPtr = ilDelta, pdbDeltaPtr = pdbDelta)
{
ApplyUpdate_internal(monoAssembly, metadataDeltaPtr, metadataDelta.Length, ilDeltaPtr, ilDelta.Length, pdbDeltaPtr, pdbDelta.Length);
}
}
#endif
}

internal static void ApplyUpdateSdb(Assembly assembly, byte[] metadataDelta, byte[] ilDelta, byte[]? pdbDelta)
{
ReadOnlySpan<byte> md = metadataDelta;
ReadOnlySpan<byte> il = ilDelta;
ReadOnlySpan<byte> dpdb = pdbDelta == null ? default : pdbDelta;
ApplyUpdate (assembly, md, il, dpdb);
}

#if FEATURE_METADATA_UPDATE
[MethodImpl (MethodImplOptions.InternalCall)]
private static unsafe extern void ApplyUpdate_internal (IntPtr base_assm, byte* dmeta_bytes, int dmeta_length, byte *dil_bytes, int dil_length, byte *dpdb_bytes, int dpdb_length);
#endif

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,5 @@ public static bool IsDynamicCodeCompiled
[Intrinsic] // the JIT/AOT compiler will change this flag to false for FullAOT scenarios, otherwise true
get => IsDynamicCodeCompiled;
}

#if !FEATURE_METADATA_UPDATE
internal static void LoadMetadataUpdate (Assembly assm, byte[] dmeta_data, byte[] dil_data) {
throw new NotSupportedException ("Method body replacement not supported in this runtime");
}
#else
[MethodImplAttribute (MethodImplOptions.InternalCall)]
private static unsafe extern void LoadMetadataUpdate_internal (IntPtr base_assm, byte* dmeta_bytes, int dmeta_length, byte *dil_bytes, int dil_length);

internal static void LoadMetadataUpdate (Assembly assm, byte[] dmeta_data, byte[] dil_data) {
unsafe {
fixed (byte* dmeta_bytes = dmeta_data)
fixed (byte* dil_bytes = dil_data) {
IntPtr mono_assembly = ((RuntimeAssembly)assm).GetUnderlyingNativeHandle ();
LoadMetadataUpdate_internal (mono_assembly, dmeta_bytes, dmeta_data.Length, dil_bytes, dil_data.Length);
}
}
}
#endif
}
}
2 changes: 1 addition & 1 deletion src/mono/mono/metadata/icall-decl.h
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ ICALL_EXPORT void ves_icall_System_Runtime_Intrinsics_X86_X86Base___cpuidex (int
#endif

#ifdef ENABLE_METADATA_UPDATE
ICALL_EXPORT void ves_icall_Mono_Runtime_LoadMetadataUpdate (MonoAssembly *assm, gconstpointer dmeta_bytes, int32_t dmeta_len, gconstpointer dil_bytes, int32_t dil_len);
ICALL_EXPORT void ves_icall_AssemblyExtensions_ApplyUpdate (MonoAssembly *assm, gconstpointer dmeta_bytes, int32_t dmeta_len, gconstpointer dil_bytes, int32_t dil_len, gconstpointer dpdb_bytes, int32_t dpdb_len);
#endif

#endif // __MONO_METADATA_ICALL_DECL_H__
10 changes: 5 additions & 5 deletions src/mono/mono/metadata/icall-def-netcore.h
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,11 @@ HANDLES(FILEDI_1, "get_marshal_info", ves_icall_System_Reflection_FieldInfo_get_

HANDLES(FILEDI_2, "internal_from_handle_type", ves_icall_System_Reflection_FieldInfo_internal_from_handle_type, MonoReflectionField, 2, (MonoClassField_ref, MonoType_ref))

#ifdef ENABLE_METADATA_UPDATE
ICALL_TYPE(ASSMEXT, "System.Reflection.Metadata.AssemblyExtensions", ASSMEXT_1)
NOHANDLES(ICALL(ASSMEXT_1, "ApplyUpdate_internal", ves_icall_AssemblyExtensions_ApplyUpdate))
#endif

ICALL_TYPE(MBASE, "System.Reflection.MethodBase", MBASE_1)
HANDLES(MBASE_1, "GetCurrentMethod", ves_icall_GetCurrentMethod, MonoReflectionMethod, 0, ())

Expand Down Expand Up @@ -324,11 +329,6 @@ HANDLES_REUSE_WRAPPER(MPROP_3, "get_metadata_token", ves_icall_reflection_get_to
HANDLES(MPROP_4, "get_property_info", ves_icall_RuntimePropertyInfo_get_property_info, void, 3, (MonoReflectionProperty, MonoPropertyInfo_ref, PInfo))
HANDLES(MPROP_5, "internal_from_handle_type", ves_icall_System_Reflection_RuntimePropertyInfo_internal_from_handle_type, MonoReflectionProperty, 2, (MonoProperty_ptr, MonoType_ptr))

#ifdef ENABLE_METADATA_UPDATE
ICALL_TYPE(RUNF, "System.Runtime.CompilerServices.RuntimeFeature", RUNF_1)
NOHANDLES(ICALL(RUNF_1, "LoadMetadataUpdate_internal", ves_icall_Mono_Runtime_LoadMetadataUpdate))
#endif

ICALL_TYPE(RUNH, "System.Runtime.CompilerServices.RuntimeHelpers", RUNH_1)
HANDLES(RUNH_1, "GetObjectValue", ves_icall_System_Runtime_CompilerServices_RuntimeHelpers_GetObjectValue, MonoObject, 1, (MonoObject))
HANDLES(RUNH_2, "GetUninitializedObjectInternal", ves_icall_System_Runtime_CompilerServices_RuntimeHelpers_GetUninitializedObjectInternal, MonoObject, 1, (MonoType_ptr))
Expand Down
6 changes: 4 additions & 2 deletions src/mono/mono/metadata/icall.c
Original file line number Diff line number Diff line change
Expand Up @@ -5842,15 +5842,17 @@ ves_icall_Mono_Runtime_DumpStateTotal (guint64 *portable_hash, guint64 *unportab

#ifdef ENABLE_METADATA_UPDATE
void
ves_icall_Mono_Runtime_LoadMetadataUpdate (MonoAssembly *assm,
ves_icall_AssemblyExtensions_ApplyUpdate (MonoAssembly *assm,
gconstpointer dmeta_bytes, int32_t dmeta_len,
gconstpointer dil_bytes, int32_t dil_len)
gconstpointer dil_bytes, int32_t dil_len,
gconstpointer dpdb_bytes, int32_t dpdb_len)
{
ERROR_DECL (error);
g_assert (assm);
g_assert (dmeta_len >= 0);
MonoImage *image_base = assm->image;
g_assert (image_base);
// TODO: use dpdb_bytes

MonoDomain *domain = mono_domain_get ();
mono_image_load_enc_delta (domain, image_base, dmeta_bytes, dmeta_len, dil_bytes, dil_len, error);
Expand Down
143 changes: 84 additions & 59 deletions src/mono/sample/mbr/DeltaHelper/DeltaHelper.cs
Original file line number Diff line number Diff line change
@@ -1,79 +1,104 @@
using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;
using System.Collections.Generic;

namespace MonoDelta {
public class DeltaHelper {
const string name = "System.Runtime.CompilerServices.RuntimeFeature";
public class DeltaHelper {
private static Action<Assembly, byte[], byte[], byte[]> _updateMethod;

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

private static MethodBase 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 MethodBase InitUpdateMethod ()
{
var monoType = Type.GetType (name, throwOnError: true);
if (monoType == null)
throw new Exception ($"Couldn't get the type {name}");
_updateMethod = monoType.GetMethod ("LoadMetadataUpdate", BindingFlags.NonPublic | BindingFlags.Static);
if (_updateMethod == null)
throw new Exception ($"Couldn't get LoadMetadataUpdate from {name}");
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[])});

private static void LoadMetadataUpdate (Assembly assm, byte[] dmeta_data, byte[] dil_data)
{
UpdateMethod.Invoke (null, new object [] { assm, dmeta_data, dil_data});
}
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);

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

public static DeltaHelper Make ()
{
return new DeltaHelper ();
}
private static void LoadMetadataUpdate (Assembly assm, byte[] dmeta_data, byte[] dil_data, byte[] dpdb_data)
{
UpdateMethod (assm, dmeta_data, dil_data, dpdb_data);
}

public static void InjectUpdate (string assemblyName, string dmeta_base64, string dil_base64) {
var an = new AssemblyName (assemblyName);
Assembly assm = null;
/* TODO: non-default ALCs */
foreach (var candidate in AssemblyLoadContext.Default.Assemblies) {
if (candidate.GetName().Name == an.Name) {
assm = candidate;
break;
}
}
if (assm == null)
throw new ArgumentException ("assemblyName");
var dmeta_data = Convert.FromBase64String (dmeta_base64);
var dil_data = Convert.FromBase64String (dil_base64);
LoadMetadataUpdate (assm, dmeta_data, dil_data);
}
DeltaHelper () { }

private Dictionary<Assembly, int> assembly_count = new Dictionary<Assembly, int> ();
public static DeltaHelper Make ()
{
return new DeltaHelper ();
}

public void Update (Assembly assm) {
int count;
if (!assembly_count.TryGetValue (assm, out count))
count = 1;
else
count++;
assembly_count [assm] = count;
public static void InjectUpdate (string assemblyName, string dmeta_base64, string dil_base64) {
var an = new AssemblyName (assemblyName);
Assembly assm = null;
/* TODO: non-default ALCs */
foreach (var candidate in AssemblyLoadContext.Default.Assemblies) {
if (candidate.GetName().Name == an.Name) {
assm = candidate;
break;
}
}
if (assm == null)
throw new ArgumentException ("assemblyName");
var dmeta_data = Convert.FromBase64String (dmeta_base64);
var dil_data = Convert.FromBase64String (dil_base64);
byte[] dpdb_data = null;
LoadMetadataUpdate (assm, dmeta_data, dil_data, dpdb_data);
}

/* FIXME WASM: Location is empty on wasm. Make up a name based on Name */
string basename = assm.Location;
if (basename == "")
basename = assm.GetName().Name + ".dll";
Console.WriteLine ($"Apply Delta Update for {basename}, revision {count}");
private Dictionary<Assembly, int> assembly_count = new Dictionary<Assembly, int> ();

string dmeta_name = $"{basename}.{count}.dmeta";
string dil_name = $"{basename}.{count}.dil";
byte[] dmeta_data = System.IO.File.ReadAllBytes (dmeta_name);
byte[] dil_data = System.IO.File.ReadAllBytes (dil_name);
public void Update (Assembly assm) {
int count;
if (!assembly_count.TryGetValue (assm, out count))
count = 1;
else
count++;
assembly_count [assm] = count;

LoadMetadataUpdate (assm, dmeta_data, dil_data);
}
}
/* FIXME WASM: Location is empty on wasm. Make up a name based on Name */
string basename = assm.Location;
if (basename == "")
basename = assm.GetName().Name + ".dll";
Console.WriteLine ($"Apply Delta Update for {basename}, revision {count}");

string dmeta_name = $"{basename}.{count}.dmeta";
string dil_name = $"{basename}.{count}.dil";
byte[] dmeta_data = System.IO.File.ReadAllBytes (dmeta_name);
byte[] dil_data = System.IO.File.ReadAllBytes (dil_name);
byte[] dpdb_data = null; // TODO also use the dpdb data

LoadMetadataUpdate (assm, dmeta_data, dil_data, dpdb_data);
}
}
}

0 comments on commit 629ad50

Please sign in to comment.