LIFULL Creators Blog

LIFULL Creators Blogとは、株式会社LIFULLの社員が記事を共有するブログです。自分の役立つ経験や知識を広めることで世界をもっとFULLにしていきます。

Simple Example: C#からC++用のDLLを使ってシリアル通信を行う

Apple原理主義者の大坪です。何故Apple原理主義者がC#を使うか?私は狂信的なApple原理主義者ですが、現実主義者でもあるのです。必要があればなんでも使ってやろうじゃないの。

とはいえ

最近昔を思い返すことが多い。当時は新しい環境に移る時まず本を買ったものです。しかし最近はGoogle先生にお伺いをたてればあれこれ情報が手に入る。とはいえ新しい環境に移るときは

「そもそも何のキーワードで探せばよいのか」

という問題にぶつかる。WindowsのPC用アプリケーションってなんて呼ばれているのか。(Macなら"Mac OS X Application"ですむのですが)

というレベルなので、今回書く内容は多分最適とか正解からはほど遠い物で、「今とりあえずこれで動いている」というものです。おまけにあれやこれやの理由から動くプロジェクトではなく、必要部分のソースだけを出しますがご容赦ください。

さて

試行錯誤の末まずたどり着いたのが"WinForm"なるキーワードです。しばらくそれを使ってあれこれやっていたのですが、どうも様子がおかしい。調べてみると最近はそれがWPFなるものに変わったらしい。でもって「ザムル」とかいろいろ恐ろしい言葉が並ぶ訳です。いつのまにかGUIはXMLで定義し、その背後のコードと分離するやり方が主流になったのだな、、などとXCodeで作る際にもInterface Builder(年がばれるか?)を使わない人間としては感慨に耽るのでした。

などとあれこれ言っている場合ではない。画面がなんとかできると、今度はC++で開発されたDLLライブラリの中の関数をC#からコールし、シリアル通信を行う必要があるのです。その部分のコード(一部)を記事の末尾に示します。ちなみにここに示したコードは「駄目なコードでもないよりはマシ。少なくとも出発点として使ってもらえるだろう」ポリシーの元公開しておりますので、左様ご承知頂きたく。

このコードは、ソーバル社のHRW-1000というRFIDリーダーからカードの情報を読み取るためのものです。ドライバはTRW社のサイトからダウンロードできます。ここからプロトコル仕様に従って、ごりごり通信を行うこともできるのでしょうか、せっかくWindowsで開発しているので、ソーバル社から提供されているDLLライブラリを利用して通信します。

やりたいことはなにか?言葉で書けば

・デバイスをオープンしてCOMポートで通信できるようにする

・「カードを読み取ってね」というコマンドをbyte列にしてリーダーに送信する

・読み取った結果を、byte列としてリーダーから受信する。

明確だし、簡単に聞こえますね。しかしこうしたハードウェアに近いところの処理というのは、その環境特有のあれこれをしないと動いてくれない。Apple原理主義者がつまづいた点を以下に列挙します。

  1. C#からC++用のDLLを呼ぶ時には、DLLImportというものを使わなくてはならない。

それまでもC#用のDLLを使う事があったのですが「参照の追加」をすればなんとかなりました。しかしC++用のDLLではそれだけではだめ。DLLImportで場所を指定する必要があります。

DllimportでGoogle先生にお伺いを立てると、シンプルな例がいくつもでてきます。そうかそうか、と

素直に

DLLImport("rfidsobal.dll);

と書いたのはだめで、ここに書いたように"CallingConvention = CallingConvention.Cdecl"とパラメータを指定する必要があるようです。なぜかというと、このDLLが「アンマネージドコード」と呼ばれるものであるため。この場合はWin32.APIを用いているようです。

  1. ライブラリとのパラメータ変換

次の問題はパラーメータの型変換です。メインのプログラムはC#で、DLLはC++用のもの。そりゃ言語も違うから型も違うだろう。ではどうしよう、とGoogle先生に聞いてみるとこんなページがヒットする。とても解りやすくかかれているのですが、ここだけ観ていればOKというほど話はストレートにいかない。

今回使うAPIでは受信データをcharにいれてくれることになっています。なるほど、ではcharは、、、そうかSystem.Text.StringBuilderにすればいいんだ、とやってみると何故か最初の一文字しか受信できない。これは困った。

後から考えれば当たり前の話で、APIではchar*になってますけど、具体的にはbyteでデータをつめて返してくれるわけです。それはもちろん文字列ではない。しかしStringBuilderで受けると「ここに入ってるのは、当然文字コードなんだよねー」と「解釈」されてしまいます。結果として、どうやっても最初の一文字しか読み取れない。(2byte目にnullがあったんだと思います)

ではどうするか、というわけでまたあちこちさがしてたどり着いたのがprivate void checkResult()に書いたような方法です。

多分これはbyte配列をそのまま受け取るようなことをしているのだと。これでめでたくデータ受信ができました。

では私はなぜApple原理主義者としての矜持を捨ててまでWindowsでプログラムを書き、RFIDリーダーと格闘しているか。その理由についてはいつかお話する機会もあろうかとおもいます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
//Sobal社製HS-1000Mを接続し、C#でシリアル通信を行うためのサンプル
//説明に必要な部分しか掲載していません。
namespace SomeName
{
class CardReader
{
private int commNo;
//使用するDLLの指定宣言;アンマネージドコードのため、CallingConventionがないと正常に動作しない
[DllImport("rfidsobal.dll", CallingConvention = CallingConvention.Cdecl)]
//デバイスオープン用の関数
extern static int so_comm_setup(System.IntPtr pWnd, int iType, int iPort, long lSpeed);
//カード読み取り要求送信用の関数
[DllImport("rfidsobal.dll", CallingConvention = CallingConvention.Cdecl)]
extern static int so_send(int iCommNo, string pcCmd, int iLen);
//カード読み取り結果受信用の関数
[DllImport("rfidsobal.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
extern static int so_recv(int iCommNo, IntPtr pcCmd, ushort usTimeOut);
//デバイスをオープンする
public void openDevice()
{
commNo = so_comm_setup(IntPtr.Zero, 1, 3, 115200L);
Console.Write("result = {0}", commNo);
}
//カード読み取り要求を送信。
private void sendRequest()
{
byte[] message = new byte[1024];
message[0] = 0x53;
message[4] = 0x20;
message[6] = 0x02;
message[8] = 0x20;
message[9] = 0x14;
message[10] = 0x01;
message[11] = 0x28;
message[12] = 0x08;
message[13] = 0x58;
message[16] = 0x01;
message[18] = 0x33;
int result = so_send(commNo, Encoding.ASCII.GetString(message), 19);
}
//読み取り結果の取得
private void checkResult()
{
byte[] message = new byte[1024];
//ここをlpBufferの代わりにStringBuilderを使うと正常に読み込めない
IntPtr lpBuffer = Marshal.AllocHGlobal(1024);
//resultには読み込んだバイト数が入っている(らしい)
int result = so_recv(commNo, lpBuffer, 1000);
if (result == 0) return;
byte[] byteData = new byte[result];
for (int i = 0; i < result; i++)
{
byteData[i] = Marshal.ReadByte(lpBuffer, i);
}
//取得したbyte[]の解析。内容は省略
this.analyzeResult(byteData);
Marshal.FreeHGlobal(lpBuffer);
lpBuffer = IntPtr.Zero;
}
}
}
view raw gistfile1.txt hosted with ❤ by GitHub