Avance.Lab

技術紹介

第2回 Visual C++で作成したDLL内のクラスをC#で利用する方法

公開日:2025.11.21 更新日:2025.11.21

tag: Windows

こんにちは、ILCです。

お待たせしました!
前回の続編の記事をお届けします。最後まで読んでいただけたら嬉しいです。
今回は第2回として、DLLに定義したクラスにC#のようなプロパティ機能を持たせる方法をご紹介します。
本記事は前回の記事の続編となりますので、まだご覧になっていない方は、ぜひそちらもチェックしてみて下さいね!

前回のおさらい

前回の記事では、VC++で定義したクラスをDLLにまとめる方法と、それを確認用のコンソールアプリケーションから利用する手順について紹介しました。

主なポイントは以下のとおりです。

  • 公開クラスと、そこから派生した非公開クラスを使った実装方法。
  • 下位互換性を意識したバージョン管理の考え方。
  • 利用者側は再ビルドなしでのDLLを更新できる。(※動的ロード時のみ)
  • 動的ロード(明示的リンク)と動的リンク(暗黙的リンク)の違いと利用方法。

このように、VC++で定義されたクラスをDLL経由で扱うための基本的な設計と実装を確認したのが前回の内容でした。

それでは今回は、そこにさらに C#のようなプロパティ機能 を持たせる方法を試してみましょう!

本記事で利用するVC++固有機能について

今回ご紹介する「プロパティ機能」は、Microsoft Visual C++(MSVC) 固有の拡張機能である__declspec(property)を利用しています。

この機能は標準C++の仕様には含まれておらず、他のコンパイラ環境では使用できません。
本記事は、VC++環境での開発を前提としています。

DLLのSDKバージョン更新

前回の記事では、DLL内のクラス定義に SDKバージョン1 としていました。

今回のサンプルでは、この SDKバージョンを2 に更新し、新たに CExportSDK2 クラスを定義していきます。

これにより、前回のバージョンとの互換性を保ちながら、新しい機能である「プロパティの実装」を追加することができ、また、クラス定義にバージョン情報を組み込むことで将来的にDLLを更新した際も、利用者側が互換性を判断しやすくなるという利点もあります。

それでは、さっそく SDKバージョン2 の雛形となるクラスを定義していきましょう。

公開用ヘッダーファイルの定義

SDKバージョン2用に CExportSDK2 クラスを定義します。

このクラスは、互換性を維持するため CExportSDK1 クラスから継承させます。

#pragma once

// クラス定義
class DLL_API CExportSDK2 : public CExportSDK1
{
protected:
    // コンストラクタ
    CExportSDK2();

    // デストラクタ
    virtual ~CExportSDK2();

    CExportSDK2(const CExportSDK2&) = delete; 
    CExportSDK2& operator= (const CExportSDK2&) = delete;

public:
    // インスタンス作成/削除
    static CExportSDK2& CreateInstance(INT Version);
    static void DeleteInstance(CExportSDK2& Object);
};

非公開用ヘッダーファイルの定義

内部で使用する非公開クラス CDerivedSDK は、CExportSDK2 を基底クラスとして継承しています。

それ以外の宣言や実装については前回の記事から変更ありません。

このクラスは CExportSDK2 の実体であり、利用者には公開されません。

#pragma once

// クラス定義
class CDerivedSDK : public CExportSDK2
{
private:
    INT     m_Version;

public:
    // コンストラクタ
    CDerivedSDK(INT Version);

    // デストラクタ
    virtual ~CDerivedSDK();

    CDerivedSDK(const CDerivedSDK&) = delete; 
    CDerivedSDK& operator= (const CDerivedSDK&) = delete;

public:
    // メソッド
    virtual INT GetVersion() const;
    virtual INT GetClassSize();
};

公開クラスの実装ファイル

次に先ほどの CExportSDK2 の実装をします。

ここでは、内部で実体の CDerivedSDK を生成し、キャストして返す構造になっています。

