ま、そんなところで。

ニッチな技術系メモとか、車輪を再発明してみたりとか.

UbuntuのVSCodeでlldbを使ってRustコードをデバッグしたらブレークポイントが効かなくて困った

Rustコードのブレークポイントで止まらないんだけど・・・

UbuntuVSCodeをつかったRustの開発環境を作ってたんですが、lldbでデバッグしようとすると、どうもブレークポイントで止まらなくてハマりました.

location zero

ターミナルからlldbを直接たたいてブレーク設定しようとしても、どうもダメっぽいのですね.
ブレークポイントを追加しても位置表示が「Location: 0」となっていて、有効化できないのです.

lldbを直接起動して、listコマンドしてみてもコードが表示されず.
あー、これはどうやら完全にシンボルが見えてないご様子.

誰かが同じ問題にぶつかってやしないかな〜と、Google先生で検索を試みてもまったくそれらしい情報がないのです.
仕方ないので、諦めてしばらくの間デバッガはgdbをつかってました.

最近、思い立ってもう一度じっくり調べてみたところ、ようやく原因が判明.
lldbでデバッグできるようになるworkaroundもみつけることができたのでメモ.

環境

環境 : Ubuntu 20.04
VS-Code : 1.57.1
CodeLLDB : 1.6.5
Rust-Analyzer: v0.2.662

ソースコードをsymlinkを含むパスにおいていることが原因

どうやらソースコードがsymlinkを含むパスにあると、デバッグ時にlldbがシンボルを見つけられないようなのです.
よもやよもや、です.
実行は出来てるのに、同じファイル内にあるシンボルが見えてないとは・・・

似たような問題が3年ほど前のCodeLLDBのバグ報告で議論されてました.
mountしてる拡張ディスク上にプロジェクトを配置してた人がこの問題にぶつかっていたようです.

どうもこれ、CodeLLDBではなくlldbの問題なんだそうで.
確かに、lldb単体でも起きるのでCodeLLDBは関係ないっぽいですね.
いまでもOpen状態なところをみると、lldb側で対応してないのか根本的な解決には至ってないということでしょうね.

github.com

Workaroundみつけた!

スレッドの方にユーザ設定のsettings.jsonでlldb.launch.sourceMapを設定したら動くよ・・とあったので試してみましたが、どうもダメっぽいですね・・・

// settings.json
{
    // これはダメっぽい...
    "lldb.launch.sourceMap": {
        "/home/fuga/workspace/gitprjs": "/home/fuga/work"
    },
}

幸いこれをヒントにworkaroundをみつけることができました.
launch.jsonデバッグ設定にsourceMapエレメントを定義してlldbにパスの対応を教えてやれば、この問題を回避できるようです.

// launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "lldb",
            "request": "launch",
            "name": "Debug lldb executable 'g_game'",
            "cargo": {
                "args": [
                    "build",
                    "--bin=g_game",
                    "--package=g_game"
                ],
                "filter": {
                    "name": "g_game",
                    "kind": "bin"
                }
            },
            "args": [],
            "cwd": "${workspaceFolder}",
            "stopOnEntry": false,
            "sourceLanguages": ["rust"],
            // *** ここを追加 ***
            "sourceMap": {
                // 以下のsymlinkの場合.
                // work -> /home/fuga/workspace/gitprjs
                // real path -> symlink path の対応をそれぞれ絶対パスで定義してやる.
                "/home/fuga/workspace/gitprjs": "/home/fuga/work"
            }
        }
    ]
}

Linux環境で(Macもかな?)作業ディレクトリパスにsymlinkを使っていないと出くわすことのない問題なので、誰も気づかなかったのかもしれませんね.
ともかく、gdbとくらべて表示される情報量が多いlldbでデバッグできるようになったので、まずは良しとします.

UbuntuでPPTPクライアントをセットアップする

とあるサーバーに固定IPで接続する必要がありまして、VPN接続を設定したのでメモ.

今回はInterlinkのMyIPというサービスを使いました.
なかなか固定IPのサービスは高価なので、安価に固定IPを使用したいときは良いかもですね.

接続はWindowsMac, RedHatLinuxなら公式ページに接続方法が紹介されていますが、 DebianLinuxの設定例はありませんでした.

