-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Marshal blittable structs via memcpy even if nested within non-blittable struct #20194
Conversation
src/vm/fieldmarshaler.cpp
Outdated
|
||
HRESULT hRESULT = pInternalImport->GetCustomAttributeByName(field, "System.Runtime.CompilerServices.FixedBufferAttribute", NULL, NULL); | ||
return hRESULT == S_OK; | ||
return false; |
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.
Unreachable code
Should this rather cover all opaque structs with explicit layout, not just "fixed buffers" ? Consider: [StructLayout(LayoutKind.Explicit, Size = 16)]
struct OpaqueStruct
{
}
struct MyStruct1
{
OpaqueStruct _f;
int _i;
}
struct MyStruct2
{
OpaqueStruct _f;
string _s;
} I expect that the opaque struct will be copied over in the first case, but it won't be copied over in the second case today. Is that correct? |
If you run following with your change on checked runtime: using System;
using System.Runtime.InteropServices;
unsafe struct MyStruct
{
fixed char X[1];
}
unsafe class My {
static void Main() {
Console.WriteLine(Marshal.SizeOf(typeof(MyStruct)));
}
} It will hit assert:
I am starting to worry that this change has subtle butterfly effects that it is impossible to reason about. It may be too risky to fix in the interop marshalling rules built into the runtime. |
src/vm/fieldmarshaler.cpp
Outdated
{ | ||
ULONG classSize = 0; | ||
|
||
if (pInternalImport->GetClassTotalSize(thNestedType.GetMethodTable()->GetCl(), &classSize) >= 0) |
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.
nit: Please Use if(SUCCEEDED(...
src/vm/nsenums.h
Outdated
@@ -73,4 +73,6 @@ DEFINE_NFT(NFT_ILLEGAL, 1, true) | |||
DEFINE_NFT(NFT_WINDOWSFOUNDATIONIREFERENCE, sizeof(IUnknown*), true) // Windows.Foundation.IReference`1 is marshaled to System.Nullable`1. | |||
#endif // FEATURE_COMINTEROP | |||
|
|||
DEFINE_NFT(NFT_FIXEDBUFFER, 0, true) // Inline fixed fields |
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.
This should be false instead of true, since FixedBufferAttribute isn't a valid winrt attribute.
src/vm/methodtable.cpp
Outdated
@@ -2807,6 +2807,24 @@ bool MethodTable::ClassifyEightBytesWithNativeLayout(SystemVStructRegisterPassin | |||
|
|||
continue; | |||
} | |||
else if (cls == NFT_FIXEDBUFFER) |
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.
This code path is same as NFT_NESTEDVALUECLASS, how about
else if(cls == NFT_NESTEDVALUECLASS || cls == NFT_FIXEDBUFFER)
src/vm/fieldmarshaler.h
Outdated
@@ -1982,6 +1989,132 @@ class FieldMarshaler_DateTimeOffset : public FieldMarshaler | |||
|
|||
#endif // FEATURE_COMINTEROP | |||
|
|||
class FieldMarshaler_FixedBuffer : public FieldMarshaler |
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.
is it possible to update FieldMarshaller_NestedValueClass marshaller instead of adding a new one? For me, These two marshaller are very similar.
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.
Possibly. I'll try to combine them.
src/vm/fieldmarshaler.h
Outdated
memcpyNoGCRefs(pNative, pCLR, sizeof(size)); | ||
} | ||
|
||
VOID ScalarUpdateCLRImpl(const VOID *pNative, LPVOID pCLR) const |
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.
I have a concern for these ScalarUpdateXXXImpl method. such as bool type, in managed side it is 1 byte and in native side(by default, it is WinBool), it is 4 bytes. memcpy may mess up its value.
@jkotas I figured out why that's failing on the checked build. I mistakenly assumed that all fixed buffers were blittable, forgetting that |
src/vm/fieldmarshaler.cpp
Outdated
CONTRACTL_END; | ||
|
||
HRESULT hRESULT = pInternalImport->GetCustomAttributeByName(field, g_FixedBufferAttribute, NULL, NULL); | ||
return hRESULT == S_OK; |
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.
There is some subtly here. The result of this operation is not an instance of type BOOL
, but rather the C++ type bool
. The difference is as follows: sizeof(BOOL) == 4
and sizeof(bool) == 1
. In this case there is an implicit conversion, but that is less than ideal most of the time. In this these cases I normally do hResult == S_OK ? TRUE : FALSE
so types are correct. I am okay with it in this case, but there is a difference and it should be known.
src/vm/fieldmarshaler.cpp
Outdated
} | ||
CONTRACTL_END; | ||
|
||
HRESULT hRESULT = pInternalImport->GetCustomAttributeByName(field, g_FixedBufferAttribute, NULL, NULL); |
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.
Please change the name to hr
or hResult
.
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.
I would prefer hr. since most of our code use hr. only 1 instance use hResult.
src/vm/fieldmarshaler.h
Outdated
|
||
FieldMarshaler_FixedBuffer(ULONG bufferSize, MethodTable* pMT) | ||
{ | ||
size = bufferSize; |
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.
Please us initializer list so size
can be made const
. (i.e. size{ bufferSize }
)
…eld to pretend that there are multiple fields consecutively in the structure.
@jkotas @luqunl @AaronRobinsonMSFT I've re-implemented my fix in a new direction. Since not all fixed buffers are blittable, a scalar copy doesn't work. So, I decided to treat a fixed buffer member as though it has multiple consecutive fields of the same type as the first field (as it is described in the C# language feature). The buffer length is pulled from the This fix also corrects the crash in the checked build mentioned above. |
@jkotas I've identified the bug that causes the heap overflow. It's another CoreCLR bug with handling fixed buffer fields that has not been found because #18521 has been masking it. When calculating the native size of a fixed buffer structure, the runtime doesn't account for the fact that the native size of each field in a fixed buffer might be more than the managed size. Since the runtime sees only one field, it calculates the I've added some asserts on my local copy to discover this issue when marshaling. I'm currently brainstorming fixes for the masked bug. |
Even once you fix that other bug, the change is still going to be changing the behavior in potentially breaking way - imagine that somebody had a hardcoded size or called |
@jkotas I've talked with @AaronRobinsonMSFT and we've decided that the cleanest way to do this would be to revert to the current behavior when the type of the fixed buffer is non-blittable (i.e. |
Sounds good. |
src/vm/fieldmarshaler.cpp
Outdated
@@ -659,7 +661,15 @@ do \ | |||
{ | |||
if (IsStructMarshalable(thNestedType)) | |||
{ | |||
INITFIELDMARSHALER(NFT_NESTEDVALUECLASS, FieldMarshaler_NestedValueClass, (thNestedType.GetMethodTable())); | |||
LONG numBufferElements; | |||
if (IsFixedBuffer(pfwalk->m_MD, pInternalImport, &numBufferElements) && thNestedType.GetMethodTable()->IsBlittable()) |
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.
Does it still make sense to use NFT_NESTEDVALUECLASS
for this now that it is limited to blittable types only? It seems to be just an expensive way to do memcpy now.
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.
I can revert it back to being a new NFT type. If I do that though, I have to replicate the non-marshaling codepaths that use NFTs to use the same code as NFT_NESTEDVALUECLASS
. For reference, blittable types contained within non-blittable types still go through the NestedValueClass
field marshaler.
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.
I do not think we need a new NFT type for this. I think that the fix is a lot more complex (and worse perf) than what it needs to be.
Can we fix it by just changing FieldMarshaler_NestedValueClass::NestedValueClassUpdateCLRImpl
to:
MethodTable* pMT = GetMethodTable();
// would be better to detect this at class load time (that have a nested value
// class with no layout) but don't have a way to know
if (!pMT->GetLayoutInfo())
COMPlusThrow(kArgumentException, IDS_NOLAYOUT_IN_EMBEDDED_VALUECLASS);
if (pMT->IsBlittable())
{
memcpyNoGCRefs((BYTE*)(*ppProtectedCLR) + startoffset, pNative, pMT->GetNativeSize());
}
else
{
LayoutUpdateCLR( (LPVOID*)ppProtectedCLR,
startoffset,
GetMethodTable(),
(BYTE *)pNative);
}
}
And similar change in FieldMarshaler_NestedValueClass::NestedValueClassUpdateNativeImpl
?
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.
That should work. I'll try it out.
if customer is use fixed char/bool[] and sizeof, it will buffer overrun(likely) either before this change or after this change. so I am not too worried that it will break customer. BTW, I would like to know what testcase matrix we want to add for this change.. |
One of the reasons we came up with for not supporting non-blittable fixed buffers at all is that the metadata needed to correctly calculate the native size of the generated fixed buffer structure available at that time, and it would require some weird special casing in the type loader to work around that. And also in the process we'd likely have to change the behavior of opaque structs that contain |
…ure calculated)." This reverts commit 496eef5.
Just curious, why it need type loader to workaround? maybe I miss something. We know its(the struct generated by roslyn) field marshaller(such as bool/char) and its fieldmarshaller will give us its nativesize(m_cbNativeSize), is it? |
The native size calculation is done while loading the class into the runtime, so that's why I mentioned the type loader. The code is technically in
|
The third option would be to use a heuristic to determine that a structure was generated by the compiler to represent a fixed buffer and then take I really don't like the heuristic idea though because it makes us even more dependent on C# compiler output of generated structures for fixed buffers not changing. |
…/coreclr into fixes/coreclr/18521
@dotnet-bot test OSX10.12 x64 Checked Innerloop Build and Test please |
@jkoritzinsky This is a relatively new piece of functionality and extends a scenario that needs additional test coverage. What is the plan for testing this and where is that PR? Please link that PR ASAP. |
So currently the PR dotnet/corefx#32538 has the testing for this functionality. I'll add in a test there for the opaque struct case as well. |
In master, when a fixed buffer is marshaled as part of a non-blittable structure, the marshaling code treats the type of a fixed buffer field
fixed byte buffer[2]
as a regular structure. However, the custom structure generated for a fixed buffer field has only one field of the type of the buffer. See the IL below for an example:So, when this member is marshaled as a regular structure only the first member is marshaled since technically it is the only member of the structure.
As a result, the rest of the data is lost.
This PR marshals nested blittable structs via a memcpy. This enables fixed buffers like the one above to be correctly marshaled across managed<-> native. This should also allow us to marshal opaque structs nested inside non-blittable structs across since they are technically blittable.
This PR does not enable correctly marshaling non-blittable fixed buffers. We decided (in PR conversation below) to not support that feature.
Fixes #18521.