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

Ensure parameters with 'in' modifier cannot be modified #420

Merged
merged 1 commit into from
Jun 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/NSubstitute/NSubstitute.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Castle.Core" Version="4.3.0-*" />
<PackageReference Include="Castle.Core" Version="4.3.1-*" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.3.0-*" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,19 @@ public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[] add
}

/// <summary>
/// Allows to dynamically create a type in runtime. Returns an instance of <see cref="TypeBuilder"/>,
/// so type could be customized and built later.
/// Allows to dynamically create a type in runtime.
/// Use the <paramref name="typeBuildCallback"/> callback to define and build the type.
/// </summary>
public TypeBuilder DefineDynamicType(string typeName, TypeAttributes flags)
/// <param name="typeBuildCallback">Callback used to construct the type.</param>
/// <returns>The result returned by <paramref name="typeBuildCallback"/> callback.</returns>
public Type DefineDynamicType(Func<ModuleBuilder, Type> typeBuildCallback)
{
return _proxyGenerator.ProxyBuilder.ModuleScope.DefineType(true, typeName, flags);
var moduleBuilder = _proxyGenerator.ProxyBuilder.ModuleScope.ObtainDynamicModuleWithStrongName();

using (_proxyGenerator.ProxyBuilder.ModuleScope.Lock.ForWriting())
Copy link
Contributor Author

@zvirja zvirja Jun 25, 2018

Choose a reason for hiding this comment

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

@stakx Here is the question to you as an author of this improvement. Currently I'm defining a dynamic interface to make a delegate proxy later. As a part of this work I need to define an internal IsReadOnly attribute type inside the dynamic assembly to later mark the in parameters with it (see DefineIsReadOnlyAttributeIfNeeded() method around). I would like to somehow guarantee that attribute type is created only once. Otherwise, code fails with the following error and it's cumbersome to write some logic on top of this API:

System.ArgumentException : Duplicate type name within an assembly.
   at System.Reflection.Emit.ModuleBuilder.CheckTypeNameConflict(String strTypeName, Type enclosingType)
   at System.Reflection.Emit.AssemblyBuilderData.CheckTypeNameConflict(String strTypeName, TypeBuilder enclosingType)
   at System.Reflection.Emit.TypeBuilder.Init(String fullname, TypeAttributes attr, Type parent, Type[] interfaces, ModuleBuilder module, PackingSize iPackingSize, Int32 iTypeSize, TypeBuilder enclosingType)
   at System.Reflection.Emit.ModuleBuilder.DefineType(String name, TypeAttributes attr, Type parent)
...

Given that you deprecated this lock, do you see any other ways to have such a guarantee?

Thanks for the help in advance! 👍

Copy link

@stakx stakx Jun 25, 2018

Choose a reason for hiding this comment

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

@zvirja, thanks for bringing this to my attention. You're right that ModuleBuilder's API now doesn't offer a good way to do thread-safe type creation now. The only possibility that I can see is ensuring in your library that your ProxyGenerator instance won't get used at all while you're generating that attribute type... i.e. having a separate lock of your own.

I've opened castleproject/Core#399 to see what can be done at DynamicProxy's end. Could you please keep an eye on that issue? If a thread-safe alternative for ModuleScope.DefineType is going to be added, it would be good to ensure that the API is suitable for downstream libraries. 😉

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The only possibility that I can see is ensuring in your library that your ProxyGenerator instance won't get used at all while you're generating that attribute type... i.e. having a separate lock of your own.

Unfortunately, it's very unsafe to proceed with such approach. It could happen that in future versions of Castle.Core it will also start to define the attribute. In this case there is a possibility of the race condition between my code and Castle.Core. Rather, we should use the same shared synchronization and play safe.

I've opened castleproject/Core#399 to see what can be done at DynamicProxy's end.

Thanks 👍 Let's try to proceed with that one and see how it goes.

{
return typeBuildCallback.Invoke(moduleBuilder);
}
}

