週末!プログラミング部

ソフトウェア開発ネタを中心に自分でいろいろ調べた内容を自分の勝手な解釈で思うがままに書いくためのブログ。サンプルソースコード、API、プラットフォーム、プログラミング言語、開発環境などを調査、分析して追求いく予定です。

CとC#で共有メモリを使ったプロセス間通信をさせてみる

ちょっとCとC#で共有メモリを使ったプロセス間通信が必要になったので調べてみました。
共有メモリを使用したプロセス間通信は昔からあるので、ネットで探せばやり方がたくさん出てきます。
今回は特にわかりやすかった以下のサイトを参考にさせていただき、自分が使いやすいように若干アレンジを加えました。
https://qiita.com/kkent030315/items/38559687812924c279a9

C側ソースコード

まずはC側からです。
対向はC#なのでWindowsで動作する環境を想定しています。
したがってWindowsAPI(windows.h)を使用します。

/************************************************************
 * インクルードファイル
 ***********************************************************/
#include <stdio.h>
#include <windows.h>

/************************************************************
 * 構造体宣言
 ***********************************************************/
typedef struct
{
    double   p1;
    short    p2;
    int      p3;
} SHARED_MEMORY_DATA;

/************************************************************
 * 大域変数宣言
 ***********************************************************/
LPCTSTR gSharedMemoryName   = L"shmem";
HANDLE  gSharedMemoryHandle = NULL;
SHARED_MEMORY_DATA* gMappingObject = NULL;

/************************************************************
 * ファイルマッピングオブジェクトを開く
 ***********************************************************/
int OpenSharedMemoryMappingObiect(void)
{
    int ret = 1;

    /* ファイルマッピングオブジェクトを開く */
    gSharedMemoryHandle = OpenFileMapping( FILE_MAP_ALL_ACCESS, 
        FALSE,
        gSharedMemoryName);

    /* ファイルマッピングオブジェクトが開けなかった場合 */
    if (gSharedMemoryHandle == NULL)
    {
        /* ファイルマッピングオブジェクトを作成 */
        gSharedMemoryHandle = CreateFileMapping(
            INVALID_HANDLE_VALUE,
            NULL,
            PAGE_READWRITE,
            0,
            sizeof(SHARED_MEMORY_DATA),
            gSharedMemoryName);

        if (gSharedMemoryHandle == NULL)
        {
            ret = 0;
        }
    }

    return (ret);
}

/************************************************************
 * メモリマップトオブジェクトを取得
 ***********************************************************/
int GetSharedMemoryMappingObiect()
{
    int ret = 1;

    /* メモリ */
    if (gSharedMemoryHandle != NULL)
    {
        gMappingObject = (SHARED_MEMORY_DATA*)MapViewOfFile(
            gSharedMemoryHandle,
            FILE_MAP_ALL_ACCESS,
            0,
            0,
            sizeof(SHARED_MEMORY_DATA));

        if (gMappingObject == NULL)
        {
            ret = 0;
        }
    }
    else
    {
        ret = 0;
    }

    return (ret);
}

/************************************************************
 * エントリポイント
 ***********************************************************/
