C++
cli
VisualStudio
CLR

Visual Studio 2017 Visual C++ による CLR コンソールアプリの開発

はじめに

Visual C++ では通常の C++ プログラムの中で .NET Framework クラスライブラリを利用できます。この機能は CLI (Common Language Infrastructure) と呼ばれます。

この機能を使うと、従来の C++ アプリケーションを .NET Framework 上でほとんど手を加えずに実行できるようになります。(実際はかなり手間がかかるが)

プロジェクトの作成

「新しいプロジェクト」ダイアログを開き、Visual C++ / CLR をクリックし表示された一覧から「CLR コンソールアプリケーション」を選んで OK をクリックします。

CLRAppProject.png

次のようなプロジェクトがソリューションに追加されます。

CLRAppSolution.png

ありがちな "Hello World" ですが、main は .NET 形式です。コマンドライン引数は System.String の配列です。配列は C++ でも C# でも [] で表現しますが、C++/CLI ではマネージ型配列は array クラスとして表現します。
また、^ が付いた変数はマネージ型の変数です。

main() 関数を含む cpp ソースは次のようになります。

// ConsoleApplication1.cpp : メイン プロジェクト ファイルです。
#include "stdafx.h"

using namespace System;

int main(array<System::String ^> ^args)
{
    Console::WriteLine(L"Hello World");
    return 0;
}

サンプル:ファイル入力

このサンプルは .NET の StreamReader を使って、コマンドライン引数で指定されたテキストファイルの内容を表示します。

ヘッダーファイル

// stdafx.h : 標準のシステム インクルード ファイルのインクルード ファイル、または
// 参照回数が多く、かつあまり変更されない、プロジェクト専用のインクルード ファイル
// を記述します。
//

#pragma once

// TODO: プログラムに必要な追加ヘッダーをここで参照してください
#include <stdio.h>

C++ ソースファイル

// ConsoleApp01.cpp : メイン プロジェクト ファイルです。

#include "stdafx.h"

using namespace System;
using namespace System::IO;
using namespace System::Text;

int main(array<System::String ^> ^args)
{
    String ^line;
    Console::WriteLine(L"ファイル内容を表示する。");

    if (args->Length == 0)
    {
        Console::WriteLine(L"ファイルを指定してください。");
        return -1;
    }

    String ^fileName = args[0];
    Console::WriteLine(fileName);

    StreamReader ^reader = gcnew StreamReader(fileName, Encoding::UTF8);

    while (! reader->EndOfStream)
    {
        line = reader->ReadLine();
        Console::WriteLine(line);
    }

    reader->Close();
    Console::WriteLine(L"-- 終わり --");

    getchar();
    return 0;
}

サンプル:C++ 変数とマネージ変数の相互変換

C++ 変数とマネージ変数の相互変換ですが、System.Int32 (32ビット整数) などの値型変数 (Struct 型) は、そのまま代入で変換できます。

しかし、文字列 (System.String) は参照型 (Class 型) なので、単純に代入できない場合もあります。そのような場合には「マーシャリング」が必要です。

ヘッダーファイル

マーシャリングには msclr/marshal.h をインクルードしておく必要があります。

// stdafx.h : 標準のシステム インクルード ファイルのインクルード ファイル、または
// 参照回数が多く、かつあまり変更されない、プロジェクト専用のインクルード ファイル
// を記述します。
//

#pragma once

// TODO: プログラムに必要な追加ヘッダーをここで参照してください
#include <iostream>
#include <string>
#include <msclr/marshal.h>

C++ ソースファイル

マーシャリングには、marshal_context クラスの marsha_as(x) メソッドを使用します。マネージ型文字列 (System.String) からワイド文字列を取り出すためにはマーシャリングが必要です。

ワイド文字列をマネージ型文字列にするには、マーシャリングは不要です。推測ですが、内部で String(Char*) コンストラクタを呼び出している物と思われます。

値型のマネージ変数 (System.Int32など) は、プリミティブ (整数値など) の薄いラッパーなので、マーシャリングは不要になっています。

// ConsoleApp02.cpp : メイン プロジェクト ファイルです。
#include "stdafx.h"

using namespace System;
using namespace std;
using namespace msclr::interop;

int main()
{
    // 文字列はマネージ型で参照型なので、変換時にマーシャリングが必要。
    wchar_t* str = L"ハローワールド";
    String ^gcStr = gcnew String(str);
    Console::WriteLine(gcStr);
    marshal_context^ context = gcnew marshal_context();
    const char* str2 = context->marshal_as<const char*>(gcStr);

    printf_s("%s\n", str2);
    cout << str2 << endl;

    // 整数はマネージ型だが値型(構造体)なので、変換時にマーシャリングは不要
    Int32 n = 1024;
    int n0 = n;
    cout << n0 << endl;

    n = n0;
    cout << n << endl;

    getchar();
    return 0;
}