private object CreateProxyUsingCastleProxyGenerator(Type typeToProxy, Type[] additionalInterfaces,
Expand Down
105 changes: 85 additions & 20 deletions src/NSubstitute/Proxies/DelegateProxy/DelegateProxyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace NSubstitute.Proxies.DelegateProxy
public class DelegateProxyFactory : IProxyFactory
{
private const string MethodNameInsideProxyContainer = "Invoke";
private const string IsReadOnlyAttributeFullTypeName = "System.Runtime.CompilerServices.IsReadOnlyAttribute";
private readonly CastleDynamicProxyFactory _castleObjectProxyFactory;
private readonly ConcurrentDictionary<Type, Type> _delegateContainerCache = new ConcurrentDictionary<Type, Type>();
private long _typeSuffixCounter;
Expand All @@ -39,7 +40,7 @@ public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[] add
return DelegateProxy(typeToProxy, callRouter);
}

private bool HasItems<T>(T[] array)
private static bool HasItems<T>(T[] array)
{
return array != null && array.Length > 0;
}
Expand Down Expand Up @@ -67,31 +68,95 @@ private Type GenerateDelegateContainerInterface(Type delegateType)
delegateTypeName,
typeSuffixCounter.ToString(CultureInfo.InvariantCulture));

var typeBuilder = _castleObjectProxyFactory.DefineDynamicType(
typeName,
TypeAttributes.Abstract | TypeAttributes.Interface | TypeAttributes.Public);
return _castleObjectProxyFactory.DefineDynamicType(moduleBuilder =>
{
var typeBuilder = moduleBuilder.DefineType(
typeName,
TypeAttributes.Abstract | TypeAttributes.Interface | TypeAttributes.Public);

// Notice, we don't copy the custom modifiers here.
Copy link
Member

Choose a reason for hiding this comment

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

This code is really nicely documented. 👍 (code comments sometimes get a lot of hate, so I wanted to give good credit where it is due 😂 )

// That's absolutely fine, as custom modifiers are ignored when delegate is constructed.
// See the related discussion here: https://github.com/dotnet/coreclr/issues/18401
var methodBuilder = typeBuilder
.DefineMethod(
MethodNameInsideProxyContainer,
MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.Public,
CallingConventions.Standard,
delegateSignature.ReturnType,
delegateSignature.GetParameters().Select(p => p.ParameterType).ToArray());

// Copy original method attributes, so "out" parameters are recognized later.
for (var i = 0; i < delegateParameters.Length; i++)
{
var parameter = delegateParameters[i];

// Increment position by 1 to skip the implicit "this" parameter.
var paramBuilder = methodBuilder.DefineParameter(i + 1, parameter.Attributes, parameter.Name);

// Read-only parameter ('in' keyword) is recognized by presence of the special attribute.
// If source parameter contained that attribute, ensure to copy it to the generated method.
// That helps Castle to understand that parameter is read-only and cannot be mutated.
DefineIsReadOnlyAttributeIfNeeded(parameter, paramBuilder, moduleBuilder);
}

// Preserve the original delegate type in attribute, so it can be retrieved later in code.
methodBuilder.SetCustomAttribute(
new CustomAttributeBuilder(
typeof(ProxiedDelegateTypeAttribute).GetConstructors().Single(),
new object[] {delegateType}));

return typeBuilder.CreateTypeInfo().AsType();
});
}

private static void DefineIsReadOnlyAttributeIfNeeded(
ParameterInfo sourceParameter, ParameterBuilder paramBuilder, ModuleBuilder dynamicModuleBuilder)
{
// Read-only parameter can be by-ref only.
if (!sourceParameter.ParameterType.IsByRef)
{
return;
}

// Lookup for the attribute using full type name.
// That's required because compiler can embed that type directly to the client's assembly
// as type identity doesn't matter - only full type attribute name is checked.
var isReadOnlyAttrType = sourceParameter.CustomAttributes
.Select(ca => ca.AttributeType)
.FirstOrDefault(t => t.FullName.Equals(IsReadOnlyAttributeFullTypeName, StringComparison.Ordinal));

var methodBuilder = typeBuilder
.DefineMethod(
MethodNameInsideProxyContainer,
MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.Public,
delegateSignature.ReturnType,
delegateParameters.Select(p => p.ParameterType).ToArray());
// Parameter doesn't contain the IsReadOnly attribute.
if (isReadOnlyAttrType == null)
{
return;
}

// Copy original method attributes, so "out" parameters are recognized later.
for (var i = 0; i < delegateParameters.Length; i++)
// If the compiler generated attribute is used (e.g. runtime doesn't contain the attribute),
// the generated attribute type might be internal, so we cannot referecnce it in the dynamic assembly.
// In this case use the attribute type from the dynamic assembly.
if (!isReadOnlyAttrType.GetTypeInfo().IsVisible)
{
// Increment position by 1 to skip the implicit "this" parameter.
methodBuilder.DefineParameter(i + 1, delegateParameters[i].Attributes, delegateParameters[i].Name);
isReadOnlyAttrType = GetIsReadOnlyAttributeInDynamicModule(dynamicModuleBuilder);
}

// Preserve the original delegate type in attribute, so it can be retrieved later in code.
methodBuilder.SetCustomAttribute(
new CustomAttributeBuilder(
typeof(ProxiedDelegateTypeAttribute).GetConstructors().Single(),
new object[] {delegateType}));
paramBuilder.SetCustomAttribute(
new CustomAttributeBuilder(isReadOnlyAttrType.GetConstructor(Type.EmptyTypes), new object[0]));
}

