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

wil::to_array_view provides safer access to IBuffer / IMemoryBuffer #359

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

oldnewthing
Copy link
Member

The existing .data() member gives you a pointer to the start of the buffer but you are on your own to get the buffer size by other means.

The new wil::to_array_view function returns the buffer bytes in the form of a winrt::array_view, which encodes both the start of the buffer as well as its length.

By default, you get an array_view<uint8_t>, but you can customize the data type by calling to_array_view<T> to get an array_view<T>. Any partial Ts are ignored. For example, if the buffer is 6 bytes long, then calling to_array_view<int32_t> will give you a view of size 1.

To get the IMemoryBuffer and IMemoryBufferReference versions, you must include winrt/Windows.Foundation.h before wil/cppwinrt_helpers.h, and you must include MemoryBuffer.h at some point before using to_array_view.

To get the IBuffer version, you must include winrt/Windows.Storage.Streams.h before wil/cppwinrt_helpers.h.

The IBuffer version defaults to returning an array_view for the buffer's length. If you want an array_view for the buffer's capacity, you can use to_array_view_for_capacity.

We follow the pattern of allowing cppwinrt_helpers.h to be included multiple times, and each time you do it, new features light up based on what headers you have included prior to including cppwinrt_helpers.h.

Fixes #360

The existing `.data()` member gives you a pointer to the start
of the buffer but you are on your own to get the buffer size by other means.

The new `wil::to_array_view` function returns the buffer bytes
in the form of a `winrt::array_view`, which encodes both the
start of the buffer as well as its length.

By default, you get an `array_view<uint8_t>`, but you can customize
the data type by calling `to_array_view<T>` to get an `array_view<T>`.
Any partial `T`s are ignored. For example, if the buffer is 6 bytes long,
then calling `to_array_view<int32_t>` will give you a view of size 1.

To get the `IMemoryBuffer` and `IMemoryBufferReference` versions,
you must include `winrt/Windows.Foundation.h` before `wil/cppwinrt_helpers.h`,
and you must include `MemoryBuffer.h` at some point before using
`to_array_view`.

To get the `IBuffer` version, you must include `winrt/Windows.Storage.Streams.h`
before `wil/cppwinrt_helpers.h`.

The `IBuffer` version defaults to returning an `array_view` for the buffer's
length. If you want an `array_view` for the buffer's capacity, you can use
`to_array_view_for_capacity`.

We follow the pattern of allowing `cppwinrt_helpers.h` to be included multiple
times, and each time you do it, new features light up based on what headers
you have included prior to including `cppwinrt_helpers.h`.
include/wil/cppwinrt_helpers.h Outdated Show resolved Hide resolved
tests/CppWinRTTests.cpp Outdated Show resolved Hide resolved
@dunhor
Copy link
Member

dunhor commented Sep 15, 2023

One thing to possibly consider: the language really wants you to use std::byte for "here's a pointer to memory occupied by data". Personally... I'm not all that sold, so I don't really care, but maybe something to consider.

@dunhor
Copy link
Member

dunhor commented Sep 15, 2023

We seem to be sporadically hitting a lot of STATUS_ACCESS_VIOLATION lately... Re-ran the pipeline

We can make it a dependent type on the existing template type
parameter T.

Also update the test to (1) avoid endianness, (2) let the
compiler deal with the magic numbers.
@sylveon
Copy link
Contributor

sylveon commented Sep 17, 2023

Suggestion: add an assert(buffer.Length() % sizeof(T) == 0)

@oldnewthing
Copy link
Member Author

Suggestion: add an assert(buffer.Length() % sizeof(T) == 0)

I don't think so. You don't always control who gave you the buffer, and you don't want to crash if somebody gave you a wrong-sized buffer. We assume you just want to ignore the fractional bytes. This can happen if, for example, somebody creates a buffer based on GlobalSize(), which is allowed to return a value larger than the value passed to GlobalAlloc.

@dunhor
Copy link
Member

dunhor commented Sep 18, 2023

For some reason, your changes are really giving the pipeline troubles. I think every build/re-build has failed with an AV in the same place and I just re-ran master which got passed that point. Can you make sure to build & run the tests with Clang targeting x86 w/ RelWithDebInfo to see if it repros locally for you?