int main()
{
    SetConsoleTitle(L"C側のプログラム");

    /* 共有メモリマップトファイルが開けなかった場合 */
    if (OpenSharedMemoryMappingObiect() != 1)
    {
        printf("not opened memory-mapped object.");
    }
    /* メモリマップトオブジェクトを取得できなかった場合 */
    else if (RefSharedMemoryMappingObiect() != 1)
    {
        printf("not referenced memory-mapped object.");
    }
    /* メモリマップトオブジェクトが作成できた場合 */
    else
    {
        /* 共有メモリ読み込み */
        printf("共有メモリ読み込み\r\n");
        printf("-----------------------\r\n");
        printf("p1 : %f\r\n", gMappingObject->p1);
        printf("p2 : %d\r\n", gMappingObject->p2);
        printf("p3 : %d\r\n", gMappingObject->p3);
        printf("-----------------------\r\n");

        getchar();  /* キー入力:C#側待ち合わせ */

        /* 共有メモリ書き込み */
        gMappingObject->p1 = 123.456;
        gMappingObject->p2 = 256;
        gMappingObject->p3 = -100;
        printf("共有メモリ書き込み\r\n");
        printf("-----------------------\r\n");
        printf("p1 : %f\r\n", gMappingObject->p1);
        printf("p2 : %d\r\n", gMappingObject->p2);
        printf("p3 : %d\r\n", gMappingObject->p3);
        printf("-----------------------\r\n");

        getchar();
    }

    /* リソースを解放 */
    if (gMappingObject != NULL)
    {
        UnmapViewOfFile(gMappingObject);
    }

    if (gSharedMemoryHandle != NULL)
    {
        CloseHandle(gSharedMemoryHandle);
    }
}

OpenSharedMemoryMappingObiect()

この関数ではOpenFileMapping()でファイルマッピングオブジェクトをオープンします。
もしオープンできなければCreateFileMapping()でファイルマッピングオブジェクトを作っています。
CreateFileMapping()はページングファイル上のメモリ領域に対するファイルマッピングオブジェクトを作ってくれます。
http://nienie.com/~masapico/api_CreateFileMapping.html

ここではC#側のプログラムがファイルマッピングオブジェクトを作ってくれてればOpenFileMapping()で開けて、 なければCreateFileMapping()で作ってC#側のプログラムで開いてもらうことを想定しています。

RefSharedMemoryMappingObiect()

この関数では開いた(または作成した)ファイルマッピングオブジェクトにアクセスするためにMapViewOfFile()を使っています。
MapViewOfFile()はマップしたメモリ領域へのポインタを返してくれます。
このポインタをSHARED_MEMORY_DATA*にキャストすれば、構造体メンバを読み書きすることで共有メモリにアクセスできるようになります。
http://nienie.com/~masapico/api_MapViewOfFile.html

C#ソースコード

続いてC#側です。

SharedMemoryクラス

共有メモリにアクセスする箇所はSharedMemoryクラスにまとめてみました。
C#(.NET)でもSystem.IO.MemoryMappedFilesというアセンブリを使用することでC側のプログラムと同様にページング領域にファイルをマッピングしたりアクセスすることができます。
System.IO.MemoryMappedFilesは.NET 4.0から使用できるようになりました。
https://codezine.jp/article/detail/4279

using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;

namespace shmem_test
{
    class SharedMemory<T> where T : struct
    {
        private MemoryMappedFile mMemoryMappedFile;
        private MemoryMappedViewAccessor mMemoryMappedViewAccessor;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public SharedMemory()
        {
            mMemoryMappedFile = null;
            mMemoryMappedViewAccessor = null;
        }

        /// <summary>
        /// デストラクタ
        /// </summary>
        ~SharedMemory()
        {
            close();
        }

        /// <summary>
        /// 共有メモリオープン
        /// </summary>
        /// <param name="sharedMemoryName"></param>
        /// <returns></returns>
        public bool open(string sharedMemoryName)
        {
            bool result = true;

            try
            {
                try
                {
                    mMemoryMappedFile = 
                        MemoryMappedFile.OpenExisting(sharedMemoryName);
                }
                catch (FileNotFoundException ex)
                {
                    mMemoryMappedFile = 
                        MemoryMappedFile.CreateOrOpen(sharedMemoryName,
                                                      Marshal.SizeOf<T>());
                }

                mMemoryMappedViewAccessor =
                    mMemoryMappedFile.CreateViewAccessor();
            }
            catch
            {
                result = false;
            }

            return (result);
        }

        /// <summary>
        /// 共有メモリクローズ
        /// </summary>
        /// <returns></returns>
        public bool close()
        {
            bool result = true;

            mMemoryMappedViewAccessor?.Dispose();
            mMemoryMappedFile?.Dispose();

            return ( result );
        }