#include "pch.h"
#include "DllAPI.h"
#include "CDerivedSDK.h"

// コンストラクタ
CExportSDK2::CExportSDK2()
{

}

// デストラクタ
CExportSDK2::~CExportSDK2()
{

}

// インスタンス作成
CExportSDK2& CExportSDK2::CreateInstance(INT Version)
{
    // バージョン確認
    if (Version != EXPORT_SDK_VER_2) {
        AfxThrowNotSupportedException();
    }

    CDerivedSDK* pObject = new CDerivedSDK(Version);

    return *pObject;
}

// インスタンス削除
void CExportSDK2::DeleteInstance(CExportSDK2& Object)
{
    delete (CDerivedSDK*)&Object;
}

公開用ヘッダーファイルのバージョン更新

定義した CExportSDK2 クラスを利用できるように、 SDKバージョン2として EXPORT_SDK_VER_2 を宣言し、ターゲットバージョンを変更しましょう。

これにより、CExportSDK が自動的に適切なバージョンのクラスを指すようになります。

#pragma once

// エクスポート/インポート定義
#ifdef _DLL_EXPORT
    #define DLL_API   __declspec(dllexport)
#else
    #define DLL_API   __declspec(dllimport)
#endif

// バージョン定義
#define EXPORT_SDK_VER_1    1
#define EXPORT_SDK_VER_2    2

// 対象バージョン
#define TARGET_SDK_VERSION  EXPORT_SDK_VER_2

// 対象バージョンに応じて切り替え
#if TARGET_SDK_VERSION <= EXPORT_SDK_VER_1
    #define CExportSDK CExportSDK1
#elif TARGET_SDK_VERSION <= EXPORT_SDK_VER_2
    #define CExportSDK CExportSDK2
#else
    // 未対応バージョン
#endif

// クラス定義のインクルード
#include "CExportSDK1.h"
#include "CExportSDK2.h"

// 関数型 定義
typedef CExportSDK& (WINAPI *CREATEEXPORTSDK)(INT);

// 関数型 定義
typedef void (WINAPI *DELETEEXPORTSDK)(CExportSDK&);

// インスタンス作成
extern "C" DLL_API CExportSDK& WINAPI CreateExportSDK(INT Version);

// インスタンス削除
extern "C" DLL_API void WINAPI DeleteExportSDK(CExportSDK& Object);

以上で、新たな SDKバージョン2 用のDLLが作成されました。

実際に更新されたかどうかを確認するには、前回作成されたコンソールアプリケーションから呼び出してみましょう。

ただし、その際には TestConsole.cpp の以下のコード部分で作成しているオブジェクトの型を変更してからビルド・実行してください。

SDK Versionの表示が 1 から 2 へ変わったことがわかります。

※ 変更前
CExportSDK1& SDKLibrary = SDK.CreateSDK();
        
※ 変更後
CExportSDK& SDKLibrary = SDK.CreateSDK();
===== 確認テスト =====
SDK Version = 2
Class Size (インスタンス) = 16
Class Size (CExportSDK) = 8

プロパティの実装

いよいよ本題である「プロパティの実装」に入ります。

ここからは、SDKバージョン2 の CExportSDK2 クラスに、C#のようなプロパティ機能を追加する方法を解説します。まずはその準備として、__declspec(property) を利用したプロパティ定義・実装用のマクロを作成してみましょう。

Visual C++ 固有の __declspec(property) は、メソッド呼び出しをプロパティアクセス構文のように扱うための機能です。例えば obj.Property のように書くと、実際には get_Property() メソッドが呼び出されるシンタックスシュガーです。
この構文は VC++ 固有の機能であり、他のコンパイラ環境では使用できませんので注意してください。

プロパティ定義・実装用マクロ

プロパティは大きく分けて「取得専用」「設定専用」「設定・取得両用」の3種類があります。
ここでは、最も基本的な取得専用プロパティを定義するためのマクロを作成してみましょう。
また、マクロ中の引数の意味は下記になります。

