Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2018年06月09日] [更新 2018年07月08日] Delphi 一般・その他
バイト配列と文字または文字列
動作確認等 Windows 7 U64(SP1) + Delphi XE5(UP2) Pro VCL-32
[整理番号 - 0780]







バイト配列と Char 型の文字または String 型の文字列


文字列のデータをバイト配列として、バイト単位で操作するには TBinaryReader や TBinaryWriter あるいは TStringStream を使用すると便利です。これらを利用するとバイトデータを文字あいは文字列へ変換する操作が容易になります。
本ページは主にそれらのメソッド類のオンラインヘルプへのリンク集と、バイト配列の操作に関係したサンプルの記事となっていす。
以下の記事にそれらのメソッド類を使用したサンプルがあります。



[備考 1]
文字列のバイト長は ByteLength 関数で取得できます。

[備考 2]
バイト単位での操作が必要な場合、TMemoryStream よりも TBytesStream の方が便利です。
TBytesSteam クラスは TMemorySteam の継承クラスで、TMemorySteam と TBytes の両方の機能を実装したクラスです。
TStringStream は、TBytesStream に更に、文字列を扱うメソッドやプロパティを実装したクラスです。文字列専用のストリームというわけではありません。


[備考 3]
MECSUtils には Null 終端文字を考慮した TBytes ルーチンがあります。







01_TBinaryReader でバイト配列の値を文字列として取得


TBinaryReader クラスを使用すると、TFileStream で読み込んだディスク上のファイルのテータ、あるいは TMemoryStream や TBytes のバイトデータをバイト配列として扱えます。

TBinaryReader のインスタンスに格納されたバイト配列のデータは、Char 型の 1 文字、Char 型の文字配列や String 型の文字列、あるいは数値として取得できます。バイト配列の値を String 型の変数に格納すると、そのデータを String 型の文字列として扱えるようになります。
格納先 (変換後の) データの型に応じたメソッドが用意されています。

文字や文字列を扱う場合は、Create メソッドでインスタンスを生成する時に TEncoding.Unicode を指定します。






02_TBinaryWriter で文字列のデータをバイト配列に格納


TBinaryWriter クラスを使用すると、文字列や数値等をバイトデータとしてディスクに書き込みことができます。書き込み先を TMemoryStream や TBytes にすることもできます。

TBinaryWriter クラスは、Char 型の 1 文字、Char 型の文字配列、String 型の文字列、あるいは数値等のデータをバイト配列に格納できます。バイト配列に格納すると、文字列等のデータをバイト単位で操作できるようになります。
実際に TBinaryWriter のバイト配列に格納するには Write メソッドを使用します。

文字や文字列を扱う場合は、Create メソッドでインスタンスを生成する時に TEncoding.Unicode を指定します。






03_バイト配列としての操作も可能な TStringStream クラス


TStringStream クラスを使用すれば、WriteString メソッドで文字列のデータをバイト配列へ書き込むことができます。ReadString メソッドあるいは DataString プロパティでバイトデータを文字列として取得できます。Bytes (TBytes 型) プロパティを使用すればバイト単位の操作も可能です。
ReadData メソッドを使用すれば数値等も扱えます。



ネット上のコミュニティの場では TStringStream を使用した Char 型の文字配列や String 型の処理に関する質問が多いようです。以下はそれらの参考になるかも知れないサンプルです。

TStringStream には Char 型の配列を直接操作するメソッドもプロパティもありませんが、以下のサンプルのように WriteBuffer や ReadBuffer メソッドで処理できます。Write メソッド、Read メソッドを使用しても同じです。



図1
Char 型の文字配列のデータを TStringStream に格納した結果を、再度別の Char 型の文字配列に格納

リスト1
//=============================================================================
//  TStringStreamのストリームへの文字書き込みと読み出しの例
//
//  Char 型の配列の文字をストリームに格納する
//  ストリームに格納されている複数の文字をChar型の配列に格納する
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LStrText    : string;
  LCharArr    : TCharArray;
  LCharArrOut : TCharArray;
  LStrSream   : TStringStream;
  LSize       : Integer;
  LResText    : string;
