P0288R9: move_only_functionが待ち遠しく、似たようなもの(劣化版)を自作しようかどうかよく悩んでいます。そんな折、Boost.TypeErasureで簡単に作れるのではないかと思いつき、試していました。

その結果がこれです: https://wandbox.org/permlink/GwWCWR0PtF4XXqmh

#include <iostream>
#include <memory>
#include <boost/type_erasure/any.hpp>
#include <boost/type_erasure/builtin.hpp>
#include <boost/type_erasure/callable.hpp>
#include <boost/type_erasure/relaxed.hpp>
 
template<typename T>
using function_t = boost::type_erasure::any<boost::mpl::vector<
    boost::type_erasure::relaxed,
    boost::type_erasure::constructible<boost::type_erasure::_self(boost::type_erasure::_self&&)>,
    boost::type_erasure::destructible<>,
    boost::type_erasure::callable<T>>>;
 
int main()
{
    auto p = std::make_unique<int>(100);
    auto q = std::make_unique<int>(200);
    function_t<void(std::unique_ptr<int>)> x([q = std::move(q)](std::unique_ptr<int> p) mutable
    {
        std::cout << *p << ' ' << *q << std::endl;
    });
    x(std::move(p));
}

コピー不可能な関数オブジェクトを格納することに成功はしています。ただ、TypeErasureのコードをぱっと読んだ感じ、どうも内部で無条件に動的メモリ確保する雰囲気なので、そこまで魅力的なやり方ではないなと思っています。move_only_functionでは、小サイズのオブジェクトを入れる際は動的メモリ確保をしないことが求められており、std::functionもそうなっています。

必ず動的メモリ確保するならstd::functionとstd::shared_ptrの組み合わせでなんとかする方法も考えられます。それとBoost.TypeErasureと完全自作とでどうするか、今日も迷っています。

この記事のカテゴリ

  • ⇒ Boost.TypeErasureでmove_only_functionを模倣

最近Boost.Asioのリファレンスを見ていて、async_acceptのオーバーロードが増えていることに気付きました: basic_socket_acceptor::async_accept – 1.66.0

このページを見る限り、仮引数でbasic_socket<>を受け取らないものが加わっていたのです。もしかして、と思い確認するとやはりそうでした。内部でsocketを作ってacceptするらしく、コールバック関数などでaccept済みのソケットを受け取るというものになっています。

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <memory>
#include <boost/asio.hpp>
 
using tcp = boost::asio::ip::tcp;
 
class Connection : public std::enable_shared_from_this<Connection>
{
public:
    static std::shared_ptr<Connection> Run(tcp::socket&& s)
    {
        auto p = std::make_shared<Connection>(std::move(s));
        p->RunImpl();
        return p;
    }
 
    Connection(tcp::socket&& s)
        : m_socket(std::move(s))
    {
    }
 
    ~Connection() = default;
 
private:
    void RunImpl()
    {
        boost::asio::async_write(m_socket, boost::asio::buffer(MESSAGE, std::strlen(MESSAGE)),
            [self = shared_from_this()](boost::system::error_code ec, std::size_t)
        {
            if (ec)
            {
                std::cerr << "write error: " << ec << std::endl;
            }
        });
    }
 
    tcp::socket m_socket;
 
    static inline constexpr const char MESSAGE[] = "OK";
 
    Connection(Connection&&) = delete;
    Connection(const Connection&) = delete;
    Connection& operator=(Connection&&) = delete;
    Connection& operator=(const Connection&) = delete;
};
 
class TcpServer
{
public:
    TcpServer(boost::asio::io_context& context, tcp::endpoint endpoint)
        : m_acceptor(context, endpoint)
    {
    }
 
    void Run()
    {
        m_acceptor.async_accept([this](boost::system::error_code ec, tcp::socket s)
        {
            OnAccept(ec, std::move(s));
        });
    }
 
    ~TcpServer() = default;
 
private:
    void OnAccept(boost::system::error_code ec, tcp::socket s)
    {
        if (ec)
        {
            std::cerr << "accept error: " << ec << std::endl;
        }
        else
        {
            Connection::Run(std::move(s));
        }
        Run();
    }
 
    tcp::acceptor m_acceptor;
 
    TcpServer(TcpServer&&) = delete;
    TcpServer(const TcpServer&) = delete;
    TcpServer& operator=(TcpServer&&) = delete;
    TcpServer& operator=(const TcpServer&) = delete;
};
 
int main()
{
    boost::asio::io_context context;
    TcpServer server(context, tcp::endpoint(tcp::v4(), 5000));
    server.Run();
    context.run();
    std::quick_exit(0);
}

