ぷろみん

プログラミング的な内容を扱ってます

文字がレンダリングされるまで

概要

utf8が普及した昨今でも文字のレンダリングは全然簡単になりません。
その理由を日本語のレンダリングを通して学びましょう。

今回は文字列自体の複雑さにはあまり触れません。
サロゲートペアとかで検索すれば良い解説記事を見つけることができるかと思います。

日本語

日本語のレンダリングを目標とした時に、まず日本語を定義する必要があります。
定義がなければ、日本語文字の画像を全部集めることができません。

十分なプリレンダリング空間が確保できる場合はISO/IEC 8859を選択すれば良いと思います。
それ以外の選択肢はDirectX9を使う必要があり2048x2048のテクスチャしか使えない等の環境要因か、 各ロケールへの最適化等の場合に利用が考えられます。

以下に候補が列挙されています。 http://www.unicode.org/Public/MAPPINGS/

JIS X 0208

一番基礎的な文字セットです。
日本語に対応したと言うには十分ですが、~等の扱いが難しい記号、半角カタカナ、ASCIIを含めない等状況によって加工する必要があるセットです。
最低限の実装には便利ですがunicode.orgからはOBSOLETE扱いされています。

Unicodeの中のJIS X 0208に当たる文字を取得 - 強火で進め

コードページ932

マイクロソフトが使っている基準だから多分大丈夫だろうという選択です。
ただ、Windowsしか考慮していないのでutf16を扱う必要があり嬉しくないことが多いです。

Microsoftコードページ932 - Wikipedia
https://www.unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP932.TXT

ISO/IEC 8859

unicodeで定義される全ての文字を一般的に扱う場合に便利な規格です。
全ての文字に対応していれば日本語にも対応しているだろうというアプローチです。
ただ、その分対応している文字は多岐に渡り全ての文字を簡単な方法でプリレンダリングしておくことは困難です。
日本語の表示という目標に対してのアプローチとしては基本的にオーバースペックです。

フォント

日本語を定義したことで必要なコードポイント一覧が完成しました。
画像を得るためにはコードポイントと関連付けられるデータフォーマットを選択する必要があります。

Apple Advanced Typography(AAT)

Apple Advanced Typography - Wikipedia
上記を見ると「え、もはや文字じゃないよね」って感じのサインのような画像が掲載されています。
文字は文字です。諦めてレンダリングしましょう。

しかし、複雑な文字をレンダリングするとなると複数の実装方法が存在してしまうのは仕方ないことと言えるかもしれません。
AATは文字の変形の役割を果たすシェーピングスクリプトをフォントデータに埋め込む方法をとりました。

OpenType Layout

OpenTypeフォントとも呼ばれたりします。
AATとは違って規格でシェーピングスクリプトまで決まっているようです。
なのでフォントデータは規格を通してシェーピングスクリプトを選択できるので最低限の情報を埋め込むことで目標を達成できます。

TrueType

OpenType Layoutに対応しようという動きもあったようですが、責務が複雑になることを嫌って分化したようです。
おなじみのTTFフォーマットです。
正しい位置にレンダリングするのはレイアウトエンジンの役割でTrueTypeはコードポイントを指定した位置にラスタライズする役割を担います。

レンダリングAPI

フォントのラスタライズまでできたら後はテクスチャに焼いてレンダリングするだけです。
しかし、DirectXOpenGLといったレンダリングAPIはただの出入り口でありプラットフォームとは別の問題です。
MacOSでのVulkanはMetalを使っていますし、Windows10のDirectX9はDirectX11や12を使っています。そして、その中では更にベンダーの固有取り決めを通して役割を果たしているでしょう。

よくある要望としてDirectX9に対応して欲しいというものがありますが、それはDirectX9 APIに対応して欲しいのか、DirectX9対応のデバイス全てに対応して欲しいのかはっきりさせる必要があります。
DirectX9対応のデバイスに対する動作確認環境を用意することは難しい割に対象ユーザがほとんど存在していないのでメリットが少ないです。