begin
  //テスト用のChar型の文字配列作成
  LStrText :='日本語 ABC 文字列';
  LCharArr := LStrText.ToCharArray;

  LStrSream := TStringStream.Create;
  try
    //ストリームのサイズを設定してChar型の配列の文字を書き込む
    LSize := Length(LCharArr) * SizeOf(Char);
    LStrSream.Size := LSize;
    LStrSream.WriteBuffer(LCharArr[0], LSize);


    //コピー先のChar型配列のサイズを設定
    SetLength(LCharArrOut, LStrSream.Size div SizeOf(Char));

    //ストリームからChar型の文字配列に文字を読み込む
    LStrSream.Position := 0;
    LStrSream.ReadBuffer(LCharArrOut[0], LStrSream.Size);
  finally
    FreeAndNil(LStrSream);
  end;

  LResText := String(LCharArrOut);
  Memo1.Lines.Add(LResText);
end;


上のサンプルでは、ストリームに格納されている文字列のデータを、新規の Char 型の文字配列に代入しています。
上のコードで、出力の Char 型の文字配列に文字をセットする部分を次のように変更すると、既に文字が格納されている Char 型の文字配列に追加して代入することができます。このコードは、サイズを設定した Char 型の配列に文字を代入します。その Char 型の配列のサイズを拡張してから、その拡張した要素にストリームから文字データを読み込みます。



図2
既存の Char 型配列にストリームの文字列を追加

リスト2
    //Char型の配列のサイズを設定して文字をセット
    SetLength(LCharArrOut, 3);
    LCharArrOut[0] := '東';
    LCharArrOut[1] := '京';
    LCharArrOut[2] := '都';

    //そのChar型配列のサイズを再設定
    SetLength(LCharArrOut, Length(LCharArrOut) + LStrSream.Size div SizeOf(Char));

    //ストリームからChar型の文字配列に文字を読み込む
    LStrSream.Position := 0;
    LStrSream.ReadBuffer(LCharArrOut[3], LStrSream.Size);


TStringStream には Bytes プロパティがあり、ストリームの任意の位置からの読み書きが可能です。それを使用すると以下のように MoveMemory または CopyMemory で処理できます。



リスト3
//=============================================================================
//  TStringStreamのストリームへの文字書き込みと読み出しの例
//
//  Char 型の配列の文字をストリームに格納する
//  ストリームに格納されている複数の文字をChar型の配列に格納する
//
//  MoveMemoryまたはCopyMemoryを使用する方法
//  Bytesプロパティを使用すると任意の位置からの読み書きが可能
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LStrText    : string;
  LCharArr    : TCharArray;
  LCharArrOut : TCharArray;
  LStrSream   : TStringStream;
  LSize       : Integer;
  LResText    : string;
begin
  //テスト用のChar型の文字配列作成
  LStrText :='日本語 ABC 文字列';
  LCharArr := LStrText.ToCharArray;

  LStrSream := TStringStream.Create;
  try
    //ストリームのサイズを設定して文字を書き込む
    LSize := Length(LCharArr) * SizeOf(Char);
    LStrSream.Size := LSize;
    //以下でも同じ
   //MoveMemory(@LStrSream.Bytes[0], @LCharArr[0], LSize);
   CopyMemory(@LStrSream.Bytes[0], @LCharArr[0], LSize);


    //コピー先のChar型配列のサイズを設定
    SetLength(LCharArrOut, LStrSream.Size div SizeOf(Char));

    //メモリの内容をコピー
    //以下でも同じ
    //MoveMemory(@LCharArrOut[0], @LStrSream.Bytes[0],  LStrSream.Size);
    CopyMemory(@LCharArrOut[0], @LStrSream.Bytes[0],  LStrSream.Size);
  finally
    FreeAndNil(LStrSream);
  end;

  LResText := String(LCharArrOut);
  Memo1.Lines.Add(LResText);
  Memo1.Lines.Add(IntToStr(Length(LCharArrOut)));
  Memo1.Lines.Add(IntToStr(LResText.Length));
end;


文字だけを格納した TStringStream から String 型の文字列の変数に文字列として格納することもできますが、String 型の最初の文字は要素番号が 1 であることに注意します。
以下のコードは CopyMemory 関数を使用する例です。



リスト4
//=============================================================================
//  TStringStreamのストリームへの文字書き込みと読み出しの例
//
//  Char 型の配列の文字をストリームに格納する
//  ストリームに格納されている複数の文字をString型の文字列に格納する
//
//  CopyMemoryを使用する方法
//  Bytesプロパティを使用すると任意の位置からの読み書きが可能
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LStrText  : string;
  LCharArr  : TCharArray;
  LStrSream : TStringStream;
  LSize     : Integer;
  LResText  : string;