どうやら1.66.0から追加されたようです。Boost 1.66.0リリースノート – boostjpより:

basic_socket_acceptor::async_accept()がソケットの参照をパラメータでとっていたが、ハンドラに渡されるよう変更。これはC++11以降でムーブサポートされている場合のみ使用できる

Boost.Asio公式のexampleも1.65と1.66とで変更が入っていました。見比べるとserverクラスのメンバー変数socket_が不要になったことが分かります。

地味ながら便利な変更点だと思います。

この記事のカテゴリ

  • ⇒ async_acceptにソケットを渡す必要がなくなっていた

HTTPサーバーを作ろうと思ったんです。それでBeastのExamplesを見てみたのですが、1番単純そうなServersのHTTP, synchronousをみても、ファイルサーバーを作るコードになっていて、まだまだ煩雑だと感じました。そこで、ファイルサーバーの処理を取り除いて、真にコアとなる処理を取り出して見ようと思いました。

常にHTTP 200 OKで空のレスポンスボディを返すようにしてみたところ、こんなコードになりました。ついでにHTTP Keep aliveへの対応やスレッドを作る処理も取っ払っています。

#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/config.hpp>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
 
namespace beast = boost::beast;
namespace http = beast::http;
namespace net = boost::asio;
using tcp = boost::asio::ip::tcp;
 
void fail(beast::error_code ec, char const* what)
{
    std::cerr << what << ": " << ec.message() << "\n";
}
 
void do_session(tcp::socket& socket)
{
    beast::error_code ec;
 
    beast::flat_buffer buffer;
    http::request<http::string_body> req;
    http::read(socket, buffer, req, ec);
    if(ec)
        return fail(ec, "read");
 
    http::response<http::empty_body> res{http::status::ok, 11};
    res.keep_alive(false);
    res.prepare_payload();
    http::serializer<false, http::empty_body> sr{res};
    http::write(socket, sr, ec);
    if(ec)
        return fail(ec, "write");
 
    socket.shutdown(tcp::socket::shutdown_send, ec);
}
 
int main(int argc, char* argv[])
{
    try
    {
        // Check command line arguments.
        if (argc != 3)
        {
            std::cerr <<
                "Usage: http-server-sync <address> <port> <doc_root>\n" <<
                "Example:\n" <<
                "    http-server-sync 0.0.0.0 8080 .\n";
            return EXIT_FAILURE;
        }
        auto const address = net::ip::make_address(argv[1]);
        auto const port = static_cast<unsigned short>(std::atoi(argv[2]));
 
        // The io_context is required for all I/O
        net::io_context ioc{1};
 
        // The acceptor receives incoming connections
        tcp::acceptor acceptor{ioc, {address, port}};
        for(;;)
        {
            // This will receive the new connection
            tcp::socket socket{ioc};
 
            // Block until we get a connection
            acceptor.accept(socket);
 
            // Launch the session
            do_session(socket);
        }
    }
    catch (const std::exception& e)
    {
        std::cerr << "Error: " << e.what() << std::endl;
        return EXIT_FAILURE;
    }
}

http::requestとhttp::responseクラスがあり、http::readとhttp::write関数があり、ボディーを表すクラスが何種類かあり、http::serializerクラスがあると言うことが分かりました。

まず、この辺りを必要に応じて作り替えていけばよいということが分かったので、満足しました。

この記事のカテゴリ

  • ⇒ Boost.Beastの最小限のコードを作ってみることにした

少し前まで、MinGWのg++ (libstdc++)ではstd::random_deviceが疑似乱数生成器で実装されていて、デフォルトコンストラクタを使うと毎回同じ乱数列が生成される、という困った挙動でした。

これが以下のコミットで最近解決されたようです。時期的にGCC 9.2から入っている様子です。このコミット、Gitリポジトリのtagのgcc-9_1_0-releaseにはなく、gcc-9_2_0-releaseに入っていることを見て確かめました。

これにより、MinGWではmsvcrt.dllのrand_s関数を使った実装になっています。また、このときプリプロセッサ定数_GLIBCXX_USE_CRT_RAND_Sが定義されるようです。これで、MinGWでも暗号論的に安全で非決定論的な乱数生成器としてrandom_deviceを使えるようになりました。

実際、cpprefjpのサンプルコードをMinGWのg++でコンパイル・実行すると、毎回異なる値が出力される結果が見られます。

なお、c++ – std::random_deviceが生成する数値列が毎回同じなのはOK? – スタック・オーバーフローのalphaさんのコメントで指摘されているCygwinの場合と同様、entropyメンバー関数は0.0を返すようになっていました。