@JaiganeshKumaran
Copy link

I don't really like having basic helpers on WinRT types in WIL - this should be in C++/WinRT.

@sylveon
Copy link
Contributor

sylveon commented Sep 25, 2023

C++/WinRT is evolving at a glacial pace, so it makes sense to focus C++/WinRT itself on the projection only and keep helpers in a repo that can evolve faster.

I wouldn't be surprised if an eventual C++/WinRT 3 extracts most built-in helpers to WIL.

@oldnewthing
Copy link
Member Author

@dunhor

Can you make sure to build & run the tests with Clang targeting x86 w/ RelWithDebInfo to see if it repros locally for you?

  • scripts\init -c clang -g ninja -b debug
  • cd build\clang32debug
  • ninja (let build complete)
  • cd ..\..
  • scripts\runtests

Running tests from D:\repos\wil\build\clang32debug
Running app tests...
===============================================================================
All tests passed (87175 assertions in 115 test cases)

Running cpplatest tests...
===============================================================================
All tests passed (108794 assertions in 222 test cases)

Running noexcept tests...
===============================================================================
All tests passed (40415 assertions in 157 test cases)

Running normal tests...
===============================================================================
All tests passed (107300 assertions in 199 test cases)

Running win7 tests...
===============================================================================
All tests passed (93425 assertions in 161 test cases)

@dunhor
Copy link
Member

dunhor commented Sep 25, 2023

Edit: woops, wrong stack initially

That's Debug, not RelWithDebInfo :). I checked out your repo and looks like this repros for me pretty reliably:

0:000> k
 # ChildEBP RetAddr      
