-
Notifications
You must be signed in to change notification settings - Fork 264
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
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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; | ||
|
@@ -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; | ||
} | ||
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 thein
parameters with it (seeDefineIsReadOnlyAttributeIfNeeded()
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:Given that you deprecated this lock, do you see any other ways to have such a guarantee?
Thanks for the help in advance! 👍
There was a problem hiding this comment.
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 yourProxyGenerator
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. 😉There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 andCastle.Core
. Rather, we should use the same shared synchronization and play safe.Thanks 👍 Let's try to proceed with that one and see how it goes.