というわけで、MinGWでstd::random_deviceが使い物になるようになりました。MinGWで古いバージョンのGCCを使っているかたは、ぜひ9.2以降にバージョンアップしましょう。

この記事のカテゴリ

  • ⇒ MinGWのrandom_deviceがちゃんとしたものになった

気分一新を兼ねて、タイトルのとおり、このブログ含めdev.activebasic.com全体を常時HTTPSでの提供に切り換えました。そういうご時世ですので。HSTSはしばらくしたら投入するつもりです。

この記事のカテゴリ

  • ⇒ HTTPSにしました

前回(ドライブ文字を使わないパス指定)の少し変わったパス文字列、<filesystem>のpathだとどうなるのか、気になって試してみました。

#include <filesystem>
#include <iostream>
 
int main()
{
  using namespace std::experimental::filesystem;
 
  path x(LR"(\\?\Volume{e96621af-7084-11e3-80b0-XXXXXXXXXXXX}\Windows\system.ini)");
  std::wcout << x.root_name() << std::endl;
}

root_nameは、ドライブ文字を含むパスであれば、ドライブ文字の部分を返すメンバー関数です。

これをVisual C++ 2017 (15.3)でコンパイルします。実行結果はこうなりました。

\\?

これはつまり、UNCのため、バックスラッシュ2つで始まる部分はroot name扱いということなのだと思います。Volume{……}までroot name扱いにはなりませんでした。

この記事のカテゴリ

  • ⇒ 変わったパスとVC++のpathクラス
  • ⇒ 変わったパスとVC++のpathクラス

Windowsの絶対パスと言えば、C:\Windowsのようなドライブ文字で始まる表記を思い浮かべることと思います。しかし、ドライブ(ボリューム)を指定する表記はほかにもあります。

ボリュームGUID
各ボリュームに対するGUIDです。mountvolコマンドで確認できます。
ディスク番号・パーティション番号による表記
正式な名称は分かりませんが、Harddisk0Partition1のような文字列です。それぞれの番号は、diskpartコマンドやWMIなどで確認できるはずです。CDドライブなどでは、CdRom0などとなるようです。

いずれも、SysinternalsのWinObjで確認すると、C:などのドライブ文字と同じ\GLOBAL??名前空間に存在しています。つまり、C:などのドライブ文字と取っ替えて使えるはずと予想するわけです。実際、次のプログラム、3種類のどのパスの表記でも同じように動きます。

#define UNICODE
#define _UNICODE
 
#include <iostream>
#include <fstream>
#include <string>
#include <windows.h>
 
int main()
{
#if 1
  WCHAR path[] = LR"(\\?\Volume{e96621af-7084-11e3-80b0-XXXXXXXXXXXX}\Windows\system.ini)";
#elif 1
  WCHAR path[] = LR"(\\?\Harddisk0Partition2\Windows\system.ini)";
#else
  WCHAR path[] = LR"(C:\Windows\system.ini)";
#endif
  std::ifstream ifs(path);
  if (ifs)
  {
    std::cout << ifs.rdbuf() << std::endl;
  }
}

すぐに思い浮かぶ用途は、ドライブ文字を割り当てていないボリューム(ドライブ)へのアクセスですね。というか、ドライブ文字を割り当てていないボリュームで、GUID表記がなされている事例を見かけて、この存在を知りました。

この記事のカテゴリ

  • ⇒ ドライブ文字を使わないパス指定
  • ⇒ ドライブ文字を使わないパス指定

C++ filesystemには、パーミッションを取得・設定する機能があります。そんなわけで、たとえばこれをその辺のUnix系システムでg++を使ってコンパイル・実行すれば、755という出力が得られるはずです。

#include <iostream>
#include <experimental/filesystem>
 
int main()
{
  using namespace std::experimental::filesystem;
 
  auto perm = status("/bin/sh").permissions();
  std::cout << std::oct << static_cast<int>(perm) << std::endl;
}

さて、これWindowsだとどうなるのだろう?と思うわけです。試してみました。


これをVisual C++ 2015でコンパイル・実行すると、777という出力になりました。

#include <iostream>
#include <experimental/filesystem>
 
int main()
{
  using namespace std::experimental::filesystem;
 
  auto perm = status(R"(C:\Windows\win.ini)").permissions();
  std::cout << std::oct << static_cast<int>(perm) << std::endl;
}

無条件に777となるのでしょうか?というわけで、Visual C++の実装を眺めてみます。探したところ、VC++ 2015やVC++ 2017ではsrc/src/filesys.hにありました。

  // FILE STATUS FUNCTIONS