00 012ff7b4 76d94370     combase!TLSAddToMap+0x3c [onecore\com\combase\tls\tls.cxx @ 182] 
01 012ff7c8 76d93ab0     combase!TLSPreallocateData+0x88 [onecore\com\combase\tls\tls.cxx @ 378] 
02 012ff7d0 76dc8cd4     combase!COleTls::TLSAllocData+0x12 [onecore\com\combase\tls\tls.cxx @ 434] 
03 (Inline) --------     combase!InternalTlsAllocData+0x8 [onecore\com\combase\tls\tls.cxx @ 1127] 
04 (Inline) --------     combase!COleTls::{ctor}+0x1c [onecore\com\combase\ih\tls.h @ 574] 
05 012ff7e4 76eb2708     combase!COleStaticMutexSem::Request+0x54 [onecore\com\combase\common\internal\olesem.cxx @ 124] 
06 012ff804 76e91aa7     combase!CSpyMalloc_Free+0x28 [onecore\com\combase\class\memapi.cxx @ 902] 
07 012ff818 75b6e250     combase!CoTaskMemFree+0xbd177 [onecore\com\combase\class\memapi.cxx @ 454] 
08 (Inline) --------     clbcatq!SafeFree+0x7 [com\complus\src\inc\svcmem.h @ 66] 
09 (Inline) --------     clbcatq!operator delete+0xb [com\complus\src\inc\svcmem.h @ 95] 
0a 012ff840 76d7cae5     clbcatq!CComCLBCatalog::Release+0x30 [com\complus\src\comcat\catalogqueries\clbcatq\comclbcatalog.cpp @ 351] 
0b 012ff84c 76d7caa4     combase!wil::com_ptr_t<IComCatalogInternal,wil::err_returncode_policy>::~com_ptr_t<IComCatalogInternal,wil::err_returncode_policy>+0x18 [onecore\internal\sdk\inc\wil\opensource\wil\com.h @ 267] 
0c 012ff85c 77469752     combase!CComCatalog::~CComCatalog+0x68
0d 012ff898 77469631     ucrtbase!<lambda_f03950bc5685219e0bcd2087efbe011e>::operator()+0x8b [minkernel\crts\ucrt\src\appcrt\startup\onexit.cpp @ 206] 
0e 012ff8cc 774695f2     ucrtbase!__crt_seh_guarded_call<int>::operator()<<lambda_69a2805e680e0e292e8ba93315fe43a8>,<lambda_f03950bc5685219e0bcd2087efbe011e> &,<lambda_03fcd07e894ec930e3f35da366ca99d6> >+0x2e [VCCRT\vcruntime\inc\internal_shared.h @ 204] 
0f (Inline) --------     ucrtbase!__acrt_lock_and_call+0x1b [minkernel\crts\ucrt\inc\corecrt_internal.h @ 975] 
10 012ff908 774a708e     ucrtbase!_execute_onexit_table+0x32 [minkernel\crts\ucrt\src\appcrt\startup\onexit.cpp @ 231] 
11 012ff944 76e39f73     ucrtbase!__crt_state_management::wrapped_invoke<int (__cdecl*)(_iobuf *),_iobuf *,int>+0x2d [minkernel\crts\ucrt\inc\corecrt_internal_state_isolation.h @ 463] 
12 012ff94c 76e39a0a     combase!__scrt_dllmain_uninitialize_c+0x13 [VCCRT\vcstartup\src\utility\utility.cpp @ 398] 
13 012ff984 76e398b0     combase!dllmain_crt_process_detach+0x42 [VCCRT\vcstartup\src\startup\dll_dllmain.cpp @ 182] 
14 012ff990 76e39b2b     combase!dllmain_crt_dispatch+0x50 [VCCRT\vcstartup\src\startup\dll_dllmain.cpp @ 226] 
15 012ff9d0 76e39bde     combase!dllmain_dispatch+0xaf [VCCRT\vcstartup\src\startup\dll_dllmain.cpp @ 293] 
16 012ff9e4 77a29626     combase!_DllMainCRTStartup+0x1e [VCCRT\vcstartup\src\startup\dll_dllmain.cpp @ 334] 
17 012ffa04 779f861e     ntdll!LdrxCallInitRoutine+0x16 [minkernel\ntdll\i386\ldrthunk.asm @ 91] 
18 012ffa50 77a1b9bd     ntdll!LdrpCallInitRoutine+0x51 [minkernel\ldr\ldr.c @ 230] 
19 012ffaf0 77a1b774     ntdll!LdrShutdownProcess+0x15d [minkernel\ldr\ldrinit.c @ 7853] 
1a 012ffbc0 76c29a02     ntdll!RtlExitUserProcess+0x174 [minkernel\ldr\rtlstrt.c @ 1641] 
1b 012ffbd4 7746abd1     KERNEL32!ExitProcessImplementation+0x12 [clientcore\base\win32\client\process.c @ 2600] 
1c 012ffbe0 7746ab69     ucrtbase!exit_or_terminate_process+0x3d [minkernel\crts\ucrt\src\appcrt\startup\exit.cpp @ 137] 
1d 012ffc18 7746acf1     ucrtbase!common_exit+0x76 [minkernel\crts\ucrt\src\appcrt\startup\exit.cpp @ 187] 
1e 012ffc24 009f20a0     ucrtbase!exit+0x11 [minkernel\crts\ucrt\src\appcrt\startup\exit.cpp @ 287] 
1f 012ffc64 76c17ec9     witest_cpplatest!__scrt_common_main_seh+0x179 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 310] 
20 012ffc74 77a1ebbb     KERNEL32!BaseThreadInitThunk+0x19 [clientcore\base\win32\client\thread.c @ 77] 
21 012ffccc 77a1eb3d     ntdll!__RtlUserThreadStart+0x2b [minkernel\ldr\rtlstrt.c @ 1198] 
22 012ffcdc 00000000     ntdll!_RtlUserThreadStart+0x1b [minkernel\ldr\rtlstrt.c @ 1077] 

I'll start looking into it to see what's triggering the issue; IIRC issues like this are generally due to Clang handling SEH a bit weird

@oldnewthing
Copy link
Member Author

oldnewthing commented Sep 25, 2023

@dunhor Oops, sorry, missed that detail. I cannot repro with clang32relwithdebinfo either.

Running tests from D:\repos\wil\build\clang32relwithdebinfo
Running app tests...
===============================================================================
All tests passed (87175 assertions in 115 test cases)

Running cpplatest tests...
===============================================================================
All tests passed (109193 assertions in 222 test cases)

From the stack, it appears to be an order-of-uninitialization issue when an app fails to uninitialize COM, and COM is forced to uninitialize itself from inside DLL_PROCESS_DETACH.