T:プロパティの型
Name:プロパティ名
Class:実装する側のクラス名

#pragma once

// プロパティ定義 (値型 取得専用)
#define DECLARE_PROPERTY_GETONLY(T, Name) \
    virtual T __getProperty_##Name() const; \
    __declspec(property(get = __getProperty_##Name)) T Name;

// プロパティ定義 (const 参照型 取得専用)
#define DECLARE_PROPERTY_CONST_REF_GETONLY(T, Name) \
    virtual const T& __getProperty_##Name() const; \
    __declspec(property(get = __getProperty_##Name)) const T& Name;

// プロパティ定義 (非const 参照型 取得専用)
#define DECLARE_PROPERTY_REF_GETONLY(T, Name) \
    virtual T& __getProperty_##Name(); \
    __declspec(property(get = __getProperty_##Name)) T& Name;

// プロパティ実装 (値型)
#define IMPLEMENT_PROPERTY_GET(Class, T, Name) \
    T Class::__getProperty_##Name() const

// プロパティ実装 (const 参照型)
#define IMPLEMENT_PROPERTY_CONST_REF_GET(Class, T, Name) \
    const T& Class::__getProperty_##Name() const

// プロパティ実装 (非const 参照型)
#define IMPLEMENT_PROPERTY_REF_GET(Class, T, Name) \
    T& Class::__getProperty_##Name()

マクロの役割

  • DECLARE 系マクロ
    クラス内でプロパティ宣言と対応する取得関数をまとめて定義します。
    値型・参照型・const参照型を用途に応じて使い分けます。
  • IMPLEMENT 系マクロ
    DECLARE で宣言した関数の実装を簡単に定義するためのマクロです。
    実際の戻り値を返す処理をこの中に記述します。

const と 非const の違い

  • const参照
    取得したオブジェクトを変更させたくない場合に使用します。
    例:内部データを読み取り専用で公開したい場合など。
  • 非const参照
    取得したオブジェクトに対して設定操作を行う可能性がある場合に使用します。
    例:取得したオブジェクトに setter プロパティを持ち、状態を変更したい場合など。

これらのマクロは、公開用ヘッダーファイルにインクルードして利用します。

これでプロパティ定義の準備が整いました。
次に、実際にこれらのマクロを使ってクラスにプロパティを追加してみましょう。

値型プロパティの実装 (SDKVersion)

まずは、SDKバージョン番号を返す値型の読み取り専用プロパティ SDKVersion を追加してみましょう。
これは既存の関数 GetVersion() をプロパティ化したものです。

公開用クラス側の定義

公開用クラス(CExportSDK2)にプロパティを定義します。
ここでの実装では、実際の値を返さずに例外を投げています。
これは、公開クラス側では直接データを保持せず、非公開クラスで処理を行う設計にしているためです。
通常、この関数が実際に呼ばれることはありません。

class DLL_API CExportSDK2 : public CExportSDK1
{
    // 既存メンバは省略

public:
    // プロパティ
    DECLARE_PROPERTY_GETONLY(INT, SDKVersion)
};
// 既存メソッドは省略

// プロパティ(SDKバージョンの取得)
IMPLEMENT_PROPERTY_GET(CExportSDK2, INT, SDKVersion)
{
    AfxThrowNotSupportedException();
}

非公開クラス側の定義

次に、実際にデータを保持し値を返す 非公開クラス(CDerivedSDK)側で実装します。

class CDerivedSDK : public CExportSDK2
{
    // 既存メンバは省略

public:
    // プロパティ
    DECLARE_PROPERTY_GETONLY(INT, SDKVersion)
};
// 既存メソッドは省略

// プロパティ(SDKバージョンの取得)
IMPLEMENT_PROPERTY_GET(CDerivedSDK, INT, SDKVersion)
{
    return m_Version;
}

これにより、内部実装を隠蔽したまま、外部からは SDK.SDKVersion のようにシンプルな構文でSDKバージョン番号を取得できます。

参照型プロパティの実装 (TimeManager)

次に、クラスオブジェクトを返す参照型プロパティを追加してみましょう。
例として、CTimeManager クラスをプロパティ対象とし、内部に現在時刻を返す NowTime プロパティを持たせます。

公開用クラス CTimeManager の定義

まず、SDK公開側の CTimeManager クラスを定義します。
このクラスはインターフェース層のため、プロパティ取得時には実際の処理を行わず、実態が存在しない場合は例外を投げるようにします。

#pragma once

// クラス定義
class DLL_API CTimeManager
{
protected:
    // コンストラクタ
    CTimeManager();

    // デストラクタ
    virtual ~CTimeManager();

    CTimeManager(const CTimeManager&) = delete;
    CTimeManager& operator= (const CTimeManager&) = delete;

public:
    // プロパティ
    DECLARE_PROPERTY_GETONLY(CString, NowTime)
};
#include "pch.h"
#include "DllAPI.h"
#include "CTimeManager.h"

// コンストラクタ
CTimeManager::CTimeManager()
{

}

// デストラクタ
CTimeManager::~CTimeManager()
{

}

// プロパティ(時間の取得)
IMPLEMENT_PROPERTY_GET(CTimeManager, CString, NowTime)
{
    AfxThrowNotSupportedException();
}

非公開用クラス CTimeManagerDerived の定義

次に、内部実装を行う非公開クラス CTimeManagerDerived を定義します。
こちらでは実際に現在時刻を取得し、文字列形式で返す処理を実装します。

#pragma once

// クラス定義
class CTimeManagerDerived : public CTimeManager
{
public:
    // コンストラクタ
    CTimeManagerDerived();

    // デストラクタ
    virtual ~CTimeManagerDerived();

    CTimeManagerDerived(const CTimeManagerDerived&) = delete;
    CTimeManagerDerived& operator= (const CTimeManagerDerived&) = delete;

public:
    // プロパティ
    DECLARE_PROPERTY_GETONLY(CString, NowTime)
};
#include "pch.h"
#include "DllAPI.h"
#include "CTimeManagerDerived.h"

// コンストラクタ
CTimeManagerDerived::CTimeManagerDerived()
{

}

// デストラクタ
CTimeManagerDerived::~CTimeManagerDerived()
{

}

// プロパティ(時間の取得)
IMPLEMENT_PROPERTY_GET(CTimeManagerDerived, CString, NowTime)
{
    CTime nowTime = CTime::GetCurrentTime();

    return nowTime.Format(_T("%H:%M:%S"));
}

公開用クラス CExportSDK2 に参照型プロパティを追加

次に、SDKのメインクラス CExportSDK2 に、CTimeManager への参照を返すプロパティを定義します。

#pragma once

// クラス定義
class DLL_API CExportSDK2 : public CExportSDK1
{
    // 既存メンバは省略

public:
    // プロパティ
    DECLARE_PROPERTY_REF_GETONLY(CTimeManager, TimeManager)
};
// 既存メソッドは省略

// プロパティ(CTimeManagerの取得)
IMPLEMENT_PROPERTY_REF_GET(CExportSDK2, CTimeManager, TimeManager)
{
    AfxThrowNotSupportedException();
}

非公開用クラス CDerivedSDK に参照型プロパティの実装

非公開クラス CDerivedSDK が CTimeManagerDerived のインスタンスを保持し、公開クラス CExportSDK2 経由で TimeManager プロパティを参照できるようにします。

#pragma once

#include "CTimeManagerDerived.h"

// クラス定義
class CDerivedSDK : public CExportSDK2
{
    // 既存メンバは省略

private:
    CTimeManagerDerived     m_TimeManager;

public:
    // プロパティ
    DECLARE_PROPERTY_REF_GETONLY(CTimeManager, TimeManager)
};
// 既存メソッドは省略

// プロパティ(CTimeManagerの取得)
IMPLEMENT_PROPERTY_REF_GET(CDerivedSDK, CTimeManager, TimeManager)
{
    return m_TimeManager;
}

これで、外部コードからは、SDK.TimeManager.NowTime のように TimeManager 経由で現在時刻を取得できるようになります。

このとき、内部的にはプロパティマクロ定義が展開され、下記の呼び出しチェーンを経由する仕組みになります。

SDK.TimeManager.NowTimeCDerivedSDK::__getProperty_TimeManager()CTimeManagerDerived::__getProperty_NowTime()

このように、公開層から内部実装層へと責務分離を保ちながら、C#のようなプロパティ構文でアクセスできるようになります。

以上、今回は、読み取り専用プロパティに焦点をあてた定義方法をご紹介しました。
ここまでで紹介した内容をすべて反映した、プロパティ実装の全体コードを以下に示します。
表示する場合は、▶をクリックして展開してください。

プロパティの確認

前章で、CExportSDK2 クラスに取得専用プロパティを実装しました。
実際にコンソールアプリからDLLを呼び出し、追加したプロパティが正しく動作するか確認してみましょう。

以下は、前回と同様にDLLをロードして、SDKインスタンスを操作するテスト用コンソールアプリのコードです。変更箇所以外のコードにつきましては、前回の記事をご確認ください。

#include "pch.h"
#include "framework.h"
#include "DllAPI.h"
#include "TestConsole.h"
#include "CSDKLibrary.h"

using namespace std;

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

// メイン関数
int wmain(int argc, wchar_t* argv[], wchar_t* envp[])
{
    CSDKLibrary SDK;

    // DLLのロード
    if (!SDK.Load(_T("DllLibrary.dll"))) {
        return 1;
    }

    // インスタンス作成
    CExportSDK& SDKLibrary = SDK.CreateSDK();

    // プロパティの呼び出し
    INT     sdkVersion = SDKLibrary.SDKVersion;
    CString timeText   = SDKLibrary.TimeManager.NowTime;

    wcout << "===== 確認テスト =====" << endl;
    wcout << "SDK Version = " << sdkVersion << endl;
    wcout << "Time Text   = " << (LPCTSTR)timeText << endl;

    // インスタンス削除
    SDK.DeleteSDK(SDKLibrary);

    system("pause");

    return 0;
}

それでは。実行してみましょう。
以下のような結果が表示されるはずです。

===== 確認テスト =====
SDK Version = 2
Time Text   = 20:09:42

このように、公開用クラスから直接プロパティにアクセスでき、非公開クラスで保持している値を取得できていることが確認できましたね!

終わりに

今回は、DLLに定義されたSDKクラスのバージョン管理や、C#のように直感的にアクセスできる取得専用プロパティの実装方法についてご紹介しました。
ポイントは、バージョンごとの公開用クラスを通して、実体であるインスタンスのメソッドやプロパティを安全に呼び出せる構造にあることです。

プロパティ定義用のマクロは一見複雑に見えますが、一度作ってしまえば再利用性が高く以降の実装が非常にシンプルになります。
実際、マクロ部分を深く理解していなくても、テンプレートとしてそのまま使うだけで十分に効果を発揮するでしょう。

今回実装した取得専用プロパティはまだ入口にすぎません。今後は、設定専用、設定・取得両用、さらには連想配列を利用したプロパティなど、より柔軟な設計へ発展させることが可能です。
基本を押さえておくことで、DLL内クラスをC#から呼び出す際の設計に手助けとなるでしょう。

最後に、この記事が Visual C++ による DLLクラス設計や、プロパティを通じたアクセス設計の理解に少しでも役立てば幸いです。

次回は、設定専用プロパティの定義方法について紹介していきたいと思います!

ILC

Windowsアプリをメインに開発。
自然豊かな場所にドライブや散歩で癒されるのが好き。

関連記事