_FS_DLL _File_type __CLRCALL_PURE_OR_CDECL _Stat(const TCHAR *_Fname, _Perms *_Pmode)
{  // get file status
WIN32_FILE_ATTRIBUTE_DATA _Data;
 
if (TFUN(GetFileAttributesEx)(_Fname, GetFileExInfoStandard, &_Data))
  {  // get file type and return permissions
  if (_Pmode != 0)
    *_Pmode = _Data.dwFileAttributes & FILE_ATTRIBUTE_READONLY
      ? READONLY_PERMS : perms::all;
  return (_Map_mode(_Data.dwFileAttributes));
  }
else

読み取り専用属性が付いていたら555、そうでなければ777という実装でした。

ちなみに、Boost.Fileystemのほうはどうなっているかというと、実行ファイル・バッチファイルの場合のみ実行ビットを立てる実装となっているようです: filesystem/operations.cpp at boost-1.64.0 · boostorg/filesystem(1805行目からのシンボリックリンクの判定も、VC++と異なります)。

Windowsのファイルシステムのアクセス制御はUnix系のパーミッションとは異なる仕組みである以上、なんらか適当な数値をでっち上げるしかありません。そんなわけなので、どうなっているのか調べてみました。

この記事のカテゴリ

  • ⇒ VC++ filesystemのパーミッション取得の実装
  • ⇒ VC++ filesystemのパーミッション取得の実装

CreateProcess時、PROC_THREAD_ATTRIBUTE_MITIGATION_POLICYでプロセスに対する一部の挙動を変更することが可能です。その中にWindows 10のいつからか、DLLの検索パスについての定数が増えています。前々から気になっていたので、試してみました。

その定数は、UpdateProcThreadAttributeに記載のある以下です。

  • PROCESS_CREATION_MITIGATION_POLICY_IMAGE_LOAD_PREFER_SYSTEM32_DEFER
  • PROCESS_CREATION_MITIGATION_POLICY_IMAGE_LOAD_PREFER_SYSTEM32_ALWAYS_ON
  • PROCESS_CREATION_MITIGATION_POLICY_IMAGE_LOAD_PREFER_SYSTEM32_ALWAYS_OFF

まず、題材となるプログラムを作ります。これをコンパイルして、app.exeを作ります。iphlpapi.dllを何も考えずにLoadLibrayしているのは、もちろんわざとです。効果を確認するため、セキュリティ上問題のある書き方にしています。

// cl /MT /EHsc app.cpp /link /subsystem:console,5.01
 
#define UNICODE
#define _UNICODE
 
#include <iostream>
#include <windows.h>
#include <iphlpapi.h>
 
int main()
{
  auto hmod = LoadLibrary(L"iphlpapi.dll");
  if (auto pfn = reinterpret_cast<decltype(GetTcpTable2)*>(
    GetProcAddress(hmod, "GetTcpTable2")))
  {
    ULONG size = 0;
    pfn(nullptr, &size, 0);
    std::cout << size << std::endl;
  }
}

では、偽のiphlpapi.dllを作りましょう。次のソースコードをコンパイルします。

// cl iphlpapi.cpp user32.lib /LD
 
#define UNICODE
#define _UNICODE
#include <windows.h>
 
#pragma comment(linker, "/export:GetTcpTable2=_GetTcpTable2@12")
 
extern "C" __declspec(dllexport)
ULONG WINAPI GetTcpTable2(void*, ULONG*, BOOL)
{
  MessageBox(nullptr, L"やっほー", L"evil bit=1", MB_OK);
  return ERROR_INVALID_PARAMETER;
}

さっきのapp.exeと同じフォルダに、偽iphlpapi.dllを置き、app.exeを実行します。すると、メッセージボックスが表示されます。

さて、例のフラグ…_ALWAYS_ONを試します。CreateProcessを呼び出すlauncher.exeを作ります。

// cl /MT launcher.cpp
 
#define UNICODE
#define _UNICODE
 
#include <iostream>
#include <memory>
#include <cstdint>
#include <cstdlib>
#include <windows.h>
 
int main()
{
  WCHAR cmdLine[] = L"app.exe";
 
  SIZE_T bufSize = 0;
  if (!InitializeProcThreadAttributeList(nullptr, 1, 0, &bufSize)
    && GetLastError() != ERROR_INSUFFICIENT_BUFFER)
  {
    std::quick_exit(1);
  }
  auto buf = std::make_unique<BYTE[]>(bufSize);
  auto attrList = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(buf.get());
  if (!InitializeProcThreadAttributeList(attrList, 1, 0, &bufSize))
  {
    std::quick_exit(1);
  }
  std::uint64_t value = PROCESS_CREATION_MITIGATION_POLICY_IMAGE_LOAD_PREFER_SYSTEM32_ALWAYS_ON;
  if (!UpdateProcThreadAttribute(
    attrList, 0, PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY,
    &value, sizeof value, nullptr, nullptr))
  {
    std::quick_exit(1);
  }
  STARTUPINFOEX si{};
  si.StartupInfo.cb = sizeof si;
  si.lpAttributeList = attrList;
  PROCESS_INFORMATION pi{};
  if (CreateProcess(nullptr, cmdLine, nullptr, nullptr, FALSE,
    EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, &si.StartupInfo, &pi))
  {
    WaitForSingleObject(pi.hProcess, INFINITE);
  }
  // DeleteProcThreadAttributeListなどは省略
}

これをコンパイルして実行すると、見事メッセージボックスは表示されませんでした。

なお、定数…_DEFERを指定して、app.exe内でPreferSystem32Images = 1を指定してSetProcessMitigationPolicyを呼び出せば同様にsystem32優先になると予想していたのですが、これはうまくいきませんでした。偽iphlpapi.dllが読み込まれる結果となりました。やり方にミスがあったのでしょうか。

この方法、ご覧のとおりCreateProcessを呼び出す側での対処です。アプリ側に手を入れずに効果を発揮させることが可能で、しかもアプリ(app.exe)側のプロセス内で無効化できないはずという点で強力です。逆に、JVNTA#91240916: Windows アプリケーションによる DLL 読み込みやコマンド実行に関する問題UNLHA32.DLLで作成された自己解凍書庫における任意のDLL読み込みに関する脆弱性)への対処に使うのは大変手間がかかるでしょう。

というわけで、DLL検索パスの指定方法がまた1つ増えました。

この記事のカテゴリ

  • ⇒ System32優先でDLLを探す指定を試してみた
  • ⇒ System32優先でDLLを探す指定を試してみた

以前より、Visual C++のファイルストリーム(basic_fstream, basic_ifstream, basic_ofstream)には、独自拡張が存在しました。ファイルパスをワイド文字列で指定できる実引数のコンストラクタとopenメンバー関数の多重定義です。理由は、WindowsのAPIがUTF-16 (wchar_t)でファイルパスを扱うためです。

さて、C++17でこれが標準に取り込まれることに気付きました。こうなったのは、std::filesystem::pathの影響です。代表として、C++17のbasic_fstreamのコンストラクタを見てみます。

basic_fstream(const char*, ios_base::openmode);
basic_fstream(const string&, ios_base::openmode);
// C++17で追加。ただし、value_typeがchar以外の場合のみ。
basic_fstream(const filesystem::path::value_type*, ios_base::openmode);
// C++17で追加。
basic_fstream(const filesystem::path&, ios_base::openmode);

多重定義が2つ追加されています。同様のものが、openメンバー関数にも、basic_ifstreamやbasic_ofstreamにも、basic_filebuf::openにも追加されています。

前回(filesystem::pathの文字の型)書いたように、filesystem::path::value_typeというのはpathクラス内で用いている文字型です。すなわち、Windows環境ではwchar_tになると考えています。

するとどうでしょう。今まで独自拡張として定義されてきた多重定義が、そのままconst filesystem::path::value_type*版に当てはまっています。

というわけで、このままいけば「ワイド文字列(const wchar_t*)でファイルパスをファイルストリームの実引数に渡す」ということがVC++の独自拡張ではなくなると考えています。

#include <fstream>
#include <iostream>
#include <string>
 
int main()
{
  // あるいはL"C:\\Windows\\win.ini"でも可
  std::ifstream is(LR"(C:\Windows\win.ini)");
  std::string s;
  std::getline(is, s);
  std::cout << s << std::endl;
}

こういうわけなので、この手のコードがUnix系でも使えるようになるという話ではありません。逆に言えば、MinGW (g++, clang++)やC++ Builderなど、Windowsターゲットなら期待できるのではないでしょうかという具合です。

C++17 filesystemが実装された暁には、std::filesystem::pathクラスオブジェクトでファイルパスを扱うことをおすすめします。が、今回はpathクラスを直接使わない場合の話でした。

この記事のカテゴリ

  • ⇒ ワイド文字列でファイル名を指定する機能がC++標準に入るようだ
  • ⇒ ワイド文字列でファイル名を指定する機能がC++標準に入るようだ

次ページへ »