It tries to clean up the CComCLBCatalog, and that calls CoTaskMemFree, but the CoTaskMemFree mutex has already been destroyed. CoTaskMemFree then starts a new round of delay-initialization, and that tries to store some state in the TLS, which has already been torn down.

It appears to be related to CoRegisterMallocSpy. Let me see if any tests use that.

(time passes)

Okay, I found it. It turns out that CoRegisterMallocSpy is sticky. If you register a malloc spy and then unregister it, the state does not completely revert to the "malloc spy not registered" state. Unregistering the malloc spy just replaces the malloc spy with a dummy one, but the "active malloc" vtable is still set to "Check for an active malloc spy", and we try to delay-initialize the malloc-spy mutex.

I think this is a COM bug, exacerbated by the fact that C++/WinRT intentionally leaks a COM initialization, which forces COM to do "desperation cleanup" inside DLL_PROCESS_DETACH.

@@ -182,6 +184,44 @@ TEST_CASE("CppWinRTTests::VectorToVector", "[cppwinrt]")
winrt::uninit_apartment();
}

TEST_CASE("CppWinRTTests::BufferToArrayView", "[cppwinrt]")
{
std::array<int32_t, 2> testData = { 314159265, 27182818 };
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
std::array<int32_t, 2> testData = { 314159265, 27182818 };
winrt::init_apartment();
auto uninit = wil::scope_exit([]{ winrt::uninit_apartment(); });
std::array<int32_t, 2> testData = { 314159265, 27182818 };

This seems to avoid the crash. Note that I still hit the crash locally for other tests, however AFAIK we haven't hit the same in the build labs

Copy link
Member

Choose a reason for hiding this comment

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

OMG! This comment exists for the CppWinRTAuthoringTests and I didn't even know about it!:

// This test cannot run in the same process as the malloc spies tests in wiTest.cpp
// MSFT_internal: https://task.ms/44191550

Making that [LocalOnly] isn't really the correct move, though...

Copy link
Member Author

Choose a reason for hiding this comment

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

@dunhor Can we proceed with this PR? We sort of gave up on the NotifyPropertyChanged test (PRs 368, 374).

Copy link
Member

Choose a reason for hiding this comment

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

I've disabled the NotifyPropertyChanged test to run during CI. We can either do the same thing here or we can update the test to call CoInitialize to avoid the crash during shutdown.

Copy link
Member

Choose a reason for hiding this comment

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

Err, actually disabling for just CI is probably not the best move. If we go the disable route, we should disable even when run locally so that the scripts don't break.

@ChrisGuzak ChrisGuzak assigned ChrisGuzak and unassigned ChrisGuzak Oct 20, 2023
@ChrisGuzak ChrisGuzak requested review from ChrisGuzak and removed request for ChrisGuzak October 20, 2023 04:47
@jonwis
Copy link
Member

jonwis commented Oct 31, 2023

Once the build finished again I'll poke the merge button. Thanks for your patience @oldnewthing.

@dunhor
Copy link
Member

dunhor commented Oct 31, 2023

Once the build finished again I'll poke the merge button. Thanks for your patience @oldnewthing.

@jonwis see the discussion above. There's a crash in the tests caused by a bug in COM that's caused by a combination of using IMallocSpy and not cleaning up C++/WinRT's CoIncrementMTAUsage. Raymond and I have chatted about this a bit offline (as it turns out, this is not the first test to fail because of it), and I've floated a few ideas:

  1. Simplest thing is to manually call CoInitialize as I've suggested above. This bypasses C++/WinRT's logic & we don't hit the issue because we've properly cleaned up COM before process exit
  2. Another simple thing to do is to disable the test, but obviously not ideal
  3. Another idea I've floated is to tag the tests that use IMallocSpy specially and then perform two passes of test invocation: one that excludes the IMallocSpy-using tests and one that only runs those tests

I'm personally a fan of option 1: it's simple and effective. Option 3 would help us avoid these issues moving forward, however.

@jonwis
Copy link
Member

jonwis commented Nov 15, 2023

Filed #393 to address the discovered issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow getting array_view/span out of IBuffer and IMemoryBuffer
7 participants