Skip to content

Commit

Permalink
feat(dotnet): introduce UnsafeCast<T>() method
Browse files Browse the repository at this point in the history
In order to allow converting an opaque object instance to an arbitrary
(jsii-managed) interface type, the `UnsafeCase` operation may be used to
instantiate a proxy while bypassing all type consistency checks.

This is similar to using `as any` or `as T` in TypeScript, meaning that
if the user performs a cast to an incorrect/unsupported type, undefined
behavior follows.

This would unblock certain specific use-case scenarios that static
languages render difficult to enact, such as the one described in
aws/aws-cdk#3284.
  • Loading branch information
RomainMuller committed Oct 27, 2020
1 parent 3ca0e42 commit 7ecfbb4
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 73 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using Amazon.JSII.Runtime.Deputy;
using Xunit;
using Xunit.Sdk;

namespace Amazon.JSII.Runtime.UnitTests.Deputy
{
public sealed class DeputyBaseTests
{
const string Prefix = "Runtime.Deputy." + nameof(DeputyBase) + ".";

[Fact(DisplayName = Prefix + nameof(CanCastToAnyInterface))]
public void CanCastToAnyInterface()
{
var subject = new AnonymousObject(new ByRefValue("object@10000", Array.Empty<string>()));
var result = subject.UnsafeCast<IManagedInterface>();
Assert.IsType<ManagedInterfaceProxy>(result);
}

[JsiiInterface(typeof(IManagedInterface), "test.IManagedInterface")]
private interface IManagedInterface
{
bool BooleanProperty { get; }
}

[JsiiTypeProxy(typeof(IManagedInterface), "test.IManagedInterface")]
private class ManagedInterfaceProxy : DeputyBase, IManagedInterface
{
public ManagedInterfaceProxy(ByRefValue byRef): base(byRef)
{
BooleanProperty = true;
}

public bool BooleanProperty { get; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ namespace Amazon.JSII.Runtime.Deputy
{
internal sealed class AnonymousObject : DeputyBase
{
AnonymousObject(ByRefValue byRefValue) : base(byRefValue)
internal AnonymousObject(ByRefValue byRefValue) : base(byRefValue)
{
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Authentication.ExtendedProtection;

namespace Amazon.JSII.Runtime.Deputy
{
Expand Down Expand Up @@ -127,7 +129,7 @@ protected static T GetStaticProperty<T>(System.Type type, [CallerMemberName] str
propertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));

JsiiTypeAttributeBase.Load(type.Assembly);

var classAttribute = ReflectionUtils.GetClassAttribute(type)!;
var propertyAttribute = GetStaticPropertyAttribute(type, propertyName);

Expand Down Expand Up @@ -178,7 +180,7 @@ protected static void SetStaticProperty<T>(System.Type type, T value, [CallerMem
propertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName));

JsiiTypeAttributeBase.Load(type.Assembly);

var classAttribute = ReflectionUtils.GetClassAttribute(type)!;
var propertyAttribute = GetStaticPropertyAttribute(type, propertyName);

Expand Down Expand Up @@ -229,12 +231,12 @@ protected void InvokeInstanceVoidMethod(System.Type[] parameterTypes, object?[]
{
InvokeInstanceMethod<object>(parameterTypes, arguments, methodName);
}

[return: MaybeNull]
protected static T InvokeStaticMethod<T>(System.Type type, System.Type[] parameterTypes, object?[] arguments, [CallerMemberName] string methodName = "")
{
JsiiTypeAttributeBase.Load(type.Assembly);

var methodAttribute = GetStaticMethodAttribute(type, methodName, parameterTypes);
var classAttribute = ReflectionUtils.GetClassAttribute(type)!;

Expand Down Expand Up @@ -289,7 +291,7 @@ private static T InvokeMethodCore<T>(
{
throw new NotSupportedException($"Could not convert result '{result}' for method '{methodAttribute.Name}'");
}

return (T)frameworkValue!;

object? GetResult()
Expand Down Expand Up @@ -466,99 +468,137 @@ private static JsiiMethodAttribute GetMethodAttributeCore(System.Type type, stri
}

#endregion

#region IConvertible


/// <summary>
/// Unsafely obtains a proxy of a given type for this instance. This method allows obtaining a proxy instance
/// that is not known to be supported by the backing object instance; in which case the behavior of any
/// operation that is not supported by the backing instance is undefined.
/// </summary>
/// <typeparam name="T">
/// A jsii-managed interface to obtain a proxy for.
/// This interface must carry a <see cref="JsiiInterfaceAttribute" /> attribute.
/// </typeparam>
/// <returns>
/// An instance of <c>T</c>
/// </returns>
/// <exception cref="ArgumentException">
/// If the type provided for <c>T</c> does not carry the <see cref="JsiiInterfaceAttribute" /> attribute.
/// </exception>
public T UnsafeCast<T>() where T: class
{
if (this is T result)
{
return result;
}

try
{
return (T) Convert.ChangeType(this, typeof(T), CultureInfo.InvariantCulture);
}
catch (InvalidCastException)
{
// At this point, we are converting to a type that we don't know for sure is applicable
if (MakeProxy<T>(true, out var proxy))
{
return proxy;
}

throw;
}
}

private IDictionary<System.Type, object> Proxies { get; } = new Dictionary<System.Type, object>();

TypeCode IConvertible.GetTypeCode()
{
return TypeCode.Object;
}

object IConvertible.ToType(System.Type conversionType, IFormatProvider? provider)
{
if (Proxies.ContainsKey(conversionType))
{
return Proxies[conversionType];
}

if (ToTypeCore(out var converted))
{
return Proxies[conversionType] = converted!;
}

throw new InvalidCastException($"Unable to cast {this.GetType().FullName} into {conversionType.FullName}");

bool ToTypeCore(out object? result)
{
if (conversionType.IsInstanceOfType(this))
{
result = this;
return true;
}
if (!conversionType.IsInstanceOfType(this)) return MakeProxy(false, out result);

if (!conversionType.IsInterface || Reference.Interfaces.Length == 0)
{
// We can only convert to interfaces that are declared on the Reference.
result = null;
return false;
}
result = this;
return true;

var interfaceAttribute = conversionType.GetCustomAttribute<JsiiInterfaceAttribute>();
if (interfaceAttribute == null)
{
// We can only convert to interfaces decorated with the JsiiInterfaceAttribute
result = null;
return false;
}
}
}

var types = ServiceContainer.ServiceProvider.GetRequiredService<ITypeCache>();

if (!TryFindSupportedInterface(interfaceAttribute.FullyQualifiedName, Reference.Interfaces, types, out var adequateFqn))
{
// We can only convert to interfaces declared by this Reference
result = null;
return false;
}

var proxyType = types.GetProxyType(interfaceAttribute.FullyQualifiedName);
var constructorInfo = proxyType.GetConstructor(
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
null,
new[] {typeof(ByRefValue)},
null
);
if (constructorInfo == null)
{
throw new JsiiException($"Could not find constructor to instantiate {proxyType.FullName}");
}
private bool MakeProxy<T>(bool force, [NotNullWhen(true)] out T? result) where T: class
{
if (!typeof(T).IsInterface)
{
result = null;
return false;
}

var interfaceAttribute = typeof(T).GetCustomAttribute<JsiiInterfaceAttribute>();
if (interfaceAttribute == null)
{
// We can only convert to interfaces decorated with the JsiiInterfaceAttribute
result = null;
return false;
}

result = constructorInfo.Invoke(new object[]{ Reference.ForProxy() });
return true;
var types = ServiceContainer.ServiceProvider.GetRequiredService<ITypeCache>();

bool TryFindSupportedInterface(string declaredFqn, string[] availableFqns, ITypeCache types, out string? foundFqn)
{
var declaredType = types.GetInterfaceType(declaredFqn);
if (!TryFindSupportedInterface(interfaceAttribute.FullyQualifiedName, Reference.Interfaces, types, out var adequateFqn))
{
// We can only convert to interfaces declared by this Reference
result = null;
return false;
}

foreach (var candidate in availableFqns)
{
var candidateType = types.GetInterfaceType(candidate);
if (declaredType.IsAssignableFrom(candidateType))
{
foundFqn = candidate;
return true;
}
}

foundFqn = null;
return false;
var proxyType = types.GetProxyType(interfaceAttribute.FullyQualifiedName);
var constructorInfo = proxyType.GetConstructor(
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
null,
new[] {typeof(ByRefValue)},
null
);
if (constructorInfo == null)
{
throw new JsiiException($"Could not find constructor to instantiate {proxyType.FullName}");
}

result = (T)constructorInfo.Invoke(new object[]{ Reference.ForProxy() });
return true;

bool TryFindSupportedInterface(string declaredFqn, string[] availableFqns, ITypeCache types, out string? foundFqn)
{
var declaredType = types.GetInterfaceType(declaredFqn);

foreach (var candidate in availableFqns)
{
var candidateType = types.GetInterfaceType(candidate);
if (!declaredType.IsAssignableFrom(candidateType)) continue;
foundFqn = candidate;
return true;
}

foundFqn = declaredFqn;
return force;
}
}

#region Impossible Conversions

bool IConvertible.ToBoolean(IFormatProvider? provider)
{
throw new InvalidCastException();
Expand All @@ -568,17 +608,17 @@ byte IConvertible.ToByte(IFormatProvider? provider)
{
throw new InvalidCastException();
}

char IConvertible.ToChar(IFormatProvider? provider)
{
throw new InvalidCastException();
}

DateTime IConvertible.ToDateTime(IFormatProvider? provider)
{
throw new InvalidCastException();
}

decimal IConvertible.ToDecimal(IFormatProvider? provider)
{
throw new InvalidCastException();
Expand Down Expand Up @@ -635,7 +675,7 @@ ulong IConvertible.ToUInt64(IFormatProvider? provider)
}

#endregion

#endregion
}
}

0 comments on commit 7ecfbb4

Please sign in to comment.