Ubuntu では NetworkManager からの設定でPPTPの設定ができないため、設定ファイルとスクリプトを駆使してPPTP接続を行う必要があります.
公式ページの手順(RHEL/CentOS系)とはずいぶん異なる感じなので注意.
(※ 16.04, 18.04, 20.04 の 各LTSバージョンで動作することを確認しました.)

1.PPTPの接続設定

Step1. PPTPクライアントをインストールする

まずはPPTPクライアントをインストールします.
これでpppなど依存パッケージもまとめてインストールされます.

$ sudo apt install pptp-linux

Step2. PPTP設定ファイルを作る

PPTP接続のための設定ファイルを配置します.
下記のコマンドを実行すると、設定ファイルの雛形を作って配置することができます.

$ sudo pptpsetup --create {接続名} --server {PPTPサーバのホスト名orIPアドレス} --username {ユーザ名} --password {パスワード} --encrypt

コマンド実行するとpeers以下に設定ファイルが作られ、chap-secrets が更新されます.

(1) peers設定ファイル

このファイルは内容だけを確認すればOKです.
特に修正する必要はありません.

/etc/ppp/peers/{接続名}

# written by pptpsetup
pty "pptp {サーバホスト名orIPアドレス} --nolaunchpppd"
lock
noauth
nobsdcomp
nodeflate
name {ユーザ名}
remotename {接続名}
ipparam {接続名}
require-mppe-128

(2) chap-secrets の編集

次に、/etc/ppp/chap-secrets を編集します.
ファイルに接続先とユーザ名/Passwordと割当IPの設定が追記されているので、こちらを編集します.

pptpsetup が生成した設定ではPPTP接続時の割当IPアドレスの部分が * になっていますが、 固定IPが割り当てられる場合は、ここに割り当てられているIPを入力します.

# Secrets for authentication using CHAP
# client server secret IP addresses
# added by pptpsetup for HogeHoge
{ユーザ名} {接続名} {パスワード} {PPTP接続時の割当IPアドレス}

Step3. ルーティングテーブルの書き換えスクリプトの準備

接続時に使用するルーティング変更処理を接続時スクリプト(ip-up, ip-down)として配置します.
内容は以下のような感じです.

(1) ip-up.local

/etc/ppp/ip-up.local (※なければ作る. filemode=0700)

#!/bin/sh
# 全ての接続をVPN経由にする場合はこちら
route add default gw {PPTP接続時の割当IPアドレス}

# 特定ホストのみをVPN経由でアクセスしたいような場合は下記の設定を追加.
# route add -net {VPN経由で接続するホスト名orIPアドレス} netmask 255.255.255.255 gw {PPTP接続時の割当IPアドレス}
exit 0

(2) ip-down.local

/etc/ppp/ip-down.local  (※なければ作る. filemode=0700)

#!/bin/sh
route del default gw {PPTP接続時の割当IPアドレス}

# 特定ホストのみVPN経由アクセスとしていた設定を削除
# route del -net {VPN経由で接続するホスト名orIPアドレス} netmask 255.255.255.255 gw {PPTP接続時の割当IPアドレス}
exit 0

Step4. 接続/切断スクリプトの準備

PPTPの接続と切断を行うスクリプトを作ります.
これはユーザが直接叩くので、作成・配置場所はお好みの場所でOKです.

(1) 接続スクリプト

ここではファイル名を startpptp.shとしました (※ filemode=0755)

#!/bin/sh
echo "Starging PPTP to {接続名}"
sudo pppd call {接続名} updetach
# デフォルトのルーティングを削除
sudo route del default gw {ルータのホスト名orIPアドレス}
echo "Connected!!"
exit 0

(2) 切断スクリプト

ここではファイル名を stoppptp.shとしました (※ filemode=0755)

#!/bin/sh
echo "Stopping PPTP to Interlink(XXXXXXXX)"
# pppdの停止
sudo pkill -TERM pppd
# デフォルトルーティングの復元
sudo route add default gw {ルータのIP}
# pppdによって追加されたPPTPサーバ接続のルーティングを削除
sudo route del -net {VPNサーバのホスト名orIPアドレス} netmask 255.255.255.255 gw {ルータのホスト名orIPアドレス}
echo "Disconnected."
exit 0

2.接続と切断

2-1. PPTPで接続する

$ ./startpptp.sh
f:id:zervoid:20200506090202p:plain

ifconfig コマンドで ネットワークアドレスを確認します.
ppp0が追加されていれば接続成功です.

f:id:zervoid:20200506085159p:plain

2-2. PPTPを切断する

