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# 側の型例 | 補足 |
|---|---|---|---|
| int | number | int | Windows.Data.Json は number を double として扱うため、巨大整数は注意 |
| double | number | double | 小数点は JSON 側で標準化される(ロケール依存にしにくい) |
| CString | string | string | Unicode(UTF-16)で渡すと Windows では扱いやすい |
| CStringArray | array(string) | List<string> | 要素ごとに Append |
| CIntArray | array(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 の実体 | 推奨方針 | 理由 |
|---|---|---|---|
| UNICODE | CStringW(UTF-16) | UTF-16 のまま渡す | Windows と .NET の string は内部的に UTF-16。最短で安全 |
| ANSI | CStringA(コードページ依存) | 可能なら 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# 側 | メリット | 注意点 |
|---|---|---|---|---|
| BSTR | SysAllocString で確保 | 戻り値 string に直マーシャリング可 | 扱いやすい、UTF-16 | COM 系の流儀。BSTR 解放規約を守る |
| CoTaskMemAlloc | CoTaskMemAlloc で確保 | 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 で拡張に備える

コメント