{"id":108146,"date":"2023-05-05T07:00:00","date_gmt":"2023-05-05T14:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/oldnewthing\/?p=108146"},"modified":"2023-05-05T06:56:54","modified_gmt":"2023-05-05T13:56:54","slug":"20230505-00","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/oldnewthing\/20230505-00\/?p=108146","title":{"rendered":"The case of the crash in a C++\/WinRT coroutine: Unpeeling the onion"},"content":{"rendered":"<p>A customer had a mysterious crash in a C++\/WinRT coroutine, and they were having trouble identifying the cause. The stack of the crash looked like this:<\/p>\n<pre>Contoso!winrt::<wbr \/>impl::<wbr \/>consume_<wbr \/>Contoso_<wbr \/>IWidgetWatcher&lt;winrt::<wbr \/>Contoso::<wbr \/>IWidgetWatcher&gt;::<wbr \/>NotifyChange+0x3\r\nContoso!winrt::<wbr \/>Contoso::<wbr \/>implementation::<wbr \/>WidgetController::<wbr \/>SaveWidget+0x16\r\nContoso!winrt::<wbr \/>impl::<wbr \/>produce&lt;winrt::<wbr \/>Contoso::<wbr \/>implementation::<wbr \/>WidgetController,<wbr \/>winrt::<wbr \/>Contoso::<wbr \/>IWidgetController&gt;::<wbr \/>SaveWidget+0x1d\r\nContoso!winrt::<wbr \/>impl::<wbr \/>consume_<wbr \/>Contoso_<wbr \/>IWidgetController&lt;winrt::<wbr \/>Contoso::<wbr \/>IWidgetController&gt;::<wbr \/>SaveWidget+0xd\r\nContoso!winrt::<wbr \/>Contoso::<wbr \/>implementation::<wbr \/>Widget::<wbr \/>OnSettingsChanged$_<wbr \/>ResumeCoro$1+0x31f\r\nContoso!std::<wbr \/>experimental::<wbr \/>coroutine_<wbr \/>handle&lt;void&gt;::<wbr \/>resume+0xc\r\nContoso!winrt::<wbr \/>impl::<wbr \/>await_<wbr \/>adapter&lt;winrt::<wbr \/>Windows::<wbr \/>Foundation::<wbr \/>IAsyncAction&gt;::<wbr \/>await_<wbr \/>suspend+0x15e\r\nContoso!winrt::<wbr \/>Contoso::<wbr \/>implementation::<wbr \/>Widget::<wbr \/>OnSettingsChanged$_<wbr \/>ResumeCoro$1+0x11a\r\nContoso!winrt::<wbr \/>Contoso::<wbr \/>implementation::<wbr \/>Widget::<wbr \/>OnSettingsChanged$_<wbr \/>InitCoro$2+0x6a\r\nContoso!winrt::<wbr \/>Contoso::<wbr \/>implementation::<wbr \/>Widget::<wbr \/>OnSettingsChanged+0x5c\r\nContoso!winrt::<wbr \/>Windows::<wbr \/>Foundation::<wbr \/>TypedEventHandler&lt;winrt::<wbr \/>Contoso::<wbr \/>Widget,<wbr \/>winrt::<wbr \/>Windows::<wbr \/>Foundation::<wbr \/>IInspectable&gt;::<wbr \/>&lt;lambda_<wbr \/>a7902c7784a1de3d47473a11e43d997c&gt;::<wbr \/>operator()+0x5b\r\nContoso!winrt::<wbr \/>impl::<wbr \/>delegate&lt;winrt::<wbr \/>Windows::<wbr \/>Foundation::<wbr \/>TypedEventHandler&lt;winrt::<wbr \/>Contoso::<wbr \/>Widget,<wbr \/>winrt::<wbr \/>Windows::<wbr \/>Foundation::<wbr \/>IInspectable&gt;,<wbr \/>&lt;lambda_<wbr \/>a7902c7784a1de3d47473a11e43d997c&gt; &gt;::<wbr \/>Invoke+0x6d\r\nrpcrt4!Invoke+0x73\r\nrpcrt4!Ndr64StubWorker+0xb8a\r\nrpcrt4!NdrStubCall3+0xd3\r\ncombase!CStdStubBuffer_<wbr \/>Invoke+0x6f\r\ncombase!InvokeStubWithExceptionPolicyAndTracing::<wbr \/>_<wbr \/>_<wbr \/>l6::<wbr \/>&lt;lambda_<wbr \/>c9f3956a20c9da92a64affc24fdd69ec&gt;::<wbr \/>operator()+0x22\r\ncombase!ObjectMethodExceptionHandlingAction&lt;&lt;lambda_<wbr \/>c9f3956a20c9da92a64affc24fdd69ec&gt; &gt;+0x4d\r\ncombase!InvokeStubWithExceptionPolicyAndTracing+0xe1\r\ncombase!DefaultStubInvoke+0x268\r\ncombase!SyncServerCall::<wbr \/>StubInvoke+0x41\r\ncombase!StubInvoke+0xf6\r\ncombase!ServerCall::<wbr \/>ContextInvoke+0x366\r\ncombase!ComInvokeWithLockAndIPID+0x9aa\r\ncombase!ThreadInvokeReturnHresult+0x17b\r\ncombase!ThreadInvoke+0x193\r\nrpcrt4!DispatchToStubInCNoAvrf+0x22\r\nrpcrt4!RPC_<wbr \/>INTERFACE::<wbr \/>DispatchToStubWorker+0x1b4\r\nrpcrt4!RPC_<wbr \/>INTERFACE::<wbr \/>DispatchToStub+0xb3\r\nrpcrt4!RPC_<wbr \/>INTERFACE::<wbr \/>DispatchToStubWithObject+0x188\r\nrpcrt4!LRPC_<wbr \/>SBINDING::<wbr \/>DispatchToStubWithObject+0x23\r\nrpcrt4!LRPC_<wbr \/>SCALL::<wbr \/>QueueOrDispatchCall+0x253\r\nrpcrt4!LRPC_<wbr \/>SCALL::<wbr \/>HandleRequest+0x996\r\nrpcrt4!LRPC_<wbr \/>SASSOCIATION::<wbr \/>HandleRequest+0x2c3\r\nrpcrt4!LRPC_<wbr \/>ADDRESS::<wbr \/>HandleRequest+0x17c\r\nrpcrt4!LRPC_<wbr \/>ADDRESS::<wbr \/>ProcessIO+0x939\r\nrpcrt4!LrpcIoComplete+0x109\r\nntdll!TppAlpcpExecuteCallback+0x157\r\nntdll!TppWorkerThread+0x72c\r\nkernel32!BaseThreadInitThunk+0x1d\r\nntdll!RtlUserThreadStart+0x28\r\n\r\nContoso!winrt::<wbr \/>impl::<wbr \/>consume_<wbr \/>Contoso_<wbr \/>IWidgetWatcher&lt;winrt::<wbr \/>Contoso::<wbr \/>IWidgetWatcher&gt;::<wbr \/>NotifyChange+0x3:\r\n00007ffd`ab49749d mov     rax,qword ptr [rcx] ds:00000000`00000000=????????????????\r\n<\/pre>\n<p>This is a big, ugly stack, but we can simplify it:<\/p>\n<pre>Contoso!winrt::<wbr \/>impl::<wbr \/>consume_<wbr \/>IWidgetWatcher::<wbr \/>NotifyChange+0x3\r\nContoso!winrt::<wbr \/>WidgetController::<wbr \/>SaveWidget+0x16\r\nContoso!winrt::<wbr \/>impl::<wbr \/>produce&lt;IWidgetController&gt;::<wbr \/>SaveWidget+0x1d\r\nContoso!winrt::<wbr \/>impl::<wbr \/>consume_<wbr \/>IWidgetController::<wbr \/>SaveWidget+0xd\r\nContoso!winrt::<wbr \/>Widget::<wbr \/>OnSettingsChanged$_<wbr \/>ResumeCoro$1+0x31f\r\nContoso!std::<wbr \/>experimental::<wbr \/>coroutine_<wbr \/>handle&lt;void&gt;::<wbr \/>resume+0xc\r\nContoso!winrt::<wbr \/>impl::<wbr \/>await_<wbr \/>adapter::<wbr \/>await_<wbr \/>suspend+0x15e\r\nContoso!winrt::Widget::<wbr \/>OnSettingsChanged$_<wbr \/>ResumeCoro$1+0x11a\r\nContoso!winrt::Widget::<wbr \/>OnSettingsChanged$_<wbr \/>InitCoro$2+0x6a\r\nContoso!winrt::Widget::<wbr \/>OnSettingsChanged+0x5c\r\nContoso!winrt::<wbr \/>TypedEventHandler::<wbr \/>&lt;lambda_...&gt;::<wbr \/>operator()+0x5b\r\nContoso!winrt::<wbr \/>impl::<wbr \/>delegate::<wbr \/>Invoke+0x6d\r\n(external event machinery)\r\n<\/pre>\n<p>Reading from the bottom up, we start with a bunch of external event machinery, which led to the invocation of an event handler, evidently the <code>On\u00adSettings\u00adChanged<\/code> handler.<\/p>\n<p>This handler is a coroutine, and it awaited something, and then upon resumption it called the <code>WidgetController::<wbr \/>SaveWidget<\/code> method, which called <code>WidgetWatcher::<wbr \/>NotifyChange<\/code>, but it crashed trying to make that call because the widget watcher is null. (We know this because <code>rcx<\/code> is null, and we&#8217;re trying to read its vtable.)<\/p>\n<p>The customer&#8217;s <code>On\u00adSettings\u00adChanged<\/code> handler looked like this:<\/p>\n<pre>fire_and_forget Widget::OnSettingsChanged(\r\n    const SettingsManager&amp;, const IInspectable&amp;)\r\n{\r\n    co_await ApplySettingsAsync();\r\n    m_widgetController.SaveWidget(*this);\r\n}\r\n<\/pre>\n<p>The customer saw this code and smacked their forehead. &#8220;Of course, the problem is that I forgot to hold a strong reference to the widget across the <code>co_await<\/code>.&#8221;<\/p>\n<pre>winrt::fire_and_forget Widget::OnSettingsChanged(\r\n    const winrt::SettingsManager&amp;, const winrt::IInspectable&amp;)\r\n{\r\n    <span style=\"color: #08f;\">auto lifetime = get_strong();<\/span>\r\n    co_await ApplySettingsAsync();\r\n    m_widgetController.SaveWidget(*this);\r\n}\r\n<\/pre>\n<p>But their fix didn&#8217;t help. The crashes kept coming.<\/p>\n<p>Let&#8217;s look at that crash stack again:<\/p>\n<pre>Contoso!winrt::<wbr \/>Widget::<wbr \/>OnSettingChanged$_<wbr \/>ResumeCoro$1+0x31f\r\nContoso!std::<wbr \/>experimental::<wbr \/>coroutine_<wbr \/>handle&lt;void&gt;::<wbr \/>resume+0xc\r\nContoso!winrt::<wbr \/>impl::<wbr \/>await_<wbr \/>adapter::<wbr \/>await_<wbr \/>suspend+0x15e\r\nContoso!winrt::Widget::<wbr \/>OnSettingChanged$_<wbr \/>ResumeCoro$1+0x11a\r\n<\/pre>\n<p>Reading upward, the coroutine was executing, and then decided to <code>co_await<\/code> something. The coroutine machinery called <code>await_<wbr \/>suspend<\/code>, and the <code>await_<wbr \/>suspend<\/code> immediately resumed the coroutine, causing <code>OnSettingChanged$_<wbr \/>ResumeCoro$1<\/code> to be re-entered.<\/p>\n<p>So the coroutine never really suspended. Adding a strong reference wouldn&#8217;t have helped here, since the strong reference&#8217;s job is to keep the object alive across a suspension, which hasn&#8217;t happened. (I mean, the strong reference is still required in the case where the <code>co_await<\/code> does suspend. I&#8217;m just saying that this particular crash didn&#8217;t involve a suspension.)<\/p>\n<p>Another way to observe that there was no suspension is that the stack trace leads all the way back to the external event machinery. We are still in the synchronous portion of the event handler. Nothing has suspended yet.<\/p>\n<p>I looked at how the event was registered:<\/p>\n<pre>struct Widget : WidgetT&lt;Widget&gt;\r\n{\r\n    \/\/ other constructor parameters elided for expository purposes\r\n    Widget::Widget(const WidgetController&amp; controller)\r\n        : m_weakController{ controller }\r\n    {\r\n        m_SettingChangedRevoker =\r\n             m_settingsWatcher.SettingChanged(winrt::auto_revoke,\r\n                <span style=\"color: #08f;\">{ this, &amp;Widget::OnSettingChanged })<\/span>;\r\n    }\r\n\r\nprivate:\r\n    fire_and_forget OnSettingChanged(\r\n        const SettingsManager&amp;, const IInspectable&amp;);\r\n\r\n    const weak_ref&lt;WidgetController&gt; m_weakWidgetController;\r\n    const SettingsWatcher m_settingsWatcher;\r\n    SettingsWatcher::SettingChanged_revoker m_SettingChangedRevoker;\r\n\r\n    \/\/ other members elided for expository purposes\r\n};\r\n<\/pre>\n<p>Notice that the event handler is registered with a raw <code>this<\/code> pointer.<\/p>\n<p>Registering with a raw <code>this<\/code> pointer means that you are taking full responsibility for ensuring that the object is alive at the time the event is received. This is manageable for events that are raised synchronously (such as UI events), since you can make sure to unregister the event handler from the thread that will raise the event, avoid the race condition where you unregister the event handler while the handler is running (or is irrevocably committed to running).<\/p>\n<table style=\"border-collapse: collapse;\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<th style=\"border: 1px currentcolor; border-style: none solid solid none; padding: 3px;\">Thread 1<\/th>\n<th style=\"border: 1px currentcolor; border-style: none none solid solid; padding: 3px;\">Thread 2<\/th>\n<\/tr>\n<tr>\n<td style=\"border-right: 1px solid currentcolor; padding: 3px 3px 0 3px;\">event triggered<\/td>\n<td style=\"border-left: 1px solid currentcolor; padding: 3px 3px 0 3px;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border-right: 1px solid currentcolor; padding: 3px 3px 0 3px;\">handler()<\/td>\n<td style=\"border-left: 1px solid currentcolor; padding: 3px 3px 0 3px;\">\u00a0<\/td>\n<\/tr>\n<tr>\n<td style=\"border-right: 1px solid currentcolor; padding: 3px 3px 0 3px;\">\u00a0<\/td>\n<td style=\"border-left: 1px solid currentcolor; padding: 3px 3px 0 3px;\">unregister handler<\/td>\n<\/tr>\n<tr>\n<td style=\"border-right: 1px solid currentcolor; padding: 3px 3px 0 3px;\">handler still running!<\/td>\n<td style=\"border-left: 1px solid currentcolor; padding: 3px 3px 0 3px;\">\u00a0<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>We can see from the stack that the <code>Settings\u00adWatcher<\/code> object raises events from a background thread, so a raw <code>this<\/code> capture is not safe. If the event is unregistered while a callback is in progress (or is irrevocably committed to running), the handler will have the object destructed out from under it (or possibly even have it destructed before it can execute a single instruction).<\/p>\n<p>In this case, the registration of the handler should be done with a weak reference.<\/p>\n<pre>        m_SettingChangedRevoker =\r\n             m_settingsWatcher.SettingChanged(winrt::auto_revoke,\r\n                { <span style=\"color: #08f;\">get_weak()<\/span>, &amp;Widget::OnSettingChanged });\r\n<\/pre>\n<p>The C++\/WinRT weak reference event handler pattern goes like this:<\/p>\n<pre>auto strong = weak.get():\r\nif (strong) { (strong-&gt;*handler)(); }\r\n<\/pre>\n<p>C++\/WinRT tries to upgrade the weak reference to a strong reference, and if successful, it calls the event handler with the strong reference active. When the handler returns, the strong reference is released.\u00b9<\/p>\n<p>This particular race condition is between the event handler and the revocation of the event. This particular object doesn&#8217;t revoke the event until it destructs, and the way the program uses the Widget, we don&#8217;t expect it to be destroyed until the program shuts down, and there&#8217;s no evidence that the program is shutting down.<\/p>\n<p>The &#8220;object got destructed before your handler run&#8221; scenario also doesn&#8217;t seem to match the crash dump:<\/p>\n<pre style=\"white-space: pre-wrap;\">0:019&gt; ?? ((winrt::Contoso::implementation::WidgetController*) 0x00000262`1b9ce810)\r\nstruct winrt::Contoso::implementation::WidgetController * 0x00000262`1b9ce810\r\n   +0x010 vtable           : ...\r\n   +0x018 vtable           : ...\r\n   +0x000 __VFN_table : 0x00007ffd`ab59d940\r\n   +0x008 m_references     : std::atomic&lt;unsigned __int64&gt;\r\n    ...\r\n   +0x0a0 m_widgetWatcher : winrt::Contoso::WidgetWatcher\r\n\r\n0:019&gt; ?? ((winrt::Contoso::implementation::WidgetController*) 0x00000262`1b9ce810)-&gt;m_references._Storage\r\nstruct std::_Atomic_padded&lt;unsigned __int64&gt;\r\n   +0x000 _Value           : 0x80000131`0e28d450\r\n\r\n0:019&gt; dps 0x80000131`0e28d450*2\r\n00000262`1c51a8a0  00007ffd`ab5998d8 Contoso!winrt::impl::weak_ref&lt;1,1&gt;::`vftable'\r\n00000262`1c51a8a8  00007ffd`ab5998b8 Contoso!winrt::impl::weak_source&lt;1,1&gt;::`vftable'\r\n00000262`1c51a8b0  00000262`1b9ce820 \/\/ m_object - pointer to original object (matches)\r\n00000262`1c51a8b8  0000002e`00000006 \/\/ weak + strong references\r\n<\/pre>\n<p>The weak reference control block seems to be valid, and it shows a reasonable non-zero number of strong references, so it doesn&#8217;t seem that we&#8217;re in the &#8220;Object destructed while handler is running&#8221; scenario.<\/p>\n<p>So we found another bug, but not the bug that caused this crash.<\/p>\n<p>The crash is at the call to <code>m_widgetWatcher.SaveWidget()<\/code>, so let&#8217;s look at that <code>m_widgetWatcher<\/code>.<\/p>\n<pre style=\"white-space: pre-wrap;\">0:019&gt; ?? ((winrt::Contoso::implementation::WidgetController*) 0x00000262`1b9ce810)-&gt;m_widgetWatcher\r\nstruct winrt::Contoso::WidgetWatcher\r\n   +0x000 m_ptr            : 0x00000262`1b86ec60\r\n0:019&gt; dps 0x00000262`1b86ec60 L2\r\n00000262`1b86ec60  00007ffd`ab5a1728 Contoso!winrt::impl::produce&lt;WidgetWatcher,IWidgetWatcher&gt;::`vftable'\r\n00000262`1b86ec68  00000000`00000000\r\n<\/pre>\n<p>Wait a second, the crash dump says that we crashed because the <code>m_widgetWatcher<\/code> is null, but in the dump, the <code>m_widgetWatcher<\/code> is non-null. This suggests to me that we encountered a race condition, where the <code>m_widgetWatcher<\/code> was null at the time of the crash, but <a title=\"The debugger lied to you because the CPU was still juggling data in the air\" href=\"https:\/\/devblogs.microsoft.com\/oldnewthing\/20121130-00\/?p=5943\"> in the time it took to create the crash dump, the value was updated<\/a>.<\/p>\n<p>This helped focus the next step of the investigation: Let&#8217;s look at the <code>WidgetController<\/code> and how it initializes the <code>WidgetWatcher<\/code>.<\/p>\n<pre>struct WidgetController : WidgetControllerT&lt;WidgetController&gt;\r\n{\r\n    \/\/ constructor parameters elided for expository purposes\r\n    WidgetController(const WidgetWatcher&amp; watcher) :\r\n        m_widget{ make&lt;Widget&gt;(*this) },\r\n        m_widgetWatcher{ watcher }\r\n    {\r\n    }\r\n\r\n    void SaveWidget(const Widget&amp;);\r\n\r\nprivate:\r\n    const Widget m_widget;\r\n    const WidgetWatcher m_widgetWatcher;\r\n\r\n    \/\/ other members elided for expository purposes\r\n};\r\n<\/pre>\n<p>Now the story comes into focus.<\/p>\n<p>The non-static data members of C++ objects are constructed in order of declaration in the class.\u00b2 In this case, we construct the <code>Widget<\/code> before we copy the <code>WidgetWatcher<\/code>.<\/p>\n<p>If a setting change occurs immediately after the <code>Widget<\/code> is constructed, then we have a race between the event callback thread (which will call back into the <code>Widget\u00adController<\/code>) and the construction of the <code>Widget\u00adController<\/code>&#8216;s <code>m_widgetWatcher<\/code>. If the event callback thread wins the race, then it will see a not-yet-constructed <code>m_widgetWatcher<\/code> and crash.<\/p>\n<p>That is the race condition that we hit. The event callback thread tried to use a not-yet-constructed object.<\/p>\n<p>Therefore, the final step of the fix is to force the <code>m_widget\u00adWatcher<\/code> to construct first:<\/p>\n<pre>struct WidgetController : WidgetControllerT&lt;WidgetController&gt;\r\n{\r\n    \/\/ constructor parameters elided for expository purposes\r\n    WidgetController(const WidgetWatcher&amp; watcher) :\r\n        m_widget{ make&lt;Widget&gt;(*this) },\r\n        m_widgetWatcher{ watcher }\r\n    {\r\n    }\r\n\r\n    void SaveWidget(const Widget&amp;);\r\n\r\nprivate:\r\n    <span style=\"color: #08f;\">\/\/ Order of declaration is important:\r\n    \/\/ WidgetWatcher must construct before the Widget,\r\n    \/\/ because the Widget may call into the WidgetWatcher.\r\n    const WidgetWatcher m_widgetWatcher;\r\n    const Widget m_widget;<\/span>\r\n\r\n    \/\/ other members elided for expository purposes\r\n};\r\n<\/pre>\n<p>This was a rather long investigation, with two red herrings! I don&#8217;t know whether you enjoyed it, but you can&#8217;t get that time back now.<\/p>\n<p>\u00b9 Note that if the handler is a coroutine, the handler &#8220;returns&#8221; at the point the coroutine first suspends, not when the coroutine runs to completion. That&#8217;s why we need the <code>get_strong()<\/code> inside the coroutine body: To keep the object alive through to completion.<\/p>\n<p>\u00b2 Note that the order in which the initializers are given in the constructor are irrelevant. It is the order of declaration in the class that controls the order of construction.<\/p>\n<pre>struct Example\r\n{\r\n    Example() : b{ 1 }, a{ 2 } {}\r\n\r\n    Thing1 a;\r\n    Thing2 b;\r\n};\r\n<\/pre>\n<p>In this example class, the <code>a<\/code> member is constructed first, and then the <code>b<\/code> member is constructed second. You might think that <code>b<\/code> is constructed first because it is listed first in the constructor, but that&#8217;s not the case.<\/p>\n<p>If construction occurred in order of initialization, and two constructors initialized the members in different orders, then the destructor would be unable to follow the rule that objects are destructed in the same order of construction, because the destructor doesn&#8217;t know which constructor was used.\u00b3<\/p>\n<p>\u00b3 I guess you could solve this problem by adding a hidden member variable that keeps track of which order the members were constructed, but this adds runtime costs and would likely be surprising to programmers that it&#8217;s possible that adding a constructor to a class, even one that is never called, can change its size.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Digging deeper and deeper.<\/p>\n","protected":false},"author":1069,"featured_media":111744,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[25],"class_list":["post-108146","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-oldnewthing","tag-code"],"acf":[],"blog_post_summary":"<p>Digging deeper and deeper.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/108146","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/users\/1069"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/comments?post=108146"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/posts\/108146\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media\/111744"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/media?parent=108146"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/categories?post=108146"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/oldnewthing\/wp-json\/wp\/v2\/tags?post=108146"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}