$ ./stoppptp.sh

3. その他

起動時に自動的に接続するとかの場合は、NetworkManagerではなく、networkのinterface設定でpre-up/post-downなどを使うと行うと良いらしいです.

参考サイト

Windows10へUpgradeしたら遅くなった問題の対処メモ

Windows10へUpgradeしたら遅くなった問題の対処メモ

ずいぶん前にWindows7→Windows10にUpgradeしたところ、かなりハイスペックなマシンだったのですが、あまりに遅くなりすぎて使わなくなっていました.

ところが、ドライバをUpdateするとストレス無くPCが使えるまでに劇的に速度向上させることができたのでメモ.

1. 症状

1-1. ディスクアクセス平均速度が遅い

Windows10へのUpdate後、とにかく重い....
こんな症状で使い物にならなくなっていました.

  • ディスクアクセスが常に100%でなかなか下がらない.
  • 起動に数分〜10分程度かかる.
  • ディスクの平均応答時間が100ms〜2000msなどおおよそいつも100ms前後.

この症状に心当たりがある場合、Intel Rapid Storage Tecnology Driver(IntelRST)が古すぎてMS製の汎用ドライバが使われてしまっていることが原因かもしれません.

2. 解決方法

2-1. Intel Rapid Storage Tecnology Driver(Intel RST)のUpdate

Intel® Rapid Storage Technology User Interface and Driver

から新しいDriverをDownloadしてインストール.
インストール時にデバイスをチェックしてくれるので、適合するかどうかはインストーラ任せで良さそうです.

インストール後、ディスク平均応答時間が数ms〜数十msと劇的な改善.
ディスクアクセスが100%に張り付くこともなくなり、ずいぶんとレスポンスが良くなりました.

ちなみに、このIntelRSTのUpdateは通常のWindowsUpdateでは行われないので注意.

気づかないと、ずっと遅いまま使い続けることになりそうです.

debとrpmパッケージの命名規則

パッケージ命名規則

Linuxのパッケージを作ることになったので調査したメモです.

debパッケージの命名規則

${PACKAGE_NAME}_${VER_MAJ}.${VER_MIN}.${VER_BUILD}(-${VER_RELEASE}(${VER_RELEASE_SUFFIX}))_${ARCH_NAME}.deb

各要素は'_' (アンダーバー)を使用する.

  • PACKAGE_NAME --- パッケージ名
  • VER_MAJ --- メジャーバージョン
  • VER_MIN --- マイナーバージョン
  • VER_BUILD --- ビルドバージョン
  • VER_RELEASE --- リリース番号
  • VER_RELEASE_SUFFIX
    リビジョン修飾文字列.
    deb/rpm共通して文字列として使用できるのは、アルファベット小文字, 数字, '~'(チルダ) '+'(プラス).
    リビジョン番号の前後関係にも影響する.
  • ARCH_NAME --- アーキテクチャ (amd64, x86, etc..)

debパッケージのバージョン比較テスト

バージョン比較テストは dpkg --compare-versions を使用する.
演算子は 'lt'(<) 'gt'(>), 'le'(<=) 'ge'(>=)', 'eq'(==), 'ne'(!=)

# リリース番号ありを後発と判定
$ if $(dpkg --compare-versions "1.3.0" "lt" "1.3.0-1"); then echo true; else echo false; fi
true
# '~'はpreバージョンとして認識される. 
# '+'は数字順
$ if $(dpkg --compare-versions "1.3.0-2~1" "lt" "1.3.0-2"); then echo true; else echo false; fi
true
$ if $(dpkg --compare-versions "1.3.0-3+1" "lt" "1.3.0-3"); then echo true; else echo false; fi
false
# ひとつ前のリリースより後発リリースのpreバージョンのほうが新しい
$ if $(dpkg --compare-versions "1.3.0-3~rc1" "lt" "1.3.0-2"); then echo true; else echo false; fi
false

rpmパッケージの命名規則

${PACKAGE_NAME}-${VER_MAJ}.${VER_MIN}.${VER_BUILD}(-${VER_RELEASE}(${VER_RELEASE_SUFFIX})).${DIST_NAME}.${ARCH_NAME}.rpm