begin
  //テスト用のChar型の文字配列作成
  LStrText :='日本語 ABC 文字列';
  LCharArr := LStrText.ToCharArray;

  LStrSream := TStringStream.Create;
  try
    //ストリームのサイズを設定して文字を書き込む
    LSize := Length(LCharArr) * SizeOf(Char);
    LStrSream.Size := LSize;
    CopyMemory(@LStrSream.Bytes[0], @LCharArr[0], LSize);


    //コピー先のString型のサイズを設定
    SetLength(LResText, LStrSream.Size div SizeOf(Char));
    //メモリの内容をコピー
    CopyMemory(@LResText[1], @LStrSream.Bytes[0], LStrSream.Size);
  finally
    FreeAndNil(LStrSream);
  end;

  Memo1.Lines.Add(LResText);
end;


この String 型の文字列を取り出す時の要素番号の注意は、ReadBuffer メソッドあるいは Read メソッドを使用する時も同じです。
以下は ReadBuffer または Read メソッドを使用して TStringStream に格納されている文字を String 型の文字列として取得する例です。
上のサンプルコードの、文字列を取得する部分を以下のコードに変更すれば動作確認できます。


リスト5
    //取り出す文字列のサイズを設定
    SetLength(LResText, LStrSream.Size div SizeOf(Char));

    //TStringStreamから文字列として取得
    LStrSream.Position := 0;
    LStrSream.ReadBuffer(LResText[1], LStrSream.Size);

リスト6
    //取り出す文字列のサイズを設定
    SetLength(LResText, LStrSream.Size div SizeOf(Char));

    //TStringStreamから文字列として取得
    LStrSream.Position := 0;
    LStrSream.Read(LResText[1], LStrSream.Size);

TStringStream には ReadString メソッドあるいは DataString プロパティがあります。これらを使用すると、バイトデータを String 型の文字列として取得できます。ただしこの場合、TStringStream のインスタンスを生成する時に以下のようにエンコードを指定する必要があります。そうしないと文字化けします。


  LStrSream := TStringStream.Create('', TEncoding.Unicode);


ネット上のコミュニティの場で、TStringStream に格納されているデータを PChar 型の文字列として取得する方法の質問を見かけました。
ReadBuffer メソッド等で直接 PChar 型の文字列として取得するには Null 終端文字分も含めたサイズがないと、文字列の末尾に「ゴミ」が表示された文字化け状態になります。以下は Null 終端の文字分のメモリを確保して処理するコードです。

TStringStream のデータを String 型の文字列として取得してから、それを PChar の文字列に変換すれば、Null 文字がなくても PChar 型の文字列にできます。



図3
ReadBuffer メソッドで PChar 型の文字列に変換
  • Null 終端文字分のメモリを確保した場合
図4
ReadBuffer メソッドで PChar 型の文字列に変換
  • Null 終端文字分のメモリを確保しなかった場合

リスト7
//=============================================================================
//  TStringStreamのストリームへの文字書き込みと読み出しの例
//
//  PChar型の文字列として取り出すにはNull終端文字が必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LStrText  : string;
  LPChar    : PChar;
  LPCharOut : PChar;
  LStrSream : TStringStream;
  LResText  : string;
  LChar     : Char;
begin
  LStrText :='日本語 ABC 文字列';
  LPChar   := PChar(LStrText);

  LStrSream := TStringStream.Create;
  try
    //TStringSteamにPChar型の文字列のデータを書き込む
    LStrSream.WriteBuffer(LPChar^, StrLen(LPChar) * SizeOf(Char));


    //コピー先のPChar型の文字列用のメモリを確保
    //Null終端文字分も確保
    LPCharOut := AllocMem(LStrSream.Size + SizeOf(Char));
    try
      //TStringStreamのテータをPChar型の文字列のデータとして取得
      LStrSream.Position := 0;
      LStrSream.ReadBuffer(LPCharOut^, LStrSream.Size);
      LResText := LPCharOut;
    finally
      FreeMem(LPCharOut);
    end;
  finally
    FreeAndNil(LStrSream);
  end;

  Memo1.Lines.Add(LResText);
