{"id":5908,"date":"2024-05-28T07:00:24","date_gmt":"2024-05-28T14:00:24","guid":{"rendered":"http:\/\/writeasync.net\/?p=5908"},"modified":"2024-05-27T21:59:49","modified_gmt":"2024-05-28T04:59:49","slug":"5908","status":"publish","type":"post","link":"https:\/\/writeasync.net\/?p=5908","title":{"rendered":"How fast from `wstring` to `string`?"},"content":{"rendered":"<p>Anyone who has dealt with both <a href=\"https:\/\/learn.microsoft.com\/en-us\/windows\/win32\/learnwin32\/working-with-strings\">&#8220;narrow&#8221; and &#8220;wide&#8221; strings<\/a> in the same C++ module is inevitably faced with this little annoyance:<\/p>\n<pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\r\nstd::wstring get_wide_text()\r\n{\r\n    \/\/ . . .\r\n}\r\n\r\nvoid write_narrow_text(std::ostream&amp; os)\r\n{\r\n    \/\/ ERROR! can't write wide string to narrow stream!\r\n    os &lt;&lt; get_wide_text();\r\n}\r\n<\/pre>\n<p>In MSVC, you might see an error like <a href=\"https:\/\/learn.microsoft.com\/en-us\/cpp\/error-messages\/compiler-errors-2\/compiler-error-c2676?view=msvc-170\">C2676<\/a> (<code>\"binary '&lt;&lt;': 'std::basic_ostream&lt;char,std::char_traits&lt;char&gt;&gt;' does not define this operator or a conversion to a type acceptable to the predefined operator\"<\/code>). But if you think about it, it&#8217;s totally sensible for the C++ standard library to <em>not<\/em> define any default operators that work across different string types.<\/p>\n<p>For one thing, it is not possible to know for certain the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Character_encoding\">encoding<\/a> of a <code>std::string<\/code> or <code>std::wstring<\/code> by just looking its character type. <a href=\"https:\/\/learn.microsoft.com\/en-us\/archive\/msdn-magazine\/2016\/september\/c-unicode-encoding-conversions-with-stl-strings-and-win32-apis\">Plenty of code out there<\/a> gets away with assuming <code>std::string<\/code> means <a href=\"https:\/\/www.unicode.org\/faq\/utf_bom.html#utf8-1\">UTF-8<\/a> and <code>std::wstring<\/code> means <a href=\"https:\/\/www.unicode.org\/faq\/utf_bom.html#utf16-1\">UTF-16<\/a>. That is, however, just convention and <strong>not<\/strong> contractual. So, if we want our program to interoperate with these string types, we have pay for what we use. We must convert!<\/p>\n<p>You might try to reach for an STL solution to this surely near-universal problem. Sadly, the code that did exist to handle this scenario, the <a href=\"https:\/\/www.open-std.org\/jtc1\/sc22\/wg21\/docs\/papers\/2017\/p0618r0.html\"><code>codecvt<\/code> header, is now deprecated<\/a>. That being said, it is not too hard to cook up a Win32 replacement for the very simple use case we are dealing with above, i.e., conversion from UTF-16 to UTF-8. The Windows API for this is <a href=\"https:\/\/learn.microsoft.com\/en-us\/windows\/win32\/api\/stringapiset\/nf-stringapiset-widechartomultibyte\">WideCharToMultiByte<\/a>.<\/p>\n<p>Now, we can fairly easily write a nice C++ wrapper over this C-style API (see <a href=\"https:\/\/github.com\/brian-dot-net\/writeasync-cpp\/commit\/1db21fc2d4ffb6e11ce09db2b6cc588e65d07545\">commit on GitHub<\/a>):<\/p>\n<pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\r\nstd::string to_utf8(const std::wstring&amp; utf16_input)\r\n{\r\n    const auto input_size = gsl::narrow&lt;int&gt;(utf16_input.size());\r\n    const auto required_size = WideCharToMultiByte(CP_UTF8, 0, utf16_input.c_str(), input_size, nullptr, 0, nullptr, nullptr);\r\n    THROW_LAST_ERROR_IF_MSG(required_size == 0, &quot;Failed to get required size&quot;);\r\n    auto utf8_result = std::string(required_size, '&#92;&#48;');\r\n    const auto actual_size = WideCharToMultiByte(CP_UTF8, 0, utf16_input.c_str(), input_size, utf8_result.data(), required_size, nullptr, nullptr);\r\n    THROW_LAST_ERROR_IF_MSG(actual_size == 0, &quot;Failed to convert to UTF-8&quot;);\r\n    return utf8_result;\r\n}\r\n<\/pre>\n<p>Note that we&#8217;re using a few helper libraries here: <a href=\"https:\/\/github.com\/microsoft\/GSL\">GSL<\/a> (for a bit of safety) and <a href=\"https:\/\/github.com\/microsoft\/wil\">WIL<\/a> (for a bit of convenience). Also note the &#8220;call twice&#8221; pattern here to first discover how long the converted string will be so that we can prepare a buffer of the right size on the final call. (This pattern is used by so many Win32 APIs, that <a href=\"https:\/\/github.com\/microsoft\/wil\/wiki\/Win32-helpers#functions-returning-variable-sized-strings\">WIL has helpers for one common resize-and-retry strategy<\/a>.)<\/p>\n<p>If you want simplicity, it&#8217;s hard to beat this implementation. Ah, but wait, it&#8217;s a bit <em>too<\/em> simple. Because 0 is used an error signal in this Win32 API, we cannot tell the difference between a true zero-length string and zero-because-we-failed. This should fix it (see <a href=\"https:\/\/github.com\/brian-dot-net\/writeasync-cpp\/commit\/0d858a49a3dab6291688c224f418a961f9c5030c\">commit on GitHub<\/a>):<\/p>\n<pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\r\n    const auto input_size = gsl::narrow&lt;int&gt;(utf16_input.size());\r\n    if (input_size == 0)\r\n    {\r\n        return {};\r\n    }\r\n<\/pre>\n<p>Wait, we still need to account for totally invalid inputs (see <a href=\"https:\/\/github.com\/brian-dot-net\/writeasync-cpp\/commit\/2175a5aa733873d27aebd32b214793611a6c09ae\">commit on GitHub<\/a>):<\/p>\n<pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\r\n    const auto required_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16_input.c_str(), input_size, nullptr, 0, nullptr, nullptr);\r\n\/\/ . . .\r\n    const auto actual_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, utf16_input.c_str(), input_size, utf8_result.data(), required_size, nullptr, nullptr);\r\n<\/pre>\n<p>Okay, so <em>now<\/em> it&#8217;s simple and it definitely solves the problem we had above:<\/p>\n<pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\r\nvoid write_narrow_text(std::ostream&amp; os)\r\n{\r\n    \/\/ Error is gone, and everything works... \r\n    \/\/ ...as long as we're prepared to deal with UTF-8 output\r\n    os &lt;&lt; to_utf8(get_wide_text());\r\n}\r\n<\/pre>\n<p>But there is something not quite satisfying about this solution. In particular, what if our access pattern looked more like this?<\/p>\n<pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\r\nvoid write_narrow_text(std::ostream&amp; os)\r\n{\r\n    const auto s = to_utf8(get_wide_text());\r\n    for (auto c : s)\r\n    {\r\n        \/\/ put commas before each space char\r\n        if (c == ' ')\r\n        {\r\n            os &lt;&lt; ',';\r\n        }\r\n\r\n        os &lt;&lt; c;\r\n    }\r\n}\r\n<\/pre>\n<p>The function above would produce an output like <code>\"a, b, c\"<\/code> when given a (wide string) input of <code>L\"a b c\"<\/code>. We have to walk through each input char individually, but we <em>also<\/em> had to do that when converting the input. In fact, we had to do it <strong>twice<\/strong> during conversion as discussed before. Luckily, there is no rule that says we have to produce a full-fledged <code>std::string<\/code> from the conversion process &#8212; the <code>WideCharToMultiByte<\/code>, being a <a href=\"https:\/\/www.reddit.com\/r\/C_Programming\/comments\/r5rs07\/can_you_use_the_windows_api_with_c_and_not_c\/\">typical C-callable Win32 API<\/a>, works purely on raw buffers. Wouldn&#8217;t it make sense then to convert as we go, block by block? Let&#8217;s find out!<\/p>\n<p>First, we have to figure out a strategy for doing this block conversion. Using a <a href=\"https:\/\/stackoverflow.com\/questions\/34968441\/function-with-a-size-t-template-parameter\"><code>size_t<\/code> template parameter<\/a> to define the output block size gives us some niceties around <code>constexpr<\/code>-ness. But what should the input block size be? Astute readers of <a href=\"https:\/\/www.unicode.org\/versions\/Unicode15.1.0\/\">the Unicode standard<\/a> will note that UTF-16 can represent all characters in at most two <a href=\"https:\/\/www.unicode.org\/glossary\/#code_unit\">code units<\/a>. In other words, we might have one or two <code>wchar_t<\/code> elements for each &#8220;character&#8221; (or really, <a href=\"https:\/\/unicode.org\/glossary\/#code_point\">code point<\/a>). Further, UTF-8 may need up to four code units (which we store as <code>char<\/code>) for the same code point. The absolute worst case then if we knew nothing more than this is the expansion of one <code>wchar_t<\/code> of input to four <code>char<\/code>s of output. (It turns out only <a href=\"https:\/\/datacadamia.com\/data\/type\/text\/surrogate\">surrogate pairs<\/a> should end up producing the longest sequences in UTF-8, but this 1:4 ratio is a safe upper bound for our purposes.)<\/p>\n<p>Next, we are keeping our char-by-char goal in mind, which means we should build a <a href=\"https:\/\/devblogs.microsoft.com\/cppblog\/documentation-for-cpp20-ranges\/\">range<\/a> type. Internally, the range should use some sort of <a href=\"https:\/\/www.geeksforgeeks.org\/input-iterators-in-cpp\/\">input iterator<\/a> to do the heavy lifting. How do we know when we&#8217;ve reached the end? We could use a <a href=\"https:\/\/www.studyplan.dev\/pro-cpp\/ranges-sentinels\">sentinel<\/a>.<\/p>\n<p>Putting all this together, we end up with something like this for our &#8220;public&#8221; API:<\/p>\n<pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\r\ntemplate &lt;size_t N&gt;\r\n    requires (N &gt; 3)\r\ndetail::Utf8Range&lt;N&gt; to_utf8(const std::wstring&amp; utf16_input)\r\n{\r\n    return { std::wstring_view{utf16_input} };\r\n}\r\n<\/pre>\n<p>The user needs to decide on a block size N and pass an input string, and we give back the range as promised:<\/p>\n<pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\r\ntemplate &lt;size_t N&gt;\r\n    requires (N &gt; 3)\r\nclass Utf8Range\r\n{\r\npublic:\r\n    using Input = std::wstring_view;\r\n    using Output = gsl::span&lt;char&gt;;\r\n    class Iterator;\r\n    struct Sentinel {};\r\n\r\n    Utf8Range(Input input) noexcept : m_input{ input }, m_block{}\r\n    {}\r\n\r\n    Iterator begin()\r\n    {\r\n        return { m_input, Output{m_block} };\r\n    }\r\n\r\n    Sentinel end()\r\n    {\r\n        return {};\r\n    }\r\n\r\n    \/\/ . . .\r\n\r\nprivate:\r\n    Input m_input;\r\n    std::array&lt;char, N&gt; m_block;\r\n};\r\n<\/pre>\n<p>This should be enough to support a <a href=\"https:\/\/learn.microsoft.com\/en-us\/cpp\/cpp\/range-based-for-statement-cpp?view=msvc-170\">range-based for loop<\/a> to do a single pass over the converted output string. Now what should the iterator look like? It has a few complexities like needing to support conversion of the next block when we exhaust the current one and the expectation of passing back the first char right away (unlike C# enumerators which are <a href=\"https:\/\/learn.microsoft.com\/en-us\/dotnet\/api\/system.collections.ienumerator.movenext?view=net-8.0#remarks\">positioned <em>before<\/em> the first element<\/a>), just to name a few. As a first cut, this might work (see <a href=\"https:\/\/github.com\/brian-dot-net\/writeasync-cpp\/commit\/c1abac1a29b27a1fc57c4efd792a3e7c0af7aee4\">commit on GitHub<\/a>):<\/p>\n<pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\r\n    class Iterator\r\n    {\r\n    public:\r\n        using difference_type = std::ptrdiff_t;\r\n        using value_type = char;\r\n\r\n        Iterator() noexcept : m_source{}, m_next_offset{}, m_block{}, m_index{}\r\n        {}\r\n\r\n        Iterator(Input source, Output block)\r\n            : m_source{ source }, m_next_offset{}, m_block{ block }, m_index{ m_block.size() - 1 }\r\n        {\r\n            move_next();\r\n        }\r\n\r\n        char operator*() const\r\n        {\r\n            return m_block&#x5B;m_index];\r\n        }\r\n\r\n        Iterator&amp; operator++()\r\n        {\r\n            if (!at_end())\r\n            {\r\n                move_next_char();\r\n            }\r\n\r\n            return *this;\r\n        }\r\n\r\n        Iterator operator++(int)\r\n        {\r\n            Iterator prev = *this;\r\n            ++*this;\r\n            return prev;\r\n        }\r\n\r\n        bool operator==(Sentinel) const noexcept\r\n        {\r\n            return at_end();\r\n        }\r\n\r\n    private:\r\n        bool at_block_end() const noexcept\r\n        {\r\n            return m_index == m_block.size() - 1;\r\n        }\r\n\r\n        bool at_end() const noexcept\r\n        {\r\n            return (m_next_offset &gt;= m_source.size()) &amp;&amp; at_block_end();\r\n        }\r\n\r\n        void move_next_char()\r\n        {\r\n            if (!at_block_end())\r\n            {\r\n                ++m_index;\r\n            }\r\n            else\r\n            {\r\n                move_next();\r\n            }\r\n        }\r\n\r\n        void move_next()\r\n        {\r\n            if (at_end())\r\n            {\r\n                return;\r\n            }\r\n\r\n            const auto hr = try_move_next();\r\n            THROW_IF_FAILED_MSG(hr, &quot;Error near offset %llu while attempting UTF-8 conversion&quot;, m_next_offset);\r\n\r\n            m_index = 0;\r\n        }\r\n\r\n        HRESULT try_move_next()\r\n        {\r\n            const auto input_block = next_block();\r\n            const auto input_size = gsl::narrow&lt;int&gt;(input_block.size());\r\n            static constexpr const auto output_size = gsl::narrow&lt;int&gt;(N);\r\n            const auto actual_size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, input_block.data(), input_size, m_block.data(), output_size, nullptr, nullptr);\r\n            if (actual_size == 0)\r\n            {\r\n                return HRESULT_FROM_WIN32(GetLastError());\r\n            }\r\n\r\n            m_block = m_block.subspan(0, actual_size);\r\n            m_next_offset += actual_size;\r\n            return S_OK;\r\n        }\r\n\r\n        auto next_block() const\r\n        {\r\n            static constexpr const auto M = (N \/ 4);\r\n            const auto max_input_size = m_source.size() - m_next_offset;\r\n            const auto input_size = (M &lt;= max_input_size) ? M : max_input_size;\r\n            return m_source.substr(m_next_offset, input_size);\r\n        }\r\n\r\n        Input m_source;\r\n        size_t m_next_offset;\r\n        Output m_block;\r\n        size_t m_index;\r\n    };\r\n<\/pre>\n<p>This works for the scenarios we described above, correctly sliding the block forward to the next as we reach the end of each previous one. And then someone asks, &#8220;What about emoji?&#8221; Ah, yes &#8212; <a href=\"https:\/\/www.contentful.com\/blog\/unicode-javascript-and-the-emoji-family\/\">emoji characters<\/a> use the surrogate pair encoding we talked about before. Each one will use two <code>wchar_t<\/code>s, and it would be an error to try to split one &#8220;down the middle,&#8221; so to speak. We can see this error in action by using this test:<\/p>\n<pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\r\nTEST(to_utf8_test, emoji_block)\r\n{\r\n    const auto raw = std::array&lt;uint16_t, 5&gt;{0xD83D, 0xDE0E, 0xD83D, 0xDC4D, 0x0000};\r\n    const auto input = std::wstring{ reinterpret_cast&lt;const wchar_t*&gt;(raw.data()) };\r\n    const auto expected = std::string{ &quot;\\xF0\\x9F\\x98\\x8E\\xF0\\x9F\\x91\\x8D&quot; };\r\n\r\n    test_utf8_block&lt;4&gt;(input, expected);\r\n    test_utf8_block&lt;9&gt;(input, expected);\r\n    test_utf8_block&lt;14&gt;(input, expected);\r\n    test_utf8_block&lt;19&gt;(input, expected);\r\n}\r\n<\/pre>\n<p>The first test function (<code>test_utf8_block&lt;4&gt;<\/code>) fails because the four <code>char<\/code> output block is mapped to a one <code>wchar_t<\/code> input block, and <a href=\"https:\/\/www.unicode.org\/faq\/utf_bom.html#utf8-5\">it is not valid to have an unpaired surrogate<\/a>. The fix is to detect this error and then widen the input block by one <code>wchar_t<\/code> as a retry step. Worst case, we&#8217;ll fail again and report the same error. With luck, though, we&#8217;ll continue happily converting the rest of the string (see <a href=\"https:\/\/github.com\/brian-dot-net\/writeasync-cpp\/commit\/9c67ec07d2f200de060afdefd982f556d5acbf59\">commit on GitHub<\/a>):<\/p>\n<pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\r\n            if (FAILED(try_move_next&lt;false&gt;()))\r\n            {\r\n                const auto hr = try_move_next&lt;true&gt;();\r\n                THROW_IF_FAILED_MSG(hr, &quot;Error near offset %llu while attempting UTF-8 conversion&quot;, m_next_offset);\r\n            }\r\n\r\n\/\/ . . .\r\n        template &lt;bool OneMore&gt;\r\n        HRESULT try_move_next()\r\n        {\r\n            const auto input_block = next_block&lt;OneMore&gt;();\r\n            \/\/ . . .\r\n        }\r\n\r\n        template &lt;bool OneMore&gt;\r\n        auto next_block() const\r\n        {\r\n            static constexpr const auto M = (N \/ 4) + (OneMore ? 1 : 0);\r\n            \/\/ . . .\r\n        }\r\n<\/pre>\n<p>Whew! That&#8217;s a fair amount of work, but it seems to do the job. Now to finally answer the question asked in the title of this article &#8212; how fast is this? Let&#8217;s run some <a href=\"https:\/\/github.com\/google\/benchmark\">benchmarks<\/a> (see <a href=\"https:\/\/github.com\/brian-dot-net\/writeasync-cpp\/commit\/8acde3400418bb3d7f706431a5a526614b77c3b2\">full commit on GitHub<\/a>):<\/p>\n<pre class=\"brush: cpp; title: ; notranslate\" title=\"\">\r\nstatic void to_utf8_very_long_string(benchmark::State&amp; state)\r\n{\r\n    const auto input = make_very_long_string();\r\n    auto ss = std::stringstream{};\r\n\r\n    for (auto _ : state)\r\n    {\r\n        ss.clear();\r\n        const auto result = str::to_utf8(input);\r\n        ss &lt;&lt; result;\r\n    }\r\n}\r\n\r\n\/\/ . . .\r\n\r\nstatic void to_utf8_very_long_string_by_char(benchmark::State&amp; state)\r\n{\r\n    const auto input = make_very_long_string();\r\n    auto ss = std::stringstream{};\r\n\r\n    for (auto _ : state)\r\n    {\r\n        ss.clear();\r\n        for (auto c : str::to_utf8(input))\r\n        {\r\n            ss &lt;&lt; c;\r\n        }\r\n    }\r\n}\r\n\r\n\/\/ ...\r\n\r\ntemplate &lt;size_t N&gt;\r\nstatic void to_utf8_block_very_long_string(benchmark::State&amp; state)\r\n{\r\n    const auto input = make_very_long_string();\r\n    auto ss = std::stringstream{};\r\n\r\n    for (auto _ : state)\r\n    {\r\n        ss.clear();\r\n        for (auto c : str::to_utf8&lt;N&gt;(input))\r\n        {\r\n            ss &lt;&lt; c;\r\n        }\r\n    }\r\n}\r\n<\/pre>\n<p>These are the basic benchmarks we will consider. We have the full round-trip through a <code>std::string<\/code> version tested in <code>to_utf8_very_long_string<\/code> and <code>to_utf8_very_long_string_by_char<\/code> as well as a template function <code>to_utf8_block_very_long_string<\/code> for testing a set of block-based variations. The results on my machine look like this:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n-------------------------------------------------------------------------------\r\nBenchmark                                     Time             CPU   Iterations\r\n-------------------------------------------------------------------------------\r\nto_utf8_very_long_string                  56961 ns        52550 ns        13380\r\nto_utf8_very_long_string_by_char        2036363 ns      1801273 ns          373\r\nto_utf8_64_block_very_long_string        728050 ns       697545 ns         1120\r\nto_utf8_256_block_very_long_string       694001 ns       655692 ns         1120\r\nto_utf8_1024_block_very_long_string      714926 ns       655692 ns         1120\r\nto_utf8_4096_block_very_long_string      718333 ns       655692 ns         1120\r\nto_utf8_16384_block_very_long_string     997483 ns      1024933 ns          747\r\n<\/pre>\n<p>This tells us that if char-by-char is the need, the block-based algorithm can beat the simpler one (when using roughly ~1K block sizes). But if you just need to output the whole string as-is, it is still orders of magnitude faster to do the full round-trip conversion (maybe there is some <a href=\"https:\/\/stackoverflow.com\/questions\/1516622\/what-does-vectorization-mean\">vectorization<\/a> going on?). In any case, we have shown yet again the triumph of measurement over assumption!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Anyone who has dealt with both &#8220;narrow&#8221; and &#8220;wide&#8221; strings in the same C++ module is inevitably faced with this little annoyance: std::wstring get_wide_text() { \/\/ . . . } void write_narrow_text(std::ostream&amp; os) { \/\/ ERROR! can&#8217;t write wide string&hellip; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[101,104],"tags":[],"class_list":["post-5908","post","type-post","status-publish","format-standard","hentry","category-native","category-performance"],"_links":{"self":[{"href":"https:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5908","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=5908"}],"version-history":[{"count":6,"href":"https:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5908\/revisions"}],"predecessor-version":[{"id":5914,"href":"https:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5908\/revisions\/5914"}],"wp:attachment":[{"href":"https:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=5908"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=5908"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=5908"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}