こちらは'-'(ハイフン)を使って要素を接続.

  • PACKAGE_NAME --- パッケージ名
  • VER_MAJ --- メジャーバージョン
  • VER_MIN --- マイナーバージョン
  • VER_BUILD --- ビルドバージョン
  • VER_RELEASE --- リリース番号
  • VER_RELEASE_SUFFIX
    リビジョン修飾文字列.
    deb/rpm共通して文字列として使用できるのは、アルファベット小文字, 数字, '~'(チルダ) '+'(プラス).
    リビジョン番号の前後関係にも影響する.
  • DIST_NAME
    パッケージの対象ディストリビューション.
    (ex. 'el7' -> RHEL7, 'el7_9' -> RHEL7〜9, 'centos' -> centos, etc..)
  • ARCH_NAME --- アーキテクチャ
    (x86, x86_64, noarch, etc..)

rpmパッケージのバージョン比較テスト

rpmdev-vercmp コマンドを使う.
rpmdevtools パッケージをインストールすると使える.

# インストール
$ sudo yum install -y rpmdevtools

バージョン比較テスト

$ rpmdev-vercmp mypkg-2.2.1-3 mypkg-2.2.1-4
mypkg-2.2.1-3 < mypkg-2.2.1-4
$ rpmdev-vercmp mypkg-1.2.0 mypkg-1.2.0-1
mypkg-1.2.0 < mypkg-1.2.0-1
$ rpmdev-vercmp mypkg-3.2.1-3 mypkg-3.2.0-4
mypkg-3.2.1-3 > mypkg-3.2.0-4
# (注意) yumは'~'をpreバージョンとしては扱わないようです.
$ rpmdev-vercmp mypkg-2.2.0-1~rc1 mypkg-2.2.0-1
mypkg-2.2.0-1~rc1 < mypkg-2.2.0-1

参考

Ubuntu18.04にpyenvでpython3.7.3をインストール時にBUILD FAILED

対処メモです.

pyenv install 3.7.3 でBUILD ERROR

環境は以下の通り.

  • Ubuntu 18.04.2 LTS (Bionic Beaver)
  • pyenv 1.2.11

pyenvはpythonのソースパッケージをDownloadしてきて、ビルドしてインストールしているんですね.
こんな感じでエラーが出ました.
install BUILD ERROR

エラーメッセージを見ると、いずれもpythonが参照しているネイティブのライブラリが見つからないことが原因のようです.

ならば、依存関係を満たしてやればうまく行きそうです.

行った対処

メッセージ中に見られたライブラリ名を手がかりに、以下のライブラリの開発パッケージをインストールしました.
(※ 当然ながら下記のリストは完全でないかもしれませんが... )

  • zlib (zlib1g-dev)
  • bzip2 (libbz2-dev)
  • openssl (libssl-dev)
  • libffi (libffi-dev)
  • readline (libreadline-dev)
  • sqlite3 (libsqlite3-dev)
$ sudo apt install -y libffi-dev libssl-dev libbz2-dev zlib1g-dev libreadline-dev libsqlite3-dev

パッケージインストール後に再度pyenvで3.7.3をインストールしたところ、無事インストール成功しました.


参考サイト

pyenvで3.7系のインストールに失敗したときのメモ - Qiita
pyenv でBUILD FAILED した時の対処法 - Qiita
3.7.3 fails to install · Issue #1319 · pyenv/pyenv · GitHub

(続)Unmanagedの C++ から Managedのクラスを使う

の続き.

ManagedクラスとUnmanagedクラスの生存期間差の問題

Unmanaged C++からManagedクラスを使用する場合、ただManagedクラスでUnmanaged C++クラスをWrapしてやるだけではダメなケースがあります.
Unmanaged クラスのポインタをVisitorやObserverパターンで使うケースです.

// 
// SomeListenerのManagedWrapper
//
ref class ListenerWrap {
public:
    // コンストラクタ
    ListenerWrap(SomeListener* cb)
    : cb_(cb)
    {

    }
    // デストラクタ
    ~ListenerWrap()
    {
        // ファイナライザへ委譲
        // C++CLIの場合, GC::SuppressFinalize(this)は無条件で行われるので呼び出しは不要.
        this->!ListenerWrap();
    }
    // ファイナライザ
    !ListenerWrap()
    {
        // ポインタを使った終了処理など.
        if (cb_) {
            cb_->close();  // Unmanaged オブジェクトの cb_ の後始末(★ ここが問題)
            cb_ = nullptr;   // ListenerWrapとSomeListenerポインタの関連付けを解除.
        }
    }

    virtual void Exec(int param);
protected:
    SomeListener* cb_;
};
// 問題になるコード・・・
void SomeClass::DoWithCallBack(int param, SomeListener* cb)
{
    // 未定義動作の危険!!
    // cbWrapはスコープアウトによってgc対象になるだけ. ファイナライザ実行まで行われるとは限らない.
    // GCによってcbWrapがFinalizeされるより前に cb が無効ポインタになってしまう可能性がある.

    ListenerWrap^ cbWrap = gcnew ListenerWrap(cb);
    someManagedObject_->Exec(param, cbWrap);
}

