private function conv($str)
    {
        // prefer mb_convert_encoding if available
        if ($this->supportsExtMbstring) {
            return mb_convert_encoding($str, 'UTF-16BE', 'UTF-8');
        }

        // otherwise match each UTF-8 character byte sequence, manually convert
        // it to its Unicode code point and then encode as UTF-16BE byte sequence.
        return preg_replace_callback('/./su', function ($m) {
            if (!isset($m[0][1])) {
                // U+0000 - U+007F single byte ASCII/UTF-8 character
                return "\x00" . $m[0];
            }

            if (isset($m[0][3])) {
                // U+10000 - U+10FFFF uses four UTF-8 bytes and 4 UTF-16 bytes
                // get code point and convert to higher and lower surrogate
                $code = ((ord($m[0][0]) & 0x07) << 18) + ((ord($m[0][1]) & 0x3F) << 12) + ((ord($m[0][2]) & 0x3F) << 6) + (ord($m[0][3]) & 0x3F);
                return pack('nn', ($code - 0x10000 >> 10) | 0xD800, ($code & 0x03FF) | 0xDC00);
            }

            if (isset($m[0][2])) {
                // U+0800 - U+FFFF uses three UTF-8 bytes
                $code = ((ord($m[0][0]) & 0x0F) << 12) + ((ord($m[0][1]) & 0x3F) << 6) + (ord($m[0][2]) & 0x3F);
            } else {
                // U+0080 - U+07FF uses two UTF-8 bytes
                $code = ((ord($m[0][0]) & 0x1F) << 6) + (ord($m[0][1]) & 0x3F);
            }

            return chr($code >> 8) . chr($code & 0xFF);
        }, $str);
    }