end;




04_BytesOf によるバイト配列への書き込みと StringOf による読出し


BytesOf 関数は文字列のデータをバイト配列に格納できます。また、StringOf 関数を使用するとバイトデータから String 型の文字列データを取得できます。
以下は実際の使用例です。Button2 クリックのコードは、バイト配列のデータを TStringStream に読み込んで、その Bytes プロパティの値を StringOf 関数で文字列にしています。



図5
実行結果

リスト8
//=============================================================================
//  BytesOfを使用して文字列をバイト配列に書き込む
//  StringOfを使用してバイト配列から文字列を取得
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LStrText  : string;
  LBytes    : TBytes;
  LResText  : string;
begin
  LStrText :='日本語 BytesOf StringOf 文字列';
  //String型の文字列のデータをバイト配列に格納
  LBytes := BytesOf(LStrText);

  //バイト配列からString型の文字列を取り出す
  LResText := StringOf(LBytes);

  Memo1.Lines.Add(LResText);
end;

//=============================================================================
//  BytesOfを使用して文字列をバイト配列に書き込む
//  StringOfを使用してTStringStreamのバイト配列から文字列を取得
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LStrText   : string;
  LBytes     : TBytes;
  LStrStream : TStringStream;
  LResText   : string;
begin
  LStrText :='日本語 BytesOf StringOf 文字列';
  //String型の文字列のデータをバイト配列に格納
  LBytes := BytesOf(LStrText);


  //TStringStreamのインスタンスを生成
  //生成時にバイト配列を読み込む
  //このバイト配列にはString型の文字列のデータが格納されている
  LStrStream := TStringStream.Create(LBytes);
  try
    //バイト配列からString型の文字列を取り出す
    LResText := StringOf(LStrStream.Bytes);

    Memo1.Lines.Add(LResText);
  finally
    FreeAndNil(LStrStream);
  end;
end;




05_TEncoding によるバイト配列と Char 型配列と String 型文字列


TEncoding クラスにもバイト配列を扱うためのメソッド類が実装されています。TEncoding クラスを使用すると文字や文字列をバイト配列に格納する際、コードページを指定てきます。

次の最初のサンプルコードでは、TEncoding.GetBytes で String 型の文字列をバイト配列に格納します。そして、そのバイト配列のデータを、TEncoding.GetString メソッドを使用して String 型の文字列に格納して、TEncoding.GetChars メソッドで Char 型の文字配列に格納します。

次のコードは Char 型の文字配列のデータをバイト配列に格納します。そして、そのバイト配列のデータを String 型の文字列と Char 型の文字配列に格納します。

最後の Button3 クリックのコードは、Unicode (UTF-16) ではなく UTF-8 で処理する例です。



図6
実行結果

リスト9
//=============================================================================
//  TEncodingを使用したバイト配列、Char型の配列、String型の文字列の操作
//
//  String型の文字列をバイト配列に格納
//  バイト配列のデータをString型の文字列として取得
//  バイト配列のデータをChar型の文字配列に取得
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LStrText    : string;
  LCharArrOut : TCharArray;
  LBytes      : TBytes;
  LResText    : string;
begin
  //テスト用の文字列
  LStrText :='日本語 TEncoding 文字列';

  //String型の文字列をバイト配列に格納
  LBytes := TEncoding.Unicode.GetBytes(LStrText);

  //バイト配列のデータをString型の文字列とChar型の文字配列に取得
  LResText    := TEncoding.Unicode.GetString(LBytes);
  LCharArrOut := TEncoding.Unicode.GetChars(LBytes);

  Memo1.Lines.Add(LResText);
  Memo1.Lines.Add(string(LCharArrOut));
end;

//=============================================================================
//  TEncodingを使用したバイト配列、Char型の配列、String型の文字列の操作
//
//  Char型の文字配列のデータをバイト配列に格納
//  バイト配列のデータをString型の文字列として取得
//  バイト配列のデータをChar型の文字配列に取得
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LStrText    : string;
  LCharArr    : TCharArray;
  LCharArrOut : TCharArray;
  LBytes      : TBytes;
  LResText    : string;
begin
  //テスト用のChar型の文字配列作成
  LStrText :='日本語 TEncoding 文字列';
  LCharArr := LStrText.ToCharArray;

  //Char型の文字配列の値をバイト配列に格納
  LBytes := TEncoding.Unicode.GetBytes(LCharArr);

  //バイト配列のデータをString型の文字列とChar型の文字配列に取得
  LResText    := TEncoding.Unicode.GetString(LBytes);
  LCharArrOut := TEncoding.Unicode.GetChars(LBytes);

  Memo1.Lines.Add(LResText);
  Memo1.Lines.Add(string(LCharArrOut));
end;

//=============================================================================
//  TEncodingを使用したバイト配列、Char型の配列、String型の文字列の操作
//
//  Char型の文字配列のデータをバイト配列に格納
//  バイト配列のデータをString型の文字列として取得
//  バイト配列のデータをChar型の文字配列に取得
//
//  コードページをUTF-8で扱う場合
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  LStrText    : string;
  LCharArr    : TCharArray;
  LCharArrOut : TCharArray;
  LBytes      : TBytes;
  LResText    : string;
begin
  //テスト用のChar型の文字配列作成
  LStrText :='日本語 TEncoding 文字列';
  LCharArr := LStrText.ToCharArray;

  //Char型の文字配列の値をバイト配列に格納
  LBytes := TEncoding.UTF8.GetBytes(LCharArr);

  //バイト配列のデータをString型の文字列とChar型の文字配列に取得
  LResText    := TEncoding.UTF8.GetString(LBytes);
  LCharArrOut := TEncoding.UTF8.GetChars(LBytes);

  Memo1.Lines.Add(LResText);
  Memo1.Lines.Add(string(LCharArrout));
end;


次のサンプルは TEncoding.GetBytes で Char 型の文字配列のデータをバイト配列に格納した後、そのバイト配列のデータを TStringStream に書き込みます。そして、そのデータを ReadBuffer メソッドで String 型の文字列のデータとして取り出します。



リスト10
//=============================================================================
//  TEncodingを使用したバイト配列、Char型の配列、String型の文字列の操作
//
//  Char型の文字配列のデータをバイト配列に格納
//  バイト配列のデータをString型の文字列として取得(TStringStream使用)
//
//  TStringStream.ReadBufferメソッド使用
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LStrText   : string;
  LCharArr   : TCharArray;
  LBytes     : TBytes;
  LStrStream : TStringStream;
  LResText   : string;
begin
  //テスト用のChar型の文字配列作成
  LStrText :='日本語 TEncoding 文字列';
  LCharArr := LStrText.ToCharArray;

  //Char型の文字配列の値をバイト配列に格納
  LBytes := TEncoding.Unicode.GetBytes(LCharArr);


  //TStringStreamのインスタンスを生成
  LStrStream := TStringStream.Create;
  try
    //TStringStreamにバイト配列を書き込む
    //バイト配列には文字データが格納されている
    LStrStream.WriteBuffer(LBytes, Length(LBytes));

    //読み込むバッファとしての文字列の長さを設定
    SetLength(LResText, LStrStream.Size div SizeOf(Char));
    //バイト配列のデータをString型の文字列として取得
    LStrStream.Position := 0;
    LStrStream.ReadBuffer(LResText[1], LStrStream.Size);
    Memo1.Lines.Add(LResText);
  finally
    FreeAndNil(LStrStream);
  end;
end;


TStringStream クラスには string 型の値を取得する DataString プロパティ、ReadStringメソッドがあります。上のサンプルで文字列としてのデータを取得する部分でこれらを使用する場合は Unicode へのエンコードが必要です。エンコードは TStringStream のインスタンス生成時に設定できまます。以下は実際の例です。




図7
実行結果
図8
エンコードの指定がない時の実行結果

リスト11
//=============================================================================
//  TEncodingを使用したバイト配列、Char型の配列、String型の文字列の操作
//
//  String型の文字データをバイト配列に格納
//  バイト配列のデータをString型の文字列として取得
//
//  TStringStream.DataStringプロパティ使用
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LStrText   : string;
  LBytes     : TBytes;
  LStrStream : TStringStream;
  LResText   : string;
begin
  //テスト用の文字列
  LStrText :='日本語 TEncoding 文字列';


  //String型の文字列をバイト配列に格納
  LBytes := TEncoding.Unicode.GetBytes(LStrText);

  //エンコードを指定してTStringStreamのインスタンスを生成
  LStrStream := TStringStream.Create('', TEncoding.Unicode);
  try
    //バイト配列をTStringStreamに書き込む
    //このバイト配列にはString型の文字列のデータが格納されている
    LStrStream.WriteBuffer(LBytes, Length(LBytes));

    //バイト配列のデータをString型の文字列として取得
    LResText := LStrStream.DataString;
    Memo1.Lines.Add(LResText);
  finally
    FreeAndNil(LStrStream);
  end;
end;

//=============================================================================
//  TEncodingを使用したバイト配列、Char型の配列、String型の文字列の操作
//
//  Char型の文字配列のデータをバイト配列に格納
//  バイト配列のデータをString型の文字列として取得(TStringStream使用)
//
//  TStringStream.ReadStringメソッド使用
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LStrText   : string;
  LCharArr   : TCharArray;
  LBytes     : TBytes;
  LStrStream : TStringStream;
  LResText   : string;
begin
  //テスト用のChar型の文字配列作成
  LStrText :='日本語 TEncoding 文字列';
  LCharArr := LStrText.ToCharArray;

  //Char型の文字配列の値をバイト配列に格納
  LBytes := TEncoding.Unicode.GetBytes(LCharArr);


  //TStringStreamのインスタンスを生成
  LStrStream := TStringStream.Create('', TEncoding.Unicode);
  try
    //TStringStreamにバイト配列を書き込む
    //バイト配列には文字データが格納されている
    LStrStream.WriteBuffer(LBytes, Length(LBytes));

    //バイト配列のデータをString型の文字列として取得
    LStrStream.Position := 0;
    LResText := LStrStream.ReadString(LStrStream.Size);
    Memo1.Lines.Add(LResText);
  finally
    FreeAndNil(LStrStream);
  end;
end;




06_DLL の関数の引数にバイト配列のデータを使用する例


DLL 内の関数等にバイト配列のデータを渡したり、バイト配列のデータを受け取る例です。
下のコードがテスト用に作成した DLL のコードです。
自分でテスト用に作成する関数であればその動作仕様は自由に設計できます。しかし、Windows API の関数等では、実際に使用する関数内の処理を知ることはできません。その関数の動作、引数や戻り値の仕様を把握しなければ適切な利用は望めません。



リスト12
library DllTest_Bytes;

uses
  Winapi.Windows,
  System.SysUtils,
  System.Classes;

// -----------------------------------------------------------------------------
// 呼び出し側でセットしたバイト配列データを引数から取得する例
// -----------------------------------------------------------------------------
procedure SetBytesData(ABuff: Pointer; ALen: Integer); stdcall;
var
  LBStream : TBytesStream;
  LText    : string;
  LIndex   : Integer;
begin
  LBStream := TBytesStream.Create;
  try
    LBStream.Write(ABuff, ALen);

    LText := '';
    for LIndex := 0 to ALen - 1 do begin
      LText := LText + IntToHex(LBStream.Bytes[LIndex], 2);
    end;

    MessageBox(0, PChar(LText), '結果', MB_ICONINFORMATION);
  finally
    FreeAndNil(LBStream);
  end;
end;

// -----------------------------------------------------------------------------
// DLL内のバイト配列のデータを呼び出し側に渡す例
// 引数をnilにすると必要なバッファの確保サイズの取得となる
//
//  戻り値はバイト数
// -----------------------------------------------------------------------------
function GetBytesData(ABuff: Pointer): DWORD; stdcall;
begin
  if ABuff <> nil then begin
    PByteArray(ABuff)^[0] := Byte($01);
    PByteArray(ABuff)^[1] := Byte($02);
    PByteArray(ABuff)^[2] := Byte($03);
    PByteArray(ABuff)^[3] := Byte($04);
  end;

  Result := 4;
end;

// -----------------------------------------------------------------------------
//  exports宣言
// -----------------------------------------------------------------------------
exports
  SetBytesData,
  GetBytesData;

end.


上の DLL 内の関数を使用する例です。

Button1 クリックのコードは DLL の関数にバイトデータを渡しますが、バイト配列の 12 番目 (インデックスの番号は 11) から関数に渡しています。上の DLL のコードでは、このデータを最初のバイトデータとして取得するようになっています。
この例のように、バイト配列のデータは、任意の要素から関数に渡したり、任意の要素からのデータとして取得するようにができます。

この Button1 クリックのコードで使用している TByteArray は静的なバイト配列の型です。




図9
DLL の関数にバイトデータを渡した結果
  • このダイアログは DLL 内で表示している
  • 表示しているのは 16進数の文字列
図10
DLL の関数から取得したバイトデータを表示

リスト13
implementation

{$R *.dfm}

procedure SetBytesData(ABuf: Pointer; ALen: Integer);
  stdcall; external 'DllTest_Bytes.dll';
function GetBytesData(ABuf: Pointer): DWORD;
  stdcall; external 'DllTest_Bytes.dll';

//=================================================================
//  DLL内の関数にバイトデータを渡す例
//  TByteArray型のデータとして渡す例
//=================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LBytesArr : TByteArray;
begin
  Memo1.Lines.Clear;

  //テスト用データを作成
  LBytesArr[11] := Byte($50);
  LBytesArr[12] := Byte($60);
  LBytesArr[13] := Byte($70);

  //関数を実行してバイトデータを渡す
  SetBytesData(@LBytesArr[11], 3);
end;

//=================================================================
//  DLL内の関数にバイトデータを渡す例
//  TBytesStream型の配列データを渡す例
//=================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LBytesStream : TBytesStream;
  LBytesSize   : Integer;
begin
  Memo1.Lines.Clear;

  LBytesStream := TBytesStream.Create;
  try
    //テスト用データを作成
    LBytesSize := 3;
    LBytesStream.Size := LBytesSize;
    LBytesStream.Bytes[0] := Byte($50);
    LBytesStream.Bytes[1] := Byte($60);
    LBytesStream.Bytes[2] := Byte($70);

    //関数を実行してバイトデータを渡す
    SetBytesData(@LBytesStream.Bytes[0], LBytesSize);
  finally
    FreeAndNil(LBytesStream);
  end;
end;

//=================================================================
//  DLL内の関数からバイト配列のデータを取得する例
//  バイトデータをTByteArrayで受け取る例
//=================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  LByteArr  : TByteArray;
  LBytesCnt : DWORD;
  LText     : string;
  LIndex    : Integer;
begin
  Memo1.Lines.Clear;

  //関数を実行してバイトデータを取得
  LBytesCnt := GetBytesData(@LByteArr[0]);

  //取得したデータを16進数の文字列で表示
  for LIndex := 0 to LBytesCnt - 1 do begin
    LText := IntToHex(LByteArr[LIndex], 2);
    Memo1.Lines.Add(LText);
  end;
end;


DLL 内の関数からバイトデータを取得する際、まず最初にメモリ確保に必要なバイト数を取得してから実際のデータを取得する例です。
この例では TBytes のバイト配列でデータを受け取るようにしています。



リスト14
//=================================================================
//  DLL内の関数からバイト配列のデータを取得する例
//  バイトデータをTBytesで受け取る例
//=================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  LByteArr  : TBytes;
  LBytesCnt : DWORD;
  LText     : string;
  LIndex    : Integer;
begin
  Memo1.Lines.Clear;

  //必要なバッファサイズの取得
  LBytesCnt := GetBytesData(nil);

  if LBytesCnt > 0 then begin
    //受け取るバイト配列のサイズを設定
    SetLength(LByteArr, LBytesCnt);

    //関数を実行してバイトデータを取得
    GetBytesData(@LByteArr[0]);

    //取得したデータを16進数の文字列で表示
    for LIndex := Low(LByteArr) to High(LByteArr) do begin
      LText := IntToHex(LByteArr[LIndex], 2);
      Memo1.Lines.Add(LText);
    end;
  end;
end;


以下も、DLL 内の関数からバイトデータを取得する際、まず最初にメモリ確保に必要なバイト数を取得してから実際のデータを取得する例です。
この例では TBytesStream でデータを受け取るようにしています。



リスト15
//=================================================================
//  DLL内の関数からバイト配列のデータを取得する例
//  バイトデータをTBytesStreamで受け取る例
//=================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  LBytesStream : TBytesStream;
  LBytesCnt    : DWORD;
  LText        : string;
  LIndex       : Integer;
begin
  Memo1.Lines.Clear;

  LBytesStream := TBytesStream.Create;
  try
    //必要なバッファサイズの取得
    LBytesCnt := GetBytesData(nil);

    if LBytesCnt > 0 then begin
      //受け取るバイト配列のサイズを設定
      LBytesStream.SetSize(LBytesCnt);

      //関数を実行してバイトデータを取得
      GetBytesData(@LBytesStream.Bytes[0]);

      //取得したデータを16進数の文字列で表示
      for LIndex := 0 to LBytesCnt - 1 do begin
        LText := IntToHex(LBytesStream.Bytes[LIndex], 2);
        Memo1.Lines.Add(LText);
      end;
    end;
  finally
    FreeAndNil(LBytesStream);
  end;
end;


以下のように、受け取るバイト配列のサイズ、つまりバッファとして十分なサイズを確保しておけば、必要なバッファサイズの取得操作は必要ありません。

このサンプルでは TStringStream でバイトデータを受け取っています。TStringStream にバイトデータを格納すると、そのバイトデータが文字列のデータであった場合、TStringStream のメソッドやプロパティを使用して文字列に変換できます。



リスト16
//=================================================================
//  DLL内の関数からバイト配列のデータを取得する例
//  十分なバッファサイズを設定しておく例
//  バッファサイズが十分であればバッファサイズの取得操作は不要
//=================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  LStrStream : TStringStream;
  LBytesCnt  : DWORD;
  LText      : string;
  LIndex     : Integer;
begin
  Memo1.Lines.Clear;

  LStrStream := TStringStream.Create;
  try
    //受け取るバイト配列のサイズを設定
    LStrStream.SetSize(High(Word) div 2);

    //関数を実行してバイトデータを取得
    LBytesCnt := GetBytesData(@LStrStream.Bytes[0]);

    //取得したデータを16進数の文字列で表示
    for LIndex := 0 to LBytesCnt - 1 do begin
      LText := IntToHex(LStrStream.Bytes[LIndex], 2);
      Memo1.Lines.Add(LText);
    end;
  finally
    FreeAndNil(LStrStream);
  end;
end;


上のサンプルでは、TByteArray, TBytes, TBytesStream, TStringStream を使用して関数からバイトデータを取得しました。
このように、ポインタとしてデータを受け取る時は、基本的には任意のクラス型のデータとしても取得できます。ただし、上のサンプルで扱っているのは単なるバイトデータです。したがって、バイト配列の機能がないクラス型で取得しようとすると例外が発生します。

例えば以下のようなコードの DLL を作成します。これは TBitmap を操作するコードです。



リスト17
library DllTest_Bitmap;

uses
  Winapi.Windows,
  Vcl.Graphics;

function OperateBitmap(ABuff: Pointer): Boolean; stdcall;
var
  LRect : TRect;
begin
  Result := True;

  TBitmap(ABuff).SetSize(100, 100);

  LRect := TBitmap(ABuff).Canvas.ClipRect;
  TBitmap(ABuff).Canvas.Brush.Color := clGreen;
  TBitmap(ABuff).Canvas.FillRect(LRect);
end;

exports
  OperateBitmap;

end.


上の DLL 内の関数を利用するコードを図の下のコードとします。実行すると下図のようになります。
DLL に渡したオブジェクトが DLL 内で動作していることが確認できます。

このサンプルの DLL の関数の引数は、上のバイト配列のデータのサンプルと同じ汎用的な Pointer 型です。しかし、処理内容は全く違います。このように、DLL 内の関数を使用する時は、その関数の動作、引数や戻り値の仕様を把握しなければ適切な結果を取得することはできないと言えます。



[備考]
この DLL の関数は、引数を TBitmap にしないと正常動作しません。したがって、Delphi 専用ということになり、汎用的とは言えません。
DLL にビットマップのオブジェクトを渡したり受け取ったりするには、一般的にはそのビットマップハンドルの値を使用します。




図11
EXE の起動開始画面
図12
DLL 内の関数を実行した結果

リスト18
implementation

{$R *.dfm}

function OperateBitmap(ABuf: Pointer): Boolean;
  stdcall; external 'DllTest_Bitmap.dll';


procedure TForm1.Button1Click(Sender: TObject);
var
  LBitmap : TBitmap;
begin
  LBitmap := TBitmap.Create;
  try
    OperateBitmap(LBitmap);
    Self.Canvas.Draw(8, 32, LBitmap);
  finally
    FreeAndNil(LBitmap);
  end;
end;