Unmanagedクラスのローカル変数は スコープアウトで削除 されますが、Managedクラスのハンドルは スコープアウトでGC対象になるだけ です.

分かりやすい例だと下記のようなコードです.

void DoSomethingClearSample()
{
    // localCbのほうが先に無効になる.
    // cbWrapのファイナライザはlocalCbが削除された後.
    SomeListener localCb;
    ListenerWrap^ cbWrap = gcnew ListenerWrap(&localCb);
             :
             :
}

この生存期間の僅かなギャップにより、Unmanagedクラスのポインタを保持するManagedクラスがFinalizerでUnmanagedクラスのポインタを使って何らかの終了処理を行うような場合に無効ポインタへのアクセスを引き起こすことになります.

C#のusingステートメントC++標準ライブラリのstd::unique_ptrのように、スコープアウトしたときに確実にDisposeしたりdeleteしたりする仕組みが欲しいなぁ、と思っていたところ、ちゃんと用意されてました.

こういうむず痒いところもさりげなーく手当てしてるのがすごい.
さすがMS.

msclr::auto_gcroot<T>テンプレートクラス

前の記事 の gcroot<T> は、UnmanagedクラスがManagedクラスのハンドルを保持するための補助的Unmanagedテンプレートクラスでした.

こちらはその機能を発展したものになります.
Unmanagedクラスのメンバに保持できるのは同じですが、こちらは デストラクタで保持しているManagedクラスのハンドルをdeleteしてくれます.

マネージドクラスのハンドルを delete するということは、IDisposable の Dispose メソッド呼び出しと同じですから、これを使うと Unmanagedのオブジェクトがdeleteされるタイミングで保持しているManagedのハンドルもDisposeさせる仕組みができます.

メンバメソッドも標準ライブラリのスマートポインタ std::unique_ptr<T> とほぼ同じで、C++の標準ライブラリと同じ感覚で使えるように設計されてました.
これは嬉しい.

// auto_gcrootヘッダをincludeします.
#include <msclr/auto_gcroot.h> 
             
SomeWrap^ someWrap = gcnew SomeWrap();
if (someWrap)
{
    // someWrapはスコープアウトでdispose.
    msclr::auto_gcroot<SomeWrap^> wrapped = someWrap;
    SomeWrap^ rawHandle = wrapped.get(); // rawハンドルの取得
               :
    wrapped->SomeFunc(); // -> でメソッド呼び出し
               :
    someWrap = nullptr;
    // someWrap.reset(other); // resetも同じ
    // someWrap.release(); // releaseもあります!
}

Managedクラスハンドルの生存期間をスコープで管理できるようになれば、生存期間のズレによる問題は容易に解決できますね.

void SomeClass::DoWithCallBack(int param, SomeListener* cb)
{
    // auto_gcrootで、スコープアウトのタイミングで確実にdelete(=Dispose)する.
    // このスコープ内ならcbが有効であることは確実なので、安全にcbの終了処理とnullptr化が行える.
    msclr::auto_gcroot<ListenerWrap^> cbWrap = gcnew ListenerWrap(cb);
    someManagedObject_->Exec(param, cbWrap);
}

これでcbポインタが無効になってからfinalizerが呼ばれることはなくなりました.

スコープ利用に特化した msclr::auto_handle<T> テンプレート

auto_gcroot<T> と似たようなクラスで auto_handle<T> ってのもあります.

これは、auto_gcroot<T>のManagedクラス版.

auto_handle<T> はManagedな参照(ref)型テンプレートクラスなので、Unmanagedクラスのメンバとして保持できないという違いがあるものの、機能としてはauto_gcroot<T> とほぼ同等です.

#include <msclr/auto_gcroot.h> 
#include <msclr/auto_handle.h> // auto_handle.hが別にあります.
             