private static Type GetIsReadOnlyAttributeInDynamicModule(ModuleBuilder moduleBuilder)
{
var existingType = moduleBuilder.Assembly.GetType(IsReadOnlyAttributeFullTypeName, throwOnError: false, ignoreCase: false);
if (existingType != null)
{
return existingType;
}

return typeBuilder.CreateTypeInfo().AsType();
return moduleBuilder
.DefineType(
IsReadOnlyAttributeFullTypeName,
TypeAttributes.Class | TypeAttributes.Sealed | TypeAttributes.NotPublic,
typeof(Attribute))
.CreateTypeInfo().AsType();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using NUnit.Framework;
using NUnit.Framework;

namespace NSubstitute.Acceptance.Specs.FieldReports
{
Expand All @@ -8,22 +7,91 @@ namespace NSubstitute.Acceptance.Specs.FieldReports
/// </summary>
public class Issue378_InValueTypes
{
public readonly struct Struct { }
public readonly struct Struct
{
public Struct(int value)
{
Value = value;
}

public int Value { get; }
}

public interface IStructByReadOnlyRefConsumer { void Consume(in Struct value); }

public interface IStructByValueConsumer { void Consume(Struct value); }

public interface IStructByRefConsumer { void Consume(in Struct message); }
public delegate void DelegateStructByReadOnlyRefConsumer(in Struct value);

public interface IStructByValueConsumer { void Consume(Struct message); }
public delegate void DelegateStructByReadOnlyRefConsumerMultipleArgs(in Struct value1, in Struct value2);

[Test]
public void IStructByRefConsumer_Test()
public void IStructByReadOnlyRefConsumer_Test()
{
_ = Substitute.For<IStructByRefConsumer>();
var value = new Struct(42);

var subs = Substitute.For<IStructByReadOnlyRefConsumer>();
subs.Consume(in value);
}

[Test]
public void IStructByValueConsumer_Test()
{
_ = Substitute.For<IStructByValueConsumer>();
var value = new Struct(42);

var subs = Substitute.For<IStructByValueConsumer>();
subs.Consume(value);
}

[Test]
public void DelegateByReadOnlyRefConsumer_Test()
{
var value = new Struct(42);

var subs = Substitute.For<DelegateStructByReadOnlyRefConsumer>();
subs.Invoke(in value);
}

[Test]
public void InterfaceReadOnlyRefCannotBeModified()
{
var readOnlyValue = new Struct(42);

var subs = Substitute.For<IStructByReadOnlyRefConsumer>();
subs.When(x => x.Consume(Arg.Any<Struct>())).Do(c => { c[0] = new Struct(24); });

subs.Consume(in readOnlyValue);

Assert.That(readOnlyValue.Value, Is.EqualTo(42));
}

[Test]
public void DelegateReadOnlyRefCannotBeModified()
{
var readOnlyValue = new Struct(42);

var subs = Substitute.For<DelegateStructByReadOnlyRefConsumer>();
subs.When(x => x.Invoke(Arg.Any<Struct>())).Do(c => { c[0] = new Struct(24); });

subs.Invoke(in readOnlyValue);

Assert.That(readOnlyValue.Value, Is.EqualTo(42));
}

[Test]
public void DelegateMultipleReadOnlyRefCannotBeModified()
{
var readOnlyValue1 = new Struct(42);
var readOnlyValue2 = new Struct(42);

var subs = Substitute.For<DelegateStructByReadOnlyRefConsumerMultipleArgs>();
subs.When(x => x.Invoke(Arg.Any<Struct>(), Arg.Any<Struct>()))
.Do(c => { c[0] = new Struct(24); c[1] = new Struct(24); });

subs.Invoke(in readOnlyValue1, in readOnlyValue2);

Assert.That(readOnlyValue1.Value, Is.EqualTo(42));
Assert.That(readOnlyValue2.Value, Is.EqualTo(42));
}
}
}