ネイティブC++(MFC)からC#へJSONでデータ受け渡しする方法|Windows.Data.Json(C++/WinRT)で安全にシリアライズ

PLC から受け取ったデータをネイティブ C++(MFC/アンマネージ)から C# に渡すなら、「文字列 1 本」に JSON を載せる設計がシンプルで堅牢です。本記事では、C++ 側に C# のような自動シリアライズがない理由から、Windows 公式の WinRT JSON(Windows.Data.Json)で安全に JSON を組み立てる方法、CString/hstring 変換、C# 側でのデシリアライズまでを実装目線で整理します。

目次

今回の要件を整理(なぜ JSON が有効か)

前提として、次のような状況を想定します。

  • PLC からデータを受け取るのは ネイティブ C++(MFC、アンマネージコード)
  • C# 側へは マーシャリングで「文字列 1 本」のみ渡せる(DLL 境界、IPC、名前付きパイプ等でも同じ発想)
  • C++ のデータ構造(例:MachineData)を C# の DTO に復元したい
  • C++/CLI は使えない(アンマネージ必須)

この条件だと、独自の区切り文字列(例:X,Speed,MachineId,...)を自作するより、JSON のほうが次の点で優位です。

  • 構造が明確(配列、ネスト、型が表現しやすい)
  • 拡張に強い(フィールド追加しても後方互換を作りやすい)
  • C# 側の復元が簡単(System.Text.Json / Newtonsoft.Json でそのまま DTO に落とせる)
  • デバッグしやすい(ログに残して可視化できる)

結論:ネイティブ C++ に「クラス丸ごと自動 JSON 化」は標準ではない

質問の核心である「C++ に C# の MyObject.Serialize() のような自動 JSON シリアライズ用ヘルパークラスはあるか?」について、まず結論です。

  • 標準のネイティブ C++ だけでは “C# みたいに” 自動シリアライズはできません
  • 理由は、C# のリフレクション(型情報の列挙、属性、プロパティ取得)が、ネイティブ C++ には標準搭載されていないためです

ただし「完全に手書きしかない」という意味ではなく、C++ でも次のようなアプローチは可能です。

  • マッピングを明示して JSON を作る(本記事の推奨)
  • サードパーティの JSON ライブラリ+マクロ定義で “半自動化” する(後述)
  • 設計上、C++ 側を DTO 専用構造に寄せてシリアライズを単純化する(運用の工夫)

JSON 生成の選択肢(Windows 公式か、軽量ライブラリか)

「アンマネージ必須」「できれば依存を増やしたくない」「MFC でも使いたい」という条件で現実的な選択肢を比較すると、次のようになります。

選択肢特徴向いているケース注意点
Windows.Data.Json(C++/WinRT)Windows 公式の WinRT JSON。オブジェクト/配列を API で組み立てるWindows 専用、依存を増やしたくない、保守性重視WinRT 初期化(apartment)が必要。数値は double 扱いが中心
nlohmann/jsonヘッダ 1 枚で導入しやすい。書き味が良いWindows に限らず使いたい、記述量を減らしたいサードパーティ依存。MFC 型(CString 等)は変換が必要
RapidJSON高速・低メモリ。ストリーミング書き込みが得意高頻度(ミリ秒単位)更新や大容量 JSON が必要API はやや低レベル。実装量は増えやすい
独自フォーマットカンマ区切り等で自作絶対に最小サイズが必要、仕様が固定拡張に弱い、エスケープ地獄になりがち、保守コスト増

本記事では、質問文の方針に合わせて Windows.Data.Json(C++/WinRT) を軸に解説し、必要に応じて代替案も提示します。

推奨方針:C++ 側は「手動マッピング+公式 JSON」で堅牢にする

ネイティブ C++ では「クラス丸ごと自動変換」は難しいので、実務では次のやり方が安定します。

  • C++ 側で JSON のキー名(契約)を決める
  • フィールドごとに JSON に詰める(手動でも 1 箇所にまとめれば保守できる)
  • C# 側は DTO にデシリアライズして型安全に扱う

このとき重要なのは、単に JSON を作るだけではなく、将来の変更に耐える契約設計にしておくことです。例えば次のような工夫が効きます。

  • JSON に SchemaVersion を入れる(後方互換の判断材料にする)
  • 配列やインデックスの整合性(サイズ一致)を C++ 側で保証する
  • 数値の精度(特に 64bit カウンタ)を JSON に落とすルールを決める

C++/WinRT(Windows.Data.Json)を MFC で使うための考え方

Windows.Data.Json は WinRT の API なので、C++ 側は C++/WinRT(WinRT を標準 C++ から呼ぶための投影)で利用します。C++/CLI ではないため、アンマネージ必須の制約にも適合します。

実装上のポイントは次の 2 つです。

  • WinRT を使うスレッドで apartment を初期化する(例:winrt::init_apartment())
  • JSON は JsonObject / JsonArray を API で組み立て、最後に Stringify() で文字列化する

MFC アプリなら、アプリ起動時(例:CWinApp::InitInstance())に 1 回だけ初期化しておくと運用が楽です。

MFC 起動時に apartment を初期化する例

#include <winrt/Windows.Foundation.h>

BOOL CMyApp::InitInstance()
{
// 既に COM 初期化済みでも問題が起きにくい形で、WinRT の初期化を行う
winrt::init_apartment(winrt::apartment_type::single_threaded);


return CWinApp::InitInstance();


}

もしワーカースレッドで JSON を作るなら、そのスレッド側でも init が必要です(「WinRT を使うスレッドで初期化」が基本ルールです)。

MachineData を JSON にする実装(Windows.Data.Json)

質問にあるような MFC 型を含むクラスを JSON にする場合、最終的には「各フィールドを JSON に押し込む」だけです。ポイントは、型の対応と文字列変換です。

型対応の目安(C++ → JSON → C#)

C++ 側の型JSON 表現C# 側の型例補足
intnumberintWindows.Data.Json は number を double として扱うため、巨大整数は注意
doublenumberdouble小数点は JSON 側で標準化される(ロケール依存にしにくい)
CStringstringstringUnicode(UTF-16)で渡すと Windows では扱いやすい
CStringArrayarray(string)List<string>要素ごとに Append
CIntArrayarray(number)List<int>巨大整数は string 化する方針も検討

MachineData を JSON 文字列にする関数例

#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Data.Json.h>

using namespace winrt;
using namespace Windows::Data::Json;

class MachineData
{
public:
int X;
double Speed;
CString MachineId;
CStringArray ArrayData;
CIntArray   ArrayDataIndex;
};

static hstring SerializeMachineDataToJson(const MachineData& src)
{
JsonObject obj;


// バージョニング(将来の拡張に備える)
obj.Insert(L"SchemaVersion", JsonValue::CreateNumberValue(1));

// 数値
obj.Insert(L"X",     JsonValue::CreateNumberValue(static_cast<double>(src.X)));
obj.Insert(L"Speed", JsonValue::CreateNumberValue(src.Speed));

// 文字列(UNICODE ビルド前提:CString は UTF-16)
obj.Insert(L"MachineId", JsonValue::CreateStringValue(hstring(src.MachineId.GetString())));

// 配列(string)
JsonArray arrData;
const INT_PTR dataCount = src.ArrayData.GetSize();
for (INT_PTR i = 0; i < dataCount; ++i)
{
    const CString& s = src.ArrayData.GetAt(static_cast<int>(i));
    arrData.Append(JsonValue::CreateStringValue(hstring(s.GetString())));
}
obj.Insert(L"ArrayData", arrData);

// 配列(number)
JsonArray arrIndex;
const INT_PTR idxCount = src.ArrayDataIndex.GetSize();
for (INT_PTR i = 0; i < idxCount; ++i)
{
    const int v = src.ArrayDataIndex.GetAt(static_cast<int>(i));
    arrIndex.Append(JsonValue::CreateNumberValue(static_cast<double>(v)));
}
obj.Insert(L"ArrayDataIndex", arrIndex);

return obj.Stringify();


}

この関数を呼ぶ前提として、どこかで winrt::init_apartment() が実行されている必要があります(先ほどの InitInstance など)。

生成される JSON の例

{
  "SchemaVersion": 1,
  "X": 120,
  "Speed": 1.25,
  "MachineId": "MC-01",
  "ArrayData": ["one","two","three"],
  "ArrayDataIndex": [0,2,5]
}

CString と hstring の変換(UNICODE / ANSI で考え方が変わる)

MFC ではビルド設定(UNICODE の有無)で CString の実体が変わります。ここを曖昧にすると、文字化けやデータ欠損の温床になります。

ビルドCString の実体推奨方針理由
UNICODECStringW(UTF-16)UTF-16 のまま渡すWindows と .NET の string は内部的に UTF-16。最短で安全
ANSICStringA(コードページ依存)可能なら UNICODE 化、難しければ UTF-8 へ統一コードページは環境差が出やすく、PLC 名称等で事故が起きやすい

UNICODE ビルドでの相互変換例

// hstring -> CString(UTF-16)
winrt::hstring jsonH = SerializeMachineDataToJson(data);
CString jsonC = jsonH.c_str();

// CString -> hstring(UTF-16)
CString machineId = L"MC-01";
winrt::hstring mid = winrt::hstring(machineId.GetString());

ANSI ビルドでの注意点(どうしても残る場合)

ANSI の CString は「システムコードページ(ACP)」に依存します。JSON にして C# に渡すなら、実務では次のいずれかに寄せるのが安全です。

  • (推奨)プロジェクトを UNICODE 化して、UTF-16 で完結させる
  • UTF-8 に統一し、C# 側は UTF-8 として受け取る(.NET の Marshal.PtrToStringUTF8 等を使う)

ANSI のまま “なんとなく CharSet.Ansi” で渡すと、マシン環境が変わった瞬間に再現しない不具合になります。PLC 名称や機種 ID に日本語や記号が混じる現場ほど、UNICODE 化の投資は回収できます。

「文字列 1 本」のマーシャリング設計を事故らせないコツ

JSON 化ができても、DLL 境界での受け渡しに失敗するとメモリリークやクラッシュになります。ここでは、C++ ⇔ C# の「文字列 1 本」受け渡しでよくある方式を整理します。

方式C++ 側C# 側メリット注意点
BSTRSysAllocString で確保戻り値 string に直マーシャリング可扱いやすい、UTF-16COM 系の流儀。BSTR 解放規約を守る
CoTaskMemAllocCoTaskMemAlloc で確保Marshal.PtrToStringUni + FreeCoTaskMem規約が明確解放関数のペアリング必須
呼び出し側バッファバッファに書き込みStringBuilder 等で受ける確保/解放が不要サイズ見積りが難しい(溢れ対策が必要)

運用で一番事故が少ないのは「確保した側が解放規約も提供する」設計です。例えば、C++ 側がメモリ確保するなら、C++ 側に Free 関数も用意して C# から呼ぶ形にします。

BSTR で JSON を返す例(分かりやすい運用)

#include <Windows.h>
#include <OleAuto.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Data.Json.h>

extern "C" __declspec(dllexport) BSTR __stdcall GetMachineDataJsonBstr()
{
try
{
// WinRT 初期化(未初期化ならここで。既に初期化済みでもOK)
winrt::init_apartment(winrt::apartment_type::single_threaded);


    MachineData data;
    // data を PLC から受け取って埋める(省略)

    winrt::hstring jsonH = SerializeMachineDataToJson(data);

    // BSTR は SysAllocString で確保
    return SysAllocString(jsonH.c_str());
}
catch (...)
{
    // 失敗時は空文字で返す/NULLで返す等、契約を決めておく
    return SysAllocString(L"");
}


}

C# 側(P/Invoke)で BSTR を string として受ける例

using System.Runtime.InteropServices;

internal static class Native
{
[DllImport("YourNative.dll", CallingConvention = CallingConvention.StdCall)]

[return: MarshalAs(UnmanagedType.BStr)]

public static extern string GetMachineDataJsonBstr(); }

この形だと、C# 側は string を受け取るだけで済み、解放の責任もランタイムが面倒を見てくれるため、実装がスッキリします(契約と例外時の返し方だけは先に決めておきます)。

C# 側でのデシリアライズ(System.Text.Json で DTO 化)

C# 側は、受け取った JSON 文字列を DTO に落とすだけです。キー名が一致していれば、基本的に追加設定なしで動きます。

DTO(受け皿クラス)の例

using System.Collections.Generic;

public class MachineDataDto
{
public int SchemaVersion { get; set; }
public int X { get; set; }
public double Speed { get; set; }
public string MachineId { get; set; } = "";
public List ArrayData { get; set; } = new();
public List ArrayDataIndex { get; set; } = new();
}

デシリアライズの例(例外/互換性を考慮)

using System;
using System.Text.Json;

string json = Native.GetMachineDataJsonBstr();

// JSON のキー名が多少ブレる可能性があるなら CaseInsensitive を有効化
var opt = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};

MachineDataDto? data = JsonSerializer.Deserialize(json, opt);

if (data == null)
{
// 受信失敗時の扱い(ログ、リトライ、既定値など)を決める
}

実務では、受信失敗時に「例外を握り潰す」のではなく、次のような情報をログに残せるようにしておくと、現場対応が圧倒的に楽になります。

  • 受信した JSON の先頭 N 文字(全量は個人情報やサイズ次第で注意)
  • SchemaVersion
  • 機械 ID(MachineId)
  • 配列長(ArrayData / ArrayDataIndex)

保守性を上げる「契約(スキーマ)」の作り方

JSON 連携は「動いたら終わり」ではなく、フィールド追加・変更が必ず来ます。PLC 周りは特に、現場改造で項目が増えがちです。そこで、最初に決めておくと効く設計ポイントをまとめます。

キー命名規則を固定する

C++ と C# の両方で読みやすいように、キー名は最初に固定します。おすすめは次のどちらかです。

  • PascalCase(例:MachineId)で統一(C# のプロパティと揃う)
  • camelCase(例:machineId)で統一(Web/JSON 文化に寄せる)

どちらでも良いですが、途中で混ぜないことが重要です。

SchemaVersion を入れて後方互換を確保する

JSON の先頭に SchemaVersion を入れておくと、C# 側で分岐できます。

  • v1:現行
  • v2:新フィールド追加、旧フィールドは残す(推奨)
  • v3:どうしても破壊的変更が必要なら、C# 側は v2/v3 を分岐して吸収

数値精度のルールを決める(重要)

Windows.Data.Json の number は double を前提に扱われます。PLC のカウンタや通算値などで 64bit 整数をそのまま JSON number にすると、値が大きい領域で丸めが起こる可能性があります。

  • 小さな int や double(速度、座標など)は number で問題になりにくい
  • 通算カウントやタイムスタンプ等の大きな整数は、string で持つ(例:”TotalCount”: “123456789012345678”)というルールにすると安全

配列の整合性を保証する

質問例では ArrayData と ArrayDataIndex のように、配列同士に関係がありそうです。実装側で次のどちらかを明確にすると、C# 側が楽になります。

  • 両配列の長さが常に一致する(不一致なら C++ 側で調整してから JSON 化)
  • または、配列を 1 つにまとめて「{ index, value }」の配列にする

後者(オブジェクト配列)にすると、拡張がさらに楽になります。

{
  "Items": [
    { "Index": 0, "Value": "one" },
    { "Index": 2, "Value": "two" }
  ]
}

運用で効くテクニック(ログ・障害解析・パフォーマンス)

JSON をそのままログに残せるようにする

現場で起きる「たまに壊れる」「特定の設備だけおかしい」は、ログがないと詰みます。C++ 側で JSON を作った直後に、次のようなログ方針を用意しておくと強いです。

  • 通常時:サイズ、SchemaVersion、MachineId、配列長だけ
  • 例外/不正時:JSON 全体(または先頭 N 文字)

頻度が高い場合は “差分” を検討する

PLC データを 10ms〜100ms で投げ続ける場合、毎回フル JSON を作ると CPU/GC のコストが見えてきます。必要なら次の工夫ができます。

  • 変化した項目だけを送る(C# 側でマージ)
  • 送信周期を分ける(高速系は最小項目、低速系は詳細項目)
  • JSON の生成を RapidJSON 等のストリーミングに寄せる(最適化が必要なときだけ)

例外の取り扱いを “契約” として決める

アンマネージ DLL 境界で例外を投げると、クラッシュの原因になります。C++ 側では次のいずれかに統一するのがおすすめです。

  • 失敗時は空 JSON(”{}”)を返す
  • 失敗時は {"Error":"...","SchemaVersion":1} のようにエラー JSON を返す
  • 失敗時は NULL を返し、C# 側で null 判定する(マーシャリング方式次第)

サードパーティで “記述量を減らす” 代替案(必要な場合のみ)

「Windows 公式にこだわらない」「将来的に Windows 以外も視野」「とにかく実装量を減らしたい」という場合は、nlohmann/json 等で書き味が良くなります。ただし MFC 型(CStringArray/CIntArray)を持つ限り、結局どこかで変換が必要です。

nlohmann/json でのイメージ(概念例)

// 例:std::string/std::vector に寄せられる場合に相性が良い
// MFC 型のままだと、CString -> std::string 等の変換コードは必要

// json j;
// j["X"] = src.X;
// j["Speed"] = src.Speed;
// j["MachineId"] = machineIdUtf8;
// j["ArrayData"] = vectorOfStrings;
// j["ArrayDataIndex"] = vectorOfInts;
// std::string jsonUtf8 = j.dump();

Windows だけ・依存を増やしたくない・現場保守重視であれば、やはり Windows.Data.Json(C++/WinRT) が堅い選択になります。

まとめ:実装の落とし穴を潰しながら、JSON 連携を安定させる

  • ネイティブ C++ には C# のようなリフレクション前提の “全自動 JSON 化” は標準ではない
  • だからこそ、キー契約を決めて手動マッピングし、公式/定番ライブラリで JSON を組み立てるのが保守的に強い
  • Windows 環境なら Windows.Data.Json(C++/WinRT) を使うと依存を増やさず実装できる
  • CString/hstring、文字コード(UNICODE/ANSI)、DLL 境界のメモリ管理は最初に設計を固める
  • C# 側は System.Text.Json で DTO 化し、SchemaVersion で拡張に備える

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次