{
    msclr::auto_gcroot<SomeWrap^>  wrapped1 = gcnew SomeWrap();
    // auto_handleにはハンドルのoperator=がないので、ハンドルのsetはコンストラクタかresetを利用する.
    msclr::auto_handle<SomeWrap> wrapped2( gcnew SomeWrap() ); // 割安
               :
               :
}

auto_gcrootがUnmanagedクラスのメンバとして保持することを想定したものであるのに対して、こちらはスコープ利用を想定したものといえそうです.
ちょうど、C++CLIC#のusingステートメント相当の処理を実現するためのもの 、といった位置づけでしょうか.

スコープ利用に限っては auto_gcroot<T> も auto_handle<T> と同じ作用をするのですが、内部処理としてGCHandle型のAllocとFreeが行われない分だけ auto_handle<T> のほうが処理コスト的に割安な感じです.

ということで、先の例は auto_handle のほうが最適な選択と言えますね.
書き換えると以下のような感じになりました.

void SomeClass::DoWithCallBack(int param, SomeListener* cb)
{
    // auto_handleで、スコープアウトのタイミングで確実にdelete(=Dispose)する.
    msclr::auto_handle<ListenerWrap> cbWrap;
    cbWrap.reset( gcnew ListenerWrap(cb) );
    someManagedObject_->Exec(param, cbWrap);
}

auto_gcroot も auto_handle も地味に便利なんですが、あまり使用例は見かけないですね・・.
ヘッダのコードを見るとそんな難しいものでもなさそうなので、大体の人は同等のクラスを自作しちゃってるのかもしれません.


参考情報

C++ Support Library | Microsoft Docs
auto_gcroot Class | Microsoft Docs
auto_handle | Microsoft Docs
unmanagedクラスにmanagedなメンバを持たせる - schima.hatenablog.com

Unmanagedの C++ から Managedのクラスを使う

Pure C++コードから、.NET Frameworkベースのライブラリを使いたい・・・

純粋なC++コード(Unmanagedコード)から、C#など.NET Frameworkベースで開発されたライブラリ(Managedクラス)を利用しなければならなくなった・・
それほど頻繁に遭遇するケースではありませんが、こんな課題に出くわすことがあります.

基本的な方法としては、共通言語ランタイムをつかって(/clrオプション)C++CLIにすれば丸く解決・・・のはずですが、これが許されないケースがあります.

例えば、Native部分のコードに標準ライブラリのmutexとかatomicとか使ってる場合.
これは共通言語ランタイムと互換性がないので、C++CLIにしてコードを共存させることができません.
(共存させようとするとコンパイラに叱られますね.)

こうなると、C++CLIにできる部分と出来ない部分はモジュールを分割し、ManagedクラスのインスタンスをラップしたクラスをUnmanaged C++から使ってやるしかないわけですね.

さて、ここで一つ問題が起こります。

error C3265: マネージ 'hoge' をアンマネージ 'fuga' で宣言できません.

おっと、UnmanagedのクラスはManagedクラスのハンドルを持てないのでした・・・

UnmanagedクラスにManagedクラスのハンドル型を持たせられる?

うーんと悩みながらドキュメントを漁ると・・msclr名前空間にとても便利なクラスがありました.

msclr::gcroot<T> を使え!

今回キモとなるのは gcroot テンプレート.
msclr/gcroot.h をインクルードして使用します.

このクラスはC++のテンプレートになっていて、Managedの参照型(ref class)をUnmanagedクラスに保持させることができるように設計されています.
Unmanagedクラスのメソッドでは、gcrootでラッピングしたハンドルからメソッドを呼び出します.
親切なことに operator ->()とoperator T()が準備されてますので、普通のハンドルとそれほど変わらない使い方が可能です.

コレは便利!!

じゃあ、value class の場合はどうするの・・?

当然の疑問ですよね.

サンプルでは全然見かけないのですが、実は value class も同じように保持できます.

これは、Managedの値型に AutoBoxingが働いて value class は自動的にref classのハンドルでWrapする処理が入るため.
下記のコードではvalue classのSystem::DateTimeを保持しています.

// System::DateTime は value class .
// ハンドル型へはAutoBoxingされる
DateTime^ dth = DateTime.Now;
// DateTime^にAutoBoxingされるので, gcrootへも直に格納できる. 
msclr::gcroot<DateTime^> dtwrap = DateTime.Now;

// ハンドル型はそのまま取得( operetor T() )
DateTime^ dth = dtwrap;
// unboxは明示キャストが必要
DateTime dtv   = static_cast<DateTime>(dth);
DateTime dtv2 = static_cast<DateTime>(dtwrap);
 

unboxは上記のように明示的にキャストすればOKです.
ただし、Unbox処理はそこそこ負荷がかかりますので注意.
なるべくBoxされたハンドルのままで取り扱うのが良さそうです.

さて、これで準備は整いました.

さあ、gcrootを使ってWrapperを作ろう.

Wrapperの実装

見せたくないものを隠すのは モザイク Wrapperが定石ですね.

Pure C++からはManagedクラスが見えてはダメなので基本的に抽象化を使います.
class の内部メンバをNativeC++モジュールから完全に隠すため、ここでは pure virtual クラスを使います.
pure virtualな型のインスタンスをインタフェースオブジェクトとしてモジュールの外のコードに提供します.

ヒープ境界の問題があるので、モジュール外部(Native C++モジュール)のランタイム側のdelete/freeがこのインタフェースに対して使われないように少し細工が必要です.
ここでは デストラクタをprotectedにしてpure virtualなインスタンスをdelete出来ないようにし、deleteの役割は仮想関数の実装側のDestroyで行います.
こうするとDestroyメソッドの実装本体がC++CLI側モジュール内にあるので、必然的にCLIモジュール側のランタイムによるdeleteを強制することができます.

//
// interface.h
// 公開用ヘッダファイル
//

// モジュールの外から使うためのインタフェースクラス
class CliDateTimeIF {
protected:
  virtual ~CliDateTimeIF() {} // 直delete禁止
public:
  virtual void Destroy() = 0; // 代理デストラクタ. deleteを行う.
  virtual int GetYear() = 0;
  virtual int GetMonth() = 0;
  virtual int GetDay() = 0;
};

そして、インタフェースの実装クラスです.

#include <msclr/gcroot.h>

using namespace System;

// ------------------------------------------------------
// ManagedクラスをUnmanaged C++でラップする
class CliDateTimeImpl : public CliDateTimeIF {
protected:
  virtual ~CliDateTimeImpl()
  {
       // 後始末
  }

public:
  CliDateTimeImpl() noexcept { }

  virtual void Destroy() override {
      delete this; // self-delete
  }

  virtual int GetYear() {
      return dt_->Year;    
  }
  virtual int GetMonth() {
      return dt_->Month;   
  }
  virtual int GetDay() {
      return dt_->Day; 
  }

  void SetDateTime(DateTime^ dt) {
      dt_ = dt;
  }

  DateTime^ GetDateTime() {
      return dt_; // gcroot<T>::operator T()
  }

protected:
  msclr::gcroot<DateTime^> dt_; // ハンドル型
};

モジュールの外から使えるようにする

最後にモジュール外に公開するファクトリ関数です.
公開ヘッダファイルにファクトリ関数の定義を追加します.

//
// interface.h
// 公開用ヘッダファイル
//

// モジュールの外から使うためのインタフェースクラス
class CliDateTimeIF {
protected:
  virtual ~CliDateTimeIF() {} // 直delete禁止
public:
  virtual void Destroy() = 0; // 代理デストラクタ. deleteを行う.
  virtual int GetYear() = 0;
  virtual int GetMonth() = 0;
  virtual int GetDay() = 0;
};

extern "C" {

// -------------------------------------------------------------------
// 公開ファクトリ関数
__declspec(dllexport) 
CliDateTimeIF* __cdecl CreateCLIDateTime();

};

Load Time Link(libファイルを使ったリンク)でもDynamic Link(LoadLibraryを使うリンク)でも使いやすいように __cdecl 規約の C関数をExportします.
この関数の戻値に pure virtual のポインタを返します.

extern "C" {
// ---------------------------------------
// 公開ファクトリ実装
CliDateTimeIF* __cdecl CreateCLIDateTime()
{
    CliDateTimeIF* ptr = nullptr;
    try {
        ptr = new CliDateTimeImpl();
        ptr->SetDateTime(DateTime.Now);
    }
    catch (std::bad_alloc&) {
         // alloc error!!
    }
    return ptr;
}

};

呼び出し側のコードでは、ファクトリ関数を使ってインスタンスを生成して使うだけです.

CliDateTimeIF* pdt = CreateCLIDateTime();
       :
int year = pdt->GetYear();
       :
pdt->Destroy(); // delete
pdt = nullptr;

参考記事