        /// <summary>
        /// 共有メモリ読み込み
        /// </summary>
        /// <param name="structure"></param>
        /// <returns></returns>
        public bool read(ref T structure)
        {
            bool result = true;

            if (mMemoryMappedViewAccessor == null)
            {
                result = false;
            }
            else
            {
                mMemoryMappedViewAccessor.Read(0, out structure);
            }

            return result;
        }

        /// <summary>
        /// 共有メモリ書き込み
        /// </summary>
        /// <param name="structure"></param>
        /// <returns></returns>
        public bool wrtie(T structure)
        {
            bool result = true;

            if (mMemoryMappedViewAccessor == null)
            {
                result = false;
            }
            else
            {
                mMemoryMappedViewAccessor.Write(0, ref structure);
            }

            return result;
        }
    }
}
open()

このメソッドではOpenExisting()を使ってファイルマッピングオブジェクトをオープンします。
もしオープンできなかった場合、C側のプログラムと違いFileNotFoundException例外をスローしてくれます。
なのでFileNotFoundException例外の中でCreateOrOpen()を使ってファイルマッピングオブジェクトを作ります。
ファイルマッピングオブジェクトができたらCreateViewAccessor()でファイルマッピングオブジェクトにアクセスするためのオブジェクトを作っています。

close()

C側のプログラムと同様に不要となったリソースオブジェクトをDispose()で破棄しています。

read()

MemoryMappedViewAccessorを使ってファイルマッピングオブジェクトを読み取ります。
streamなので他のstramクラスと同様な感じで使用できそうです。
ただよくわからなかったのはC側のプログラムと同様に構造体のメンバ変数単位でアクセスする方法がわかりませんでした。。
なにかよい方法があるのかなぁ(・´ω`・;)

write()

ファイルマッピングオブジェクトの書き込みもMemoryMappedViewAccessorを使って行います。
read()同様に書き込みも構造体のメンバ変数単位でアクセスする方法が不明。。。

エントリポイントクラス

こちらはSharedMemoryクラスを使うクラスです。
使い方は以下のとおりです!

using System;

namespace shmem_test
{
    public struct Sh_mem_data_st
    {
        public double p1;
        public short  p2;
        public int    p3;
    }

    class Program
    {
        static void Main(string[] args)
        {
            Console.Title = "C#側のプログラム";

            SharedMemory<Sh_mem_data_st> shmem
                = new SharedMemory<Sh_mem_data_st>();
            shmem.open("shmem");

            Sh_mem_data_st st = new Sh_mem_data_st();
            st.p1 = 567.987;
            st.p2 = 0;
            st.p3 = 10;
            shmem.wrtie(st);
            Console.WriteLine("共有メモリ書き込み");
            Console.WriteLine("-----------------------");
            Console.WriteLine(st.p1);
            Console.WriteLine(st.p2);
            Console.WriteLine(st.p3);
            Console.WriteLine("-----------------------\r\n");

            Console.ReadKey();  // キー入力:C側待ち合わせ

            Sh_mem_data_st result = new Sh_mem_data_st();
            shmem.read(ref result);
            Console.WriteLine("共有メモリ読み込み");
            Console.WriteLine("-----------------------");
            Console.WriteLine(result.p1);
            Console.WriteLine(result.p2);
            Console.WriteLine(result.p3);
            Console.WriteLine("-----------------------");

            Console.ReadKey();
        }
    }
}

実行結果

参考にしたサイトさんのやり方をまねてコンソールタイトルにそれぞれどちら側のプログラムか表示するようにしてみました。
今回のプログラムの実行手順としては以下のようになります。

  1. C#側プログラムを起動(先に共有メモリに書き込むので)
  2. C側プログラムを起動(共有メモリの値を読み込み)
  3. C側プログラムでキー入力(共有メモリに値を書き込み)
  4. C#側プログラムでキー入力(共有メモリの値を読み込み)

結果は以下のとおり。 f:id:NATSU_NO_OMOIDE:20201229230647g:plain