以下は私の推測であり、確実な情報ではありません。
動作環境としてはまずWindows Vistaを用意してもらう必要があります。また、DirectX9対応と書かれた最古のGPUも準備する必要があります。
これはXPぐらいからのDirectX9は内部的にDirectX9.1以降を使っていると私が考えているためです。
DirectX9.1/9.2/9.3は公式なバージョニングかは不明ですが、DirectX11以降に設定できるD3D_FEATURE_LEVEL_9_1と対応しています。
これらから生成できる互換スワップチェインは厳密にはDirectX9との互換性がありません。
この互換性がないことについてはMicrosoft公式が言及していたので確実だと思います。

まとめ

DirectX9で日本語表示して欲しいという要望は曖昧過ぎる。

参考

Chromeとかで使われている文字レンダリング技術の作者のサイト
http://behdad.org/text/

C++でgRPC for Windows

必要な前提知識

ライブラリのビルド

gRPCはビルドパスが複雑な上、公式のWindows対応が少し弱いため試すだけで一苦労になりがちです。 単にやってみたいだけなら安定して色々できるGoを使うことをお勧めします。Windowsでも簡単です。
これもBazelがWindowsで安定に動作するようになれば、そっちに移行すべきだと思います。 現状ではgRPCの単純なビルドでも失敗するので採用し辛いです。

ここで利用するのはvcpkgです。
vcpkgの最大の利点はMicrosoftが管理していることです。これでWindowsのツールチェインの取得を失敗するようなことは限りなく少なくなるでしょう。 私はいつもGit for WindowsというBash付きの環境を導入するのですが、vcpkgはportable gitを利用しているようなので、それに合わせた方が環境を汚さないかもしれません。

git clone https://github.com/Microsoft/vcpkg
cd vcpkg

Windows環境の場合、コマンドプロンプトpowershellで実行しないと失敗する時があるので注意。 後、Visual Studioの英語の言語パックがないと失敗する。

bootstrap-vcpkg.bat

これでvcpkg.exeがビルドされる。

vcpkg.exe install grpc

これでx86版のライブラリがビルドされます。

ビルドは内部的にはCMakeが使われているので、ライブラリの全ての機能を使うためにはCMakeを使う必要があります。

手動

結局はパス問題なので、パスを好きな手段で繋げることでCMakeを使わずにビルドすることもできます。 vcpkgは親切にprotobufferのコンパイルと同時にProtoファイルのコンパイラであるprotoc.exeもビルドしてくれています。

その配置先である以下のバイナリを利用してProtoファイルから.pb.ccと.pb.hを生成します。 vcpkg/installed/x86-windows/tools/protobuf/protoc.exe

protocの引数については今回のテーマではないので以下のリンク等を参考にしてください。

protocプラグインの書き方 - Qiita
grpc/CMakeLists.txt at master · grpc/grpc · GitHub

gRPCのファイル生成にはプラグインが必要です。プラグインのパスは以下です。
vcpkg/installed/x86-windows/tools/grpc/grpc_cpp_plugin.exe

上記パスを引数に含めたprotocの実行によりProtoファイルから.grpc.pb.ccと.grpc.pb.hが生成されます。 これでファイル生成作業は終了です。

ビルドするには以下のパスを通してください。
vcpkg/installed/x86-windows/debug/lib
vcpkg/installed/x86-windows/include
実行環境には以下のパスを通してください。
vcpkg/installed/x86-windows/debug/bin

後は普通にビルドできます。

CMake

vcpkgは各ビルドしたライブラリをパッケージ化してくれます。 ただ、そのままではパッケージを見つけることができないのでCMake実行時にパッケージを見つけてくれるスクリプトを読み込ませます。

詳細を知りたい方は以下を読んでください。
vcpkg/integration.md at master · Microsoft/vcpkg · GitHub

方法としてはcmakeの実行時にvcpkg.cmakeのパスを渡すだけです。

cmake .-DCMAKE_TOOLCHAIN_FILE=C:\vcpkg\scripts\buildsystems\vcpkg.cmake

上記によりfind_packageによりinstallしたライブラリが見つかるようになります。

ただ、見つかるパッケージについては
vcpkg/installed/x86-windows/share/gRPCTargets.cmake
等を読む必要があり不便に感じます。

install終了時に

The package grpc:x86-windows provides CMake targets:

find_package(gRPC CONFIG REQUIRED) # Note: 8 target(s) were omitted. target_link_libraries(main PRIVATE gRPC::gpr gRPC::grpc gRPC::grpc++ gRPC::grpc_cronet)

等とは言ってくれているのですが。

protoファイルからCMakeで.pb.ccと.pb.hの生成を行うには以下のようにします。

find_package(protobuf REQUIRED)
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS foo.proto)

grpcの方のファイル生成サポートはgRPC::grpc_cpp_pluginだけなので下記のようにadd_custom_commandでコマンドを走らせる必要があります。
https://github.com/grpc/grpc/blob/master/examples/cpp/helloworld/CMakeLists.txt

後は各出力を自身のプロジェクトに組み込みビルドするだけです。 Protoファイルを書き換えてビルドするとそのまま反映されるので便利です。

C++20時代のポリモーフィズム

概要

ポリモーフィズム良いですよね。
でも、継承嫌ですよね。
MixInみたいな綺麗な継承が使える場面も現実的なサービスを考えると難しい。
そこで簡単に追加、削除ができ可読性も悪くないstd::variantとconceptを使ったC++20時代のポリモーフィズムを紹介します。

コード

#include <iostream>
#include <vector>
#include <string>
#include <variant>

struct A{};
struct B
{
    std::string ToString()
    {
        return "B";
    }
};
struct C{};
struct D{};
struct E{};

using VariantType = std::variant<A, B, C, D, E>;

template<class T>
concept bool Printable = requires(T t){
    t.ToString();
};

void PrintIfPrintable(VariantType variant)
{
    std::visit([](auto&& arg)
        {
            if constexpr (Printable<decltype(arg)>)
            {
                std::cout << arg.ToString() << std::endl;
            }
        }, variant);
}

int main()
{
    std::vector<VariantType> variants;
    variants.emplace_back(A());
    variants.emplace_back(B());
    variants.emplace_back(C());
    variants.emplace_back(D());
    variants.emplace_back(B());
    variants.emplace_back(E());

    for(const auto& variant : variants)
    {
        PrintIfPrintable(variant);
    }
}
// 出力
B
B

visitがやや複雑なのを除けば素直な実装なのではないでしょうか。

variant

variantは状態として特定の型を持つという非常にポリモーフィズムと親和性の高い特徴を持っています。
現在の型にアクセスするためにはstd::visitを使わなければならないという制約は付くもののvariantで定義した型全てに同様の処理を加えることが簡単にできます。
また、継承によるポリモーフィズムにありがちな、このインターフェース継承しているクラスが見つからない問題を解決できることは素晴らしいです。
継承しているクラスは定義ジャンプで到達できないので大規模なプロジェクトだと数十秒かかる検索で探さないといけません。

concept

上記コードはWandboxで動作します。
Compiler optionsに-fconceptsを付けるのを忘れずに。
コンセプトの強さはこの雑に使い方書いたら動くという所にあります。
かなり実務よりな機能なので早く標準に入って欲しいです。

vcpkgでinstallするとCould not locate a complete toolsetとか言われる。

概要

Warning: The following VS instances are excluded because the English language pack is unavailable.
    C:\Program Files (x86)\Microsoft Visual Studio\2017\Community
Please install the English language pack.
Could not locate a complete toolset.

解決策

素直に書いてある通りに新しくインストーラーを持ってきて英語の言語パックを入れましょう。
ビルドチェーンが言語パックに依存しているの何とかならないんですかね。500MBくらいあるし。

その他

最近流行りのgrpcとかも難なくビルドできるし、思ったよりも良さげになってきましたね。vcpkg
googleのprotobufとかもウィンドウズならvcpkg使えよって感じになってきているし期待しても良いかもしれません。
少なくともgypよりはるかにビルド簡単なので素敵です。
自分のプロジェクトを組み込むのはまだ難しそうですが。

C++17から導入される範囲 for ループの制限緩和で何が変わるか

概要

C++17で範囲forループのbeginとendで違う型を指定できるようになりました。
範囲 for ループの制限緩和 - cpprefjp C++日本語リファレンス

これによって何が変わるのでしょうか。

イテレータの思想の違い

STLイテレータはメモリはコンテナが持ってイテレータは最小限の値のみ持つという思想で作られていました。
一方マイクロソフトの提供するAPIイテレータ自体が終端の判断をする機能を持つことが多いです。

その思想の違いをこの制限緩和は扱いやすくしてくれます。

親プロセスの名前を取得するサンプル

#include <iostream>
#include <optional>
#include <Windows.h>
#include <TlHelp32.h>

struct SelfOperationTag {};

class ProcessEntryIterator {
public:
    ProcessEntryIterator()
    {
        processEntries = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
        if (processEntries == INVALID_HANDLE_VALUE)
        {
            result = false;
            return;
        }

        processEntry.dwSize = sizeof(PROCESSENTRY32);
        result = Process32First(processEntries, &processEntry);
    }

    ~ProcessEntryIterator()
    {
        CloseHandle(processEntries);
    }

    const PROCESSENTRY32& operator*()
    {
        return processEntry;
    }

    void operator++()
    {
        result = Process32Next(processEntries, &processEntry);
    }

    operator bool() const
    {
        return result;
    }

private:
    HANDLE processEntries;
    PROCESSENTRY32 processEntry;
    bool result;
};

bool operator!=(const ProcessEntryIterator& iterator, SelfOperationTag)
{
    return iterator;
}

class Toolhelp32Snapshot {
public:
    auto begin() const
    {
        return ProcessEntryIterator();
    }

    auto end() const
    {
        return SelfOperationTag();
    };

    static std::optional<PROCESSENTRY32> Find(DWORD processID)
    {
        for (const auto& processEntry : Toolhelp32Snapshot())
        {
            if (processEntry.th32ProcessID == processID)
            {
                return processEntry;
            }
        }
        return std::nullopt;
    }
};

int main() {
    auto currentProcessID = GetCurrentProcessId();
    auto currentProcess = Toolhelp32Snapshot::Find(currentProcessID);
    if (currentProcess)
    {
        auto parentProcess = Toolhelp32Snapshot::Find(currentProcess->th32ParentProcessID);
        if (parentProcess)
        {
            std::wcout << parentProcess->szExeFile << std::endl;
        }
    }
}

煩雑なエラーハンドリングで抜けが出てしまいにくいのではないでしょうか。下記サイトでは現実的には起こらないでしょうがProcess32Firstのエラーハンドリングが抜けています。
モジュールの列挙 (PSAPI)

正しくハンドリングしてgotoやdo whileが出てくるのも辛いものです。
Windows-classic-samples/shared.c at master · Microsoft/Windows-classic-samples · GitHub

Visual Studio2017にVsVimをインストールすると上スクロールがバグる

概要

新しい環境にVisual Studio2017とVsVimをインストールすると上スクロールの際に期待しない動きをすることがある(2/3回)

バグの再現させ方

文字の折り返しが起こっているスクロールできるファイルでGHと移動すると私の環境では画面一番上の行にカレント行が移動し、カレント行が表示画面の一番下に行く。

暫定的な対処

.vimrcに下記を追加。

nnoremap H Hzt

C++17からはwstring_convertが非推奨となるので環境毎のコードが推奨される

概要

推奨されるマルチバイト文字変換。

非推奨

C++17からスタンダードライブラリのものは非推奨になる

cpprefjp.github.io

Visual Studioで下記のようなコードをC++17を有効にした状態で書くと

#include <codecvt>
#include <locale>

int main()
{
    std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter;
}

エラー C4996 'std::codecvt_utf8<wchar_t,1114111,0>': warning STL4017: std::wbuffer_convert, std::wstring_convert, and the header (containing std::codecvt_mode, std::codecvt_utf8, std::codecvt_utf16, and std::codecvt_utf8_utf16) are deprecated in C++17. (The std::codecvt class template is NOT deprecated.) The C++ Standard doesn't provide equivalent non-deprecated functionality; consider using MultiByteToWideChar() and WideCharToMultiByte() from <Windows.h> instead. You can define SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING or SILENCE_ALL_CXX17_DEPRECATION_WARNINGS to acknowledge that you have received this warning.

上記のようなエラーが出る。

推奨

それをふまえて、windows環境で推奨されるマルチバイト変換は下記のようになるだろう。

stackoverflow.com

std::basic_string::dataメソッドの知名度が低い。