サンプル:クラス

マネージ型のクラスを宣言する場合は、先頭に ref を付けます。また、インスタンス化する場合は、new でなく gcnew 演算子を使用します。

new で作成したオブジェクトは自動ガーベージコレクションの対象になりません。gcnew で作成したオブジェクトは、マネージ型なので自動でガーベージコレクションが行われます。よって、delete 演算子でオブジェクトを解放する必要はありません。

このサンプル (Class1) では、ToString メソッドのオーバーライド、コンストラクタのオーバーロードを行っています。

ヘッダーファイル

// stdafx.h : 標準のシステム インクルード ファイルのインクルード ファイル、または
// 参照回数が多く、かつあまり変更されない、プロジェクト専用のインクルード ファイル
// を記述します。
//

#pragma once

// TODO: プログラムに必要な追加ヘッダーをここで参照してください

C++ ソースファイル

// ConsoleApp03.cpp : メイン プロジェクト ファイルです。

#include "stdafx.h"
#include "Class1.h"

using namespace System;

int main(array<System::String ^> ^args)
{
    Console::WriteLine(L"Class1 のテスト");

    String^ product = L"Lure";
    String^ info = L"";
    DateTime^ expir = gcnew DateTime(2020, 12, 28);

    Class1^ p1 = gcnew Class1(product, (Int16)9881, (Decimal)980, expir, (Int32)50, info);
    Console::WriteLine(p1->ToString());
    Console::WriteLine(p1->IsExpired());

    Console::ReadKey();
    return 0;
}

Class1 ヘッダーファイル

ref が付いた class キーワードでクラスを定義しています。ref が付いたクラスはマネージ型として扱われます。

ToString() メソッドはオーバーライドしているので、override キーワードが必要です。

#pragma once
ref class Class1
{
public:
    System::String^ Product;  // 商品名
    System::Int16^ Code;      // 商品コード
    System::Decimal^ Price;   // 単価
    System::DateTime^ Expired; // 販売期限
    System::Int32^ Count;     // 在庫数量
    System::String^ Info;     // 備考

public:
    Class1();
    Class1(System::String^ name, System::Int16^ code, System::Decimal^ price, System::DateTime^ expir, System::Int32^ count, System::String^ info);

    System::Boolean^ IsExpired();
    virtual System::String ^ ToString() override;
};

Class1 C++ ファイル

ヘッダーファイルで宣言したマネージ型クラス Class1 の実装です。

#include "stdafx.h"
#include "Class1.h"

using namespace System;

// デフォルトのコンストラクタ
Class1::Class1()
{
}

// コンストラクタ
Class1::Class1(String^ name, Int16^ code, Decimal^ price, DateTime^ expir, Int32^ count, String^ info)
{
    Product = name;
    Code = code;
    Price = price;
    Expired = expir;
    Count = count;
    Info = info;
}

// 販売期限を超えていたら TRUE
Boolean^ Class1::IsExpired()
{
    return (Expired->CompareTo(DateTime::Today) < 0);
}

// 商品をカンマ区切りテキストで表現
String^ Class1::ToString()
{
    String^  str =  Product + "," + Code + "," + Price + "," + Expired->ToString(L"yyyy-MM-dd") + "," + Count;
    if (Info->Length > 0)
    {
        str += (" " + Info);
    }
    return str;
}

サンプル:Win32 API の使用

C++/CLI でも普通に Win32 API の関数が利用できます。ただし、ヘッダーファイルで Windows.h をインクルードしなければなりません。

ヘッダーファイル
// stdafx.h : 標準のシステム インクルード ファイルのインクルード ファイル、または
// 参照回数が多く、かつあまり変更されない、プロジェクト専用のインクルード ファイル
// を記述します。
//

#pragma once

// TODO: プログラムに必要な追加ヘッダーをここで参照してください
#include <stdio.h>
#include <Windows.h>

C++ ソースファイル

Win32 API 関数と CLR の Console.WriteLine() を同時に使用しています。

// ConsoleApp04.cpp : メイン プロジェクト ファイルです。
#include "stdafx.h"
#define BUFFSIZE 1024

using namespace System;

int main()
{
    WCHAR buff[50];
    DWORD nBuff = sizeof(buff);
    String^ pcName;

    // Win32 API 関数
    GetComputerNameW(buff, &nBuff);

    pcName = gcnew String(buff);
    Console::WriteLine(pcName);

    getchar();
    return 0;
}

-