Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2019年09月07日] [更新 2019年09月19日] Delphi 一般・その他
Windows API 等、DLL の関数の配列型引数
動作確認等 Windows 7 U64(SP1) + Delphi XE5(UP2) Pro VCL-32
[整理番号 - 0700]







Windows API 等、DLL 内の関数の配列型引数


数値型の配列の引数が、ポインタ型である DLL 内の配列処理用の関数を使用する時、あるいは、DLL に組み込む汎用的な配列処理用の関数を Delphi で作成する際の参考記事です。

次のような引数の定義の関数があったとします。 これは、引数が整数型と実数型の要素を持つ 1 次元の配列で、要素数が固定の配列を意味しています。


BOOL test_conv(
  int   src[10],
  float dst1[5],
  float dst2[5]
);

要素数が固定でなければ以下の定義と同じです。つまり、ポインタ型です。
上の記述では処理する要素数が固定の関数ですが、要素数が固定でない場合は、通常は要素数を指定する引数があります。要素数を指定する引数がない場合、一般的には、関数内部で配列の要素数を知ることはできません。


BOOL test_conv(
  int   *src,
  float *dst1,
  float *dst2
);


この関数は Delphi には実装されていないのですから、Delphi には実装されていないWindows API の関数を利用とする時と同じように、使用する関数を宣言します。


// 汎用的なポインタ型で定義
function test_conv(src: Pointer; dst1: Pointer; dst2: Pointer): Boolean;
  stdcall; external 'dll_testarray.dll';


配列の変数は例えば以下のようにします。型を定義して使用することもできます。


// 1 次元の静的配列を使用する場合
var
  Lsrc_dat  : array [0..9] of Integer;
  Ldst1_dat : array [0..4] of Single;
  Ldst2_dat : array [0..4] of Single;


// 1 次元の動的配列を使用する場合
var
  Lsrc_dat  : array of Integer;
  Ldst1_dat : array of Single;
  Ldst2_dat : array of Single;

  // 動的配列は実行時に要素の設定が必要
  SetLength(Lsrc_dat, 10);
  SetLength(Ldst1_dat, 5);
  SetLength(Ldst2_dat, 5);


関数の実行部分は次のようになります。


  // 関数を実行
  // 配列の最初の要素のアドレスを渡す
  test_conv(@Lsrc_dat[0], @Ldst1_dat[0], @Ldst2_dat[0]);



[備考 1]
Fortran (フォートラン) は数値計算専用のプログラム言語です。Fortran で作成された 行列演算、数値計算用の DLL ライブラリもあります。Fortran で扱う行列は、C++, Delphi が扱う配列の行と列が逆となります。Fortran で作成されたライブラリを C++, Delphi で使用する場合は、転置行列を作成する必要があるかも知れません。
一般的に Fortran が生成するコードは高速です。


[備考 2]
要素数の多い配列を扱う場合、ローカル変数だとメモリの確保に失敗して、実行時にメモリアクセス違反の例外が発生することがあります。その場合は配列変数をグローバル変数にします。





01_引数が 1 次元の静的配列の場合


1 次元の静的配列を処理する関数とその関数を使用する例です。
1 次元の静的配列の各要素のデータはメモリに連続して格納されます。ポインタをインクリメントすることでデータを順番に取得できます。本サンプルの関数の処理もそうなっています。
引数には配列要素の最初のデータのアドレスを渡します。静的配列の配列自身のアドレスと、最初の要素のデータのアドレスは同じです。したがって、以下のどちらのでも処理できます。


  // 以下のどちらでも構わない
  test_conv(5, @Lsrc_dat, @Ldst_dat);
  test_conv(5, @Lsrc_dat[0], @Ldst_dat[0]);


本ページのサンプルでは、引数のポインタ型として、汎用のポインタ型である Pointer を使用していますが、以下のように、数値の型に応じたポインタ型を使用することもできます。実際、C や C++ の関数ではほとんどそうなっています。


  function test_conv(ASize: Integer; ASrcArray, ADstArray:PSingle): Boolean;


図1
引数が 1 次元の静的配列の関数の実行結果

リスト1
1 次元の静的配列を処理する関数とその使用例
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

  function test_conv(ASize: Integer; ASrcArray, ADstArray:Pointer): Boolean;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  1 次元配列の引数をポインタで渡す例
//  静的配列の場合
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  Lsrc_dat : array [0..4] of Single;
  Ldst_dat : array [0..4] of Single;
  LIndex   : Integer;
begin
  Memo1.Lines.Clear;
  Memo1.Lines.BeginUpdate;

  for LIndex := Low(Lsrc_dat) to High(Lsrc_dat) do begin
    Lsrc_dat[LIndex]:=1000.0 + LIndex;
  end;

  // 関数を実行
  test_conv(5, @Lsrc_dat[0], @Ldst_dat[0]);

  // 関数の戻り値を表示
  for Lindex := Low(Ldst_dat) to High(Ldst_dat) do begin
    Memo1.Lines.Add(FloatToStr(Lsrc_dat[LIndex]) + '   '
                  + FloatToStr(Ldst_dat[LIndex]));
  end;

  Memo1.Lines.EndUpdate;
end;

//----------------------------------------------------------------------------
//  1 次元配列を引数に持つ関数の例
//  引数の配列が Single 型の静的配列と仮定した場合の処理例
//
//  Windows API の関数や他の DLL 内の関数の実際の処理は不明
//  Windoows API の関数は DLL で提供されている
//----------------------------------------------------------------------------
function test_conv(ASize: Integer; ASrcArray, ADstArray:Pointer): Boolean;
var
  LpSrc  : Single;
  Lindex : Integer;
begin
  Result := True;

  // 配列データを要素順に操作
  for Lindex := 0 to ASize - 1 do begin
    LpSrc := PSingle(ASrcArray)^;
    PSingle(ADstArray)^ := LpSrc * 2.0;

    // 次のデータのある場所 (ポインタ) に移動
    inc(PSingle(AsrcArray));
    inc(PSingle(ADstArray));
  end;
end;

end.


上のコードはポインタを連続的に移動して処理しているだけです 。配列に対して汎用的な処理を行うには、以下のように、任意の配列要素への代入や値の取得の機能が必要です。



リスト2
1 次元の静的配列を処理する関数の例。任意の配列要素へのアクセスを可能にする例
//----------------------------------------------------------------------------
//  指定要素の値を取得
//  引数の ArrayPtr は配列の最初の要素のポインタ
//----------------------------------------------------------------------------
function Get1DSingle(ArrayPtr: Pointer; AIndex: Integer): Single;
begin
  // 配列の先頭要素からポインタをインクリメント
  inc(PSingle(ArrayPtr), AIndex);
  // そのポインタにおける値を逆参照で取得
  Result := PSingle(ArrayPtr)^;
end;

//----------------------------------------------------------------------------
//  指定要素に値を代入
//----------------------------------------------------------------------------
procedure Set1DSingle(ArrayPtr: Pointer; AIndex: Integer; AValue: Single);
begin
  inc(PSingle(ArrayPtr), AIndex);
  PSingle(ArrayPtr)^ := AValue;
end;

//----------------------------------------------------------------------------
//  1 次元配列を処理する関数の例
//
//  (1) 引数の配列が Single 型の静的配列
//  (2) 配列の最初の要素のアドレスを引数にしている
//  と仮定した場合の処理例
//----------------------------------------------------------------------------
function test_conv(ASize: Integer; ASrcArray, ADstArray:Pointer): Boolean;
var
  LSrcValue : Single;
  Lindex    : Integer;
begin
  Result := True;

  for Lindex := 0 to ASize - 1 do begin
    LSrcValue := Get1DSingle(ASrcArray, Lindex);
    Set1DSingle(ADstArray, Lindex, LSrcValue * 2.0);
  end;
end;


Delphi では配列を型定義して使用することができます。Delphi の型定義の静的配列を使用した関数の処理例です。



リスト3
Delphi の型定義の静的配列を使用した、1 次元の静的配列を処理する関数の例
//----------------------------------------------------------------------------
//  1 次元配列を処理する関数の例
//
//  (1) 引数の配列が Single 型の静的配列
//  (2) 配列の最初の要素のアドレスを引数にしている
//  と仮定した場合の処理例
//----------------------------------------------------------------------------
function test_conv(ASize: Integer; ASrcArray, ADstArray:Pointer): Boolean;
type
  TStatArray = array [0..$7FFFFFF] of Single;
  PStatArray = ^TStatArray;
var
  LpSrcArray : PStatArray;
  LpDstArray : PStatArray;
  Lindex     : Integer;
begin
  Result := True;

  // 最初の要素のポインタを取得
  LpSrcArray := PStatArray(ASrcArray);
  LpDstArray := PStatArray(ADstArray);

  for Lindex := 0 to ASize - 1 do begin
    LpDstArray^[Lindex] := LpSrcArray^[Lindex] * 2.0;
  end;
end;




02_引数が 1 次元の動的配列の場合


1 次元の動的配列を処理する関数とその関数を使用する例です。
1 次元の静的配列を処理する引数であれば、1 次元の動的配列の処理にも使用できます。
動的配列の場合、配列自身のアドレスと最初の配列要素のデータのアドレスが違います。引数に、次のように、明示的に最初の要素のデータのアドレスを渡せば処理できます。


  // 最初の配列要素のアドレスを渡す
  test_conv(5, @Lsrc_dat[0], @Ldst_dat[0]);


[備考]
実行時に配列要素数を確保する配列を動的配列と言いますが、可変長配列、あるいは可変長サイズの配列と言うことももあります。


関数の内部で、引数の配列を動的配列として処理している場合、その配列引数に静的配列を渡しても正常に処理できません。通常は例外が発生します。このことは、 1 次元の配列でも 2 次元の配列でも同じです。

下に示すコードは 1 次元の動的配列を処理する関数と使用例です。この場合は、以下のように配列自身のアドレスを引数に渡します。この関数では 1 次元の静的配列は処理できません。このような関数は 1 次元の動的配列専用の関数となります。


  // 最初の配列要素のアドレスではなく、配列のアドレスを渡す
  test_conv(5, @Lsrc_dat, @Ldst_dat);


リスト4
1 次元の動的配列を処理する関数とその使用例。配列のアドレスを引数に渡す
静的配列と動的配列とでは配列データの格納構造が違う

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

  function test_conv(ASize: Integer; ASrcArray, ADstArray:Pointer): Boolean;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  1 次元配列の引数をポインタで渡す例
//  動的配列の場合
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  Lsrc_dat : array of Single;
  Ldst_dat : array of Single;
  LIndex   : Integer;
begin
  Memo1.Lines.Clear;
  Memo1.Lines.BeginUpdate;

  SetLength(Lsrc_dat, 5);
  SetLength(Ldst_dat, 5);

  for LIndex := Low(Lsrc_dat) to High(Lsrc_dat) do begin
    Lsrc_dat[LIndex]:=1000.0 + LIndex;
  end;

  // 関数を実行
  test_conv(5, @Lsrc_dat, @Ldst_dat);

  // 関数の戻り値を表示
  for Lindex := Low(Ldst_dat) to High(Ldst_dat) do begin
    Memo1.Lines.Add(FloatToStr(Lsrc_dat[LIndex]) + '   '
                  + FloatToStr(Ldst_dat[LIndex]));
  end;

  Memo1.Lines.EndUpdate;
end;

//----------------------------------------------------------------------------
//  1 次元配列を引数に持つ関数の例
//  引数の配列が Single 型の動的配列と仮定した場合の処理例
//
//  この関数の引数は、配列の先頭要素のアドレスではなく、配列のアドレスとする
//
//  Windows API の関数や他の DLL 内の関数の実際の処理は不明
//  Windoows API の関数は DLL で提供されている
//----------------------------------------------------------------------------
function test_conv(ASize: Integer; ASrcArray, ADstArray:Pointer): Boolean;
var
  LSrcArrAdr : NativeUInt;
  LDstArrAdr : NativeUInt;
  LSrcData   : Single;
  Lindex     : Integer;
begin
  Result := True;

  // 配列のアドレスを取得
  // このアドレスは最初の配列要素のポインタ
  LSrcArrAdr := PNativeUInt(ASrcArray)^;
  LDstArrAdr := PNativeUInt(ADstArray)^;

  // 配列データを要素順に操作
  for Lindex := 0 to ASize - 1 do begin
    LSrcData := PSingle(LSrcArrAdr)^;
    PSingle(LDstArrAdr)^ := LSrcData * 2.0;

    // 次のデータのある場所 (ポインタ) に移動
    inc(PSingle(LSrcArrAdr));
    inc(PSingle(LDstArrAdr));
  end;
end;

end.


上のコードはポインタを連続的に移動して処理しているだけです 。配列に対して汎用的な処理を行うには、以下のように、任意の配列要素への代入や値の取得の機能が必要です。



リスト5
1 次元の動的配列を処理する関数の例。任意の配列要素へのアクセスを可能にする例
//----------------------------------------------------------------------------
//  指定要素の値を取得
//  引数の ArrayPtr は配列の最初の要素のアドレス
//----------------------------------------------------------------------------
function GetDyn1DSingle(ArrayPtr: NativeUInt; AIndex: Integer): Single;
begin
  // 配列の先頭要素からポインタをインクリメント
  inc(PSingle(ArrayPtr), AIndex);
  // そのポインタにおける値を逆参照で取得
  Result := PSingle(ArrayPtr)^;
end;

//----------------------------------------------------------------------------
//  指定要素に値を代入
//----------------------------------------------------------------------------
procedure SetDyn1DSingle(ArrayPtr: NativeUInt; AIndex: Integer; AValue: Single);
begin
  inc(PSingle(ArrayPtr), AIndex);
  PSingle(ArrayPtr)^ := AValue;
end;

//----------------------------------------------------------------------------
//  1 次元配列を処理する関数の例
//
//  (1) 引数の配列が Single 型の動的配列
//  (2) 配列のアドレスを引数にしている
//  と仮定した場合の処理例
//----------------------------------------------------------------------------
function test_conv(ASize: Integer; ASrcArray, ADstArray:Pointer): Boolean;
var
  LpSrcArrAddr : NativeUInt;
  LpDstArrAddr : NativeUInt;
  LSrcData     : Single;
  Lindex       : Integer;
begin
  Result := True;

  // 配列のアドレスを取得
  // このアドレスは最初の配列要素のポインタ
  LpSrcArrAddr := PNativeUInt(ASrcArray)^;
  LpDstArrAddr := PNativeUInt(ADstArray)^;

  for Lindex := 0 to ASize - 1 do begin
    LSrcData := GetDyn1DSingle(LpSrcArrAddr, Lindex);
    SetDyn1DSingle(LpDstArrAddr, Lindex, LSrcData * 2.0);
  end;
end;


Delphi では配列を型定義して使用することができます。Delphi の型定義の動的配列を使用すると、1 次元の動的配列の処理を以下のように書けます。
動的配列のデータの格納構造を意識しないで処理コードが書けます。



リスト6
Delphi の型定義の動的配列を使用した、1 次元の動的配列を処理する関数の例
//----------------------------------------------------------------------------
//  1 次元配列を処理する関数の例
//
//  (1) 引数の配列が Single 型の動的配列
//  (2) 配列のアドレスを引数にしている
//  と仮定した場合の処理例
//----------------------------------------------------------------------------
function test_conv(ASize: Integer; ASrcArray, ADstArray:Pointer): Boolean;
type
  TDynArray = array of Single;
  PDynArray = ^TDynArray;
var
  LpSrcArray : PDynArray;
  LpDstArray : PDynArray;
  Lindex     : Integer;
begin
  Result := True;

  LpSrcArray := PDynArray(ASrcArray);
  LpDstArray := PDynArray(ADstArray);

  for Lindex := 0 to ASize - 1 do begin
    LpDstArray^[Lindex] := LpSrcArray^[Lindex] * 2.0;
  end;
end;


上のような処理コードであれば、以下のように、配列の要素数が取得てぎす。この値を利用すれば、関数の引数に配列の要素数がなくても処理できます。


  LSize := LEngth(LpSrcArray^);




03_引数が 2 次元の静的配列の場合


2 次元の静的配列を処理する関数とその関数を使用する例です。
Delphi では 2 次元の配列を array of array のように設定します。これは配列の配列という形式になっていますが、実際には 1 次元の配列と同じように、各要素のデータはメモリに連続的して格納されます。ポインタをインクリメントすることでデータを順番に取得できます。本サンプルの関数の処理もそうなっています。
つまり、関数内部では 1 次元の配列を 2 次元の配列として処理しています。

引数には配列要素の最初のデータのアドレスを渡します。静的配列の配列自身のアドレスと、最初の要素のデータのアドレスは同じです。したがって、以下のどちらのでも処理できます。

2 次元の静的配列用として作成されている配列の処理は、2 次元の動的配列の処理はできません。


  // 以下のどれでも構わない  
  SetArrayElement(LSize, @LSrcArray, @LDstArray);
  SetArrayElement(LSize, @LSrcArray[0], @LDstArray[0]);
  SetArrayElement(LSize, @LSrcArray[0][0], @LDstArray[0][0]);



図2
引数が 2 次元の静的配列の実行結果

リスト7
2 次元の静的配列を処理する関数とその使用例
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

  function SetArrayElement(ASize: Integer; ASrcArray, ADstArray: Pointer): Boolean;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.Font.Charset := ANSI_CHARSET;
  Memo1.Font.Name    := 'Courier New';
  Memo1.Font.Size    := 11;
end;

//=============================================================================
//  2 次元配列の引数をポインタで渡す例
//  静的配列の場合
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LSrcArray : array [0..2] of array [0..2] of Double;
  LDstArray : array [0..2] of array [0..2] of Double;
  LSize     : Integer;
  LText     : string;
  LRow      : Integer;
  LCol      : Integer;
begin
  Memo1.Lines.Clear;
  Memo1.Lines.BeginUpdate;

  // 配列の各要素に値を代入
  for LRow := Low(LSrcArray) to High(LSrcArray) do begin
    for LCol := Low(LSrcArray[LRow]) to High(LSrcArray[LRow]) do begin
      LSrcArray[LRow, LCol] := (LRow + 1) * 10 + (LCol + 1) + 0.2;
    end;
  end;

  // 代入した値を確認表示
  for LRow := Low(LSrcArray) to High(LSrcArray) do begin
    LText := '';
    for LCol := Low(LSrcArray[LRow]) to High(LSrcArray[LRow]) do begin
      LText := LText + FormatFloat('0.0    ', LSrcArray[LRow][LCol]);
    end;
    Memo1.Lines.Add(LText);
  end;

  // 関数の実行
  LSize := 3;
  SetArrayElement(LSize, @LSrcArray[0][0], @LDstArray[0][0]);

  // 関数の実行結果しての配列要素の値を表示
  Memo1.Lines.Add('');
  for LRow := Low(LDstArray) to High(LDstArray) do begin
    LText := '';
    for LCol := Low(LDstArray[LRow]) to High(LDstArray[LRow]) do begin
      LText := LText + FormatFloat('0.0    ', LDstArray[LRow][LCol]);
    end;
    Memo1.Lines.Add(LText);
  end;

  Memo1.Lines.EndUpdate;
end;

//-----------------------------------------------------------------------------
//  2 次元配列を引数に持つ関数の例
//  引数の配列が  Double 型の2 次元の静的配列と仮定した場合の処理例

//  この関数は静的配列を渡さないと正常に処理できない
//
//  Windows API の関数や他の DLL 内の関数の実際の処理は不明
//  Windoows API の関数は DLL で提供されている
//-----------------------------------------------------------------------------
function SetArrayElement(ASize: Integer; ASrcArray, ADstArray: Pointer): Boolean;
var
  LDstPtr   : Pointer;
  LSrcValue : Double;
  LDstValue : Double;
  LRow      : Integer;
  LCol      : Integer;
begin
  Result := True;

  // 引数のポインタの値を退避
  // 使用るす必要がなければ不要
  LDstPtr := Pointer(ADstArray);

  for LRow := 0 to ASize - 1 do begin
    for LCol := 0 to ASize - 1 do begin
      // 配列要素の値を取得
      LSrcValue := PDouble(ASrcArray)^;
     ;
     // 結果の配列要素に値を代入
      LDstValue := LSrcValue * 2.0;
      PDouble(ADstArray)^ := LDstValue;

      // 次のデータのある場所 (ポインタ) に移動
      inc(PDouble(ASrcArray));
      inc(PDouble(ADstArray));
    end;
  end;
end;

end.


上のコードはポインタを連続的に移動して処理しているだけです 。配列に対して汎用的な処理を行うには、以下のように、任意の配列要素への代入や値の取得の機能が必要です。



リスト8
2 次元の静的配列を処理する関数の例。任意の配列要素へのアクセスを可能にする例
//-----------------------------------------------------------------------------
//  指定要素の値を取得
//  引数の ArrayPtr は配列の最初の要素のポインタ
//  引数の ASize は配列の列数 
//-----------------------------------------------------------------------------
function Get2DDouble(ArrayPtr: Pointer; ASize, ARow, ACol: Integer): Double;
begin
  // 配列の先頭からポインタをインクリメント
  inc(PDouble(ArrayPtr), ARow * ASize + ACol);
  // そのポインタにおける値を逆参照で取得
  Result := PDouble(ArrayPtr)^;
end;

//-----------------------------------------------------------------------------
//  指定要素に値を代入
//-----------------------------------------------------------------------------
procedure Set2DDouble(ArrayPtr: Pointer; ASize, ARow, ACol: Integer; AValue: Double);
begin
  inc(PDouble(ArrayPtr), ARow * ASize + ACol);
  PDouble(ArrayPtr)^ := AValue;
end;

//-----------------------------------------------------------------------------
//  2 次元配列を処理する関数の例
//
//  (1) 引数の配列が Double 型の静的配列
//  (2) 配列の最初の要素のアドレスを引数にしている
//  と仮定した場合の処理例
//-----------------------------------------------------------------------------
function SetArrayElement(ASize: Integer; ASrcArray, ADstArray: Pointer): Boolean;
var
  LSrcValue : Double;
  LDstValue : Double;
  LRow      : Integer;
  LCol      : Integer;
begin
  Result := True;

  for LRow := 0 to ASize - 1 do begin
    for LCol := 0 to ASize - 1 do begin
      LSrcValue := Get2DDouble(ASrcArray, ASize, LRow, LCol);
      LDstValue := LSrcValue * 2.0;
      Set2DDouble(ADstArray, ASize,LRow, LCol, LDstValue);
    end;
  end;
end;


Delphi では配列を型定義して使用することができます。Delphi の型定義の静的配列を使用すると、2 次元の静的配列の処理を以下のように書けます。



リスト9
Delphi の型定義の静的配列を使用した、1 次元の静的配列を 2 次元の静的配列として処理
//-----------------------------------------------------------------------------
//  2 次元配列を処理する関数の例
//
//  (1) 引数の配列が Double 型の静的配列
//  (2) 配列の最初の要素のアドレスを引数にしている
//  と仮定した場合の処理例
//-----------------------------------------------------------------------------
function SetArrayElement(ASize: Integer; ASrcArray, ADstArray: Pointer): Boolean;
type
  TStatArray = array [0..$5FFFFFF] of Double;
  PStatArray = ^TStatArray;
var
  LpSrcArray : PStatArray;
  LpDstArray : PStatArray;
  LSrcValue  : Double;
  LRow       : Integer;
  LCol       : Integer;
begin
  Result := True;

  // 最初の要素のポインタを取得
  LpSrcArray := PStatArray(ASrcArray);
  LpDstArray := PStatArray(ADstArray);

  for LRow := 0 to ASize - 1 do begin
    for LCol := 0 to ASize - 1 do begin
      // 以下の式の ASize は列数の値
      LSrcValue := LpSrcArray^[LRow * ASize + LCol];
      LpDstArray^[LRow * ASize + LCol] := LSrcValue * 2.0;
    end;
  end;
end;




04_引数が 2 次元の動的配列の場合


2 次元の動的配列を処理する関数とその関数を使用する例です。
動的な 2 次元の配列は、複数の配列を格納した配列の配列という構造になっています。各行が1 1 つの 1 次元の配列となっていて、それそれに配列のポインタとしてのアドレスがあります。そのため、引数には配列自身のアドレスを渡さないと各行の配列にアクセスできません。本当の意味で array of array となっているとも言えます。

この 2 次元の静的配列との構造の違いにより、2 次元の動的配列を処理すコードでは、2 次元の静的配列は処理できません。その逆に、2 次元の静的配列を処理するコードで 2 次元の動的配列を処理することはできません。

本サンプルの関数を使用する場合は、以下のように、配列の引数にはその配列自身のアドレスを指定しないと正常に処理できません。例外が発生します。


  // 最初の配列要素のアドレスではなく、配列のアドレスを渡す  
  SetArrayElement(LSize, @LSrcArray, @LDstArray);


[備考]
実行時に配列要素数を確保する配列を動的配列と言いますが、可変長配列、あるいは可変長サイズの配列と言うことももあります。


図3
引数が 2 次元の動的配列の実行結果

リスト10
2 次元の動的配列を処理する関数とその使用例
静的配列と動的配列とでは配列データの格納構造が違う
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

  function SetArrayElement(ASize: Integer; ASrcArray, ADstArray: Pointer): Boolean;

var
  Form1: TForm1;

implementation

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.Font.Charset := ANSI_CHARSET;
  Memo1.Font.Name    := 'Courier New';
  Memo1.Font.Size    := 11;
end;

//=============================================================================
//  2 次元配列の引数をポインタで渡す例
//  動的配列の場合
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LSrcArray : array of array of Double;
  LDstArray : array of array of Double;
  LSize     : Integer;
  LRow      : Integer;
  LCol      : Integer;
  LText     : string;
begin
  Memo1.Lines.Clear;
  Memo1.Lines.BeginUpdate;


  // 配列のサイズを設定
  LSize := 3;
  SetLength(LSrcArray, LSize, LSize);
  SetLength(LDstArray, LSize, LSize);

  // 配列の各要素に値を代入
  for LRow := Low(LSrcArray) to High(LSrcArray) do begin
    for LCol := Low(LSrcArray[LRow]) to High(LSrcArray[LRow]) do begin
      LSrcArray[LRow, LCol] := (LRow + 1) * 10 + (LCol + 1) + 0.2;
    end;
  end;

  // 代入した値を確認表示
  for LRow := Low(LSrcArray) to High(LSrcArray) do begin
    LText := '';
    for LCol := Low(LSrcArray[LRow]) to High(LSrcArray[LRow]) do begin
      LText := LText + FormatFloat('0.0    ', LSrcArray[LRow][LCol]);
    end;
    Memo1.Lines.Add(LText);
  end;

  // 関数の実行
  // 本サンプルの関数の処理の場合は,配列自身のアドレスを指定する
  SetArrayElement(LSize, @LSrcArray, @LDstArray);

  // 関数の実行結果しての配列要素の値を表示
  Memo1.Lines.Add('');
  for LRow := Low(LDstArray) to High(LDstArray) do begin
    LText := '';
    for LCol := Low(LDstArray[LRow]) to High(LDstArray[LRow]) do begin
      LText := LText + FormatFloat('0.0    ', LDstArray[LRow][LCol]);
    end;
    Memo1.Lines.Add(LText);
  end;

  Memo1.Lines.EndUpdate;
end;

//-----------------------------------------------------------------------------
//  2 次元配列を引数に持つ関数の例
//  引数の配列が Double 型の動的配と仮定した処理例

//  この関数は動的配列のアドレスを渡さないと正常に処理できない
//
//  Windows API の関数や他の DLL 内の関数の実際の処理は不明
//  Windoows API の関数は DLL で提供されている
//-----------------------------------------------------------------------------
function SetArrayElement(ASize: Integer; ASrcArray, ADstArray: Pointer): Boolean;
var
  LSrcArrayOrigin  : NativeUInt;
  LSrcArrayAddr    : NativeUInt;
  LDstArrayAddr    : NativeUInt;
  LSrcRowArrayAddr : NativeUInt;
  LDstRowArrayAddr : NativeUInt;
  LSrcValue        : Double;
  LRow             : Integer;
  LCol             : Integer;
begin
  Result := True;

  // 配列のアドレスを逆参照で取得
  // このアドレスは最初の行の配列のポインタ
  LSrcArrayAddr := PNativeUInt(ASrcArray)^;
  LDstArrayAddr := PNativeUInt(ADstArray)^;

  // 配列のアドレスを退避
  LSrcArrayOrigin := LSrcArrayAddr;

  for LRow := 0 to ASize - 1 do begin
    // LRow 行目の配列のアドレスを逆参照で取得
    // このアドレスはこの行の配列の最初の要素のポインタ
    LSrcRowArrayAddr := PNativeUInt(LSrcArrayAddr)^;
    LDstRowArrayAddr := PNativeUInt(LDstArrayAddr)^;

    for LCol := 0 to ASize - 1 do begin

      // 配列要素の値を逆参照で取得
      // LSrcValue := ASrcArray[LRow][LCol]; の操作
      LSrcValue := PDouble(LSrcRowArrayAddr)^;

      // 配列要素に値を代入
      // ADstArray[LRow][LCol] := LSrcValue * 2.0; の操作
      PDouble(LDstRowArrayAddr)^ := LSrcValue * 2.0;

      // 同じ行の次のデータのある場所 (ポインタ) に移動
      inc(PDouble(LSrcRowArrayAddr));
      inc(PDouble(LDstRowArrayAddr));
    end;
    // 次の行の配列ポインタに移動
    inc(PNativeUInt(LSrcArrayAddr));
    inc(PNativeUInt(LDstArrayAddr));
  end;
end;

end.


上のコードはポインタを連続的に移動して処理しているだけです 。配列に対して汎用的な処理を行うには、以下のように、任意の配列要素への代入や値の取得の機能が必要です。



リスト11
2 次元の動的配列を処理する関数の例。任意の配列要素へのアクセスを可能にする例
//-----------------------------------------------------------------------------
//  指定要素の値を取得
//  引数の ARowAddr は最初の行の配列のアドレス 
//-----------------------------------------------------------------------------
function GetDyn2DDouble(ARowAddr: NativeUInt; ARow, ACol: Integer): Double;
var
  LRowAddr : NativeUInt;
begin
  // 行方向にポインタをインクリメントして目的の行の配列のアドレスを取得
  inc(PNativeUInt(ARowAddr), ARow);
  LRowAddr := PNativeUInt(ARowAddr)^;

  // 列方向にポインタをインクリメント
  inc(PDouble(LRowAddr), ACol);
  // そのポインタにおける値を逆参照で取得
  Result := PDouble(LRowAddr)^;
end;

//-----------------------------------------------------------------------------
//  指定要素に値を代入
//-----------------------------------------------------------------------------
procedure SetDyn2DDouble(ARowAddr: NativeUInt; ARow, ACol: Integer; AValue: Double);
var
  LRowAddr : NativeUInt;
begin
  inc(PNativeUInt(ARowAddr), ARow);
  LRowAddr := PNativeUInt(ARowAddr)^;

  inc(PDouble(LRowAddr), ACol);
  PDouble(LRowAddr)^:= AValue;
end;

//-----------------------------------------------------------------------------
//  2 次元配列を処理する関数の例
//
//  (1) 引数の配列が Double 型の動的配列
//  (2) 配列のアドレスを引数にしている
//  と仮定した場合の処理例
//-----------------------------------------------------------------------------
function SetArrayElement(ASize: Integer; ASrcArray, ADstArray: Pointer): Boolean;
var
  LSrcArrayAddr : NativeUInt;
  LDstArrayAddr : NativeUInt;
  LSrcValue     : Double;
  LRow          : Integer;
  LCol          : Integer;
begin
  Result := True;

  // 配列のアドレスを逆参照で取得
  // このアドレスは最初の行の配列のポインタ
  LSrcArrayAddr := PNativeUInt(ASrcArray)^;
  LDstArrayAddr := PNativeUInt(ADstArray)^;

  for LRow := 0 to ASize - 1 do begin
    for LCol := 0 to ASize - 1 do begin
      LSrcValue := GetDyn2DDouble(LSrcArrayAddr, LRow, LCol);
      SetDyn2DDouble(LDstArrayAddr, LRow, LCol, LSrcValue * 2.0);
    end;
  end;
end;


Delphi では配列を型定義して使用することができます。Delphi の型定義の動的配列を使用すると、2 次元の動的配列の処理を以下のように書けます。
2 次元の動的配列のデータの格納構造を意識しないで処理コードが書けます。



リスト12
Delphi の型定義の動的配列を使用した、2 次元の動的配列を処理する関数の例
//-----------------------------------------------------------------------------
//  2 次元配列を処理する関数の例
//
//  (1) 引数の配列が Double 型の動的配列
//  (2) 配列のアドレスを引数にしている
//  と仮定した場合の処理例
//-----------------------------------------------------------------------------
function SetArrayElement(ASize: Integer; ASrcArray, ADstArray: Pointer): Boolean;
type
  TDyn2DArray = array of array of Double;
  PDyn2DArray = ^TDyn2DArray;
var
  LpSrcArray : PDyn2DArray;
  LpDstArray : PDyn2DArray;
  LRow       : Integer;
  LCol       : Integer;
begin
  Result := True;

  // 配列のポインタを取得
  LpSrcArray := PDyn2DArray(ASrcArray);
  LpDstArray := PDyn2DArray(ADstArray);

  for LRow := 0 to ASize - 1 do begin
    for LCol := 0 to ASize - 1 do begin
      LpDstArray^[LRow][LCol] := LpSrcArray^[LRow][LCol] * 2.0
    end;
  end;
end;


上のような処理コードであれば以下のように、配列の行数や列数等が取得てぎす。これらの値を利用すれば、関数の引数に行数や列数の値がなくても処理できます。


  // 行数を取得
  LRowSize := Length(LpSrcArray^);
  // 列数を取得 (1 番目の行の配列の要素数)
  LColSizse := Length(LpSrcArray^[0]);

  // 行の最小要素番号 (動的配列の場合は常に 0)
  LValue := Low(LpSrcArray^);
  // 行の最大要素番号 )行数 - 1)
  LValue := High(LpSrcArray^);




05_型宣言の配列型とその引数の型指定


Delphi のクラス等の型はポインタです。関数の引数として型定義した配列変数も渡せます。以下のように配列の引数の型として、定義した型のポインタ型も指定できます。


// DLL 内の関数を使用する側の配列の型宣言 
  Type
    TDataArray = array [0..4] of Double;
    PDataArray = ^TDataArray;

var
  Lsrc_dat : TDataArray;
  Ldst_dat : TDataArray;


  // 引数に型のポインタ型であれば使用できる 
  procedure test_func(ASize: integer; ASrcArray, ADstArray: PDataArray); stdcall;
    external 'TestDll.dll'; 


動的配列の場合は以下のように定義した型を指定できます。


  procedure test_func(ASize: integer; ASrcArray, ADstArray: TDataArray); stdcall;
    external 'TestDll.dll'; 


一般的に、DLL 内の関数は Delphi のクラスの型や、呼び出し側で宣言した型のことを知りません。したがって、実際には DLL 内の関数で定義されている型として処理されます。本ペーに掲載しているサンプルでは Pointer 型として処理されることになります。





06_関数の内部処理が静的配列か動的配列かの判定


行列演算ライブラリ、数値計算ライブラリとして提供されている 2 次元の配列関係の関数の多くは静的配列用です。
引数の配列が関数内部で静的配列として処理されるか、動的配列として処理されるかは、その関数の説明等で調べることになります。1 次元の静的配列を処理する関数では 1 次元の動的配列も処理できますから、問題は 2 次元の配列の場合です。

C または C++ の関数の定義が以下のようにアドレスを意味する * 演算子が 2 つのポインタのポインタとなっている場合は、2 次元の動的配列のデータ構造を考えると、おそらく動的配列です。静的配列の場合は * 演算子が 1 つだけです。


// 関数のプロトタイプ宣言
void array_sample(double **, int, int);

// 関数の定義
void array_sample(double **array, int row, int col)
{
  // コードは省略
}


不明な場合は、関数を実行する際、次のように配列のアドレスを渡して実際に実行して確認すると確実しかないでしょう。


  procedure array_test(ASize: Integer; ASrcArray, ADstArray: Pointer);
    stdcall; external 'TestDll.dll'

var
  Form1: TForm1;

implementation

{$R *.dfm}

//-----------------------------------------------------------------------------
//  ローカル変数が確保可能なメモリに制限があるのでグローバル変数で確保
//-----------------------------------------------------------------------------
const
  FSize = 10;
type
  TDataArray = array [0..FSize - 1] of array [0..FSize - 1] of Double;
var
  FSrcArray : TDataArray;
  FDstArray : TDataArray;

procedure TForm1.Button1Click(Sender: TObject);
begin
  FSrcArray := System.Default(TDataArray);
  FDstArray := System.Default(TDataArray);

  // 関数実行
  array_test(FSize, @FSrcArray, @FDstArray);

  ShowMessage('Close');
  Close;
end;


テストは静的配列、動的配列のどちらかで十分です。
静的配列を渡してメモリアクセス違反等の例外が発生しなければ、関数の内部では静的配列として処理されていると言えます。その関数に動的配列を渡すと例外が発生する可能性があります。
逆に、その関数に動的配列を渡して、メモリアクセス違反等の例外が発生しなければ、関数の内部では動的配列として処理されていると言えます。
このようなテストをする場合、配列の要素を初期化、あるいは初期値を設定しておく必要があります。そうしないと演算のオーバフロー等のエラーが発生して正しく判定がてぎなくなることがあります。






07_1 次元の配列を 2 次元の配列として操作


行列演算用のライブラリや数値計算ライブラリの中には、1 次元の配列を渡すと、内部では 2 次元の配列として処理する関数を実装しているライブラリがあります。この方式であれば、その関数が 1 次元の静的配列用であっても、動的な配列も扱えます。
そんな時に参考になるかも知れないコードです。



図4
1 次元の配列を 2 次元の配列として操作する例

リスト13
1 次元の配列を 2 次元の配列として操作する例
//=============================================================================
//  フォーム生成時の処理
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  Memo1.Font.Charset := ANSI_CHARSET;
  Memo1.Font.Name    := 'Courier New';
  Memo1.Font.Size    := 11;
end;

//=============================================================================
//  1 次元の配列を 2 次元の配列として操作する例
//  動的配列の場合
//
//  1 次元の配列を 2 次元の配列として扱う関数は、2 次元の静的配列が使用可能
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LSrcArray : array of Double;
  LRowSize  : Integer;
  LColSize  : Integer;
  LRow      : Integer;
  LCol      : Integer;
  LIndex    : Integer;
  LText     : string;
begin
  Memo1.Lines.Clear;
  Memo1.Lines.BeginUpdate;

  // 動的配列のサイズを設定
  LRowSize := 3;
  LColSize := 4;
  SetLength(LSrcArray, (LRowSize * LColSize));

  // 配列の各要素に値を代入
  LIndex := 0;
  for LRow := 0 to LRowSize - 1 do begin
    for LCol := 0 to LColSize - 1 do begin
      LSrcArray[LIndex] := (LRow + 1) * 10.0 + (LCol + 1) + 0.2;
      Inc(LIndex);
    end;
  end;

  // 代入した値を確認表示
  LIndex := 0;
  for LRow := 0 to LRowSize - 1 do begin
    LText := '';
    for LCol := 0 to LColSize - 1 do begin
      LText := LText + FormatFloat('0.0    ', LSrcArray[LIndex]);
      Inc(LIndex);
    end;
    Memo1.Lines.Add(LText);
  end;

  // 2 行 3 列目の値を取得して表示
  LRow := 1;
  LCol := 2;
  LIndex := LRow * LColSize + LCol;
  LText := FormatFloat('0.0    ', LSrcArray[LIndex]);
  Memo1.Lines.Add('');
  Memo1.Lines.Add(LText);

  // 指定インデック (0 ベース) の行と列番号(0 ベース) を取得して表示
  LRow := LIndex div LColSize;
  LCol := LIndex mod LColSize;
  LText := '[' + IntToStr(LRow) + '][' + IntToStr(LCol) + ']';
  Memo1.Lines.Add('');
  Memo1.Lines.Add(LText);

  // 対角要素の値
  // uses に Math が必要
  Memo1.Lines.Add('');
  for LCol := 0 to Min(LRowSize, LColSize) - 1 do begin
    LIndex := LCol * LColSize + LCol;
    LText := FormatFloat('0.0    ', LSrcArray[LIndex]);
    Memo1.Lines.Add(LText);
  end;

  Memo1.Lines.EndUpdate;
end;




08_DLL に関数を組み込む - DLL 内の関数の独立性と汎用性


本ページに掲載している関数のコードは同じユニット内に書いていますが、そのまま DLL 内の関数として組み込めるようになっています。

DLL 内の関数は独立した機能で汎用的である必要があります。Integer, Single, Double 等の単純型の型であれば、どのプログラム言語にも実装されているので DLL 内の関数の引数の型として使用できます。また、Windows API で定義済みの構造体 (Delphi ではレコード型) も DLL 内の関数の引数の型として使用できます。

配列にはプログラム言語に共通の型がありません。配列を使用する関数を汎用的にするには、配列を受け取る引数の型をポインタ型にします。関数を使用するアプリは、配列のアドレス、あるいは配列の最初の要素のアドレスをポインタとして渡します。
そうすると、関数が受け取る変数の実体はポインタです。そこには配列であるという情報はありません。配列要素にインデックス (添字) でアクセスすることはできません。本ページのサンプルのようにポインタの操作、あるいは DLL 内で配列を型前言して操作することになります。

DLL を使用する側のプログラムにも、DLL 側にも同じ配列の型宣言をしておけば、関数の引数にその型を使用できますが、Delphi 専用の DLL になってしまって汎用的とは言えません。Delphi 専用でいいのであればユニット形式で提供した方が便利です。
Delphi 専用だが、関数の処理コードを公開したくないというのであれば DLL で提供する意味はあります。ユニットファイルをコンパイルした dcu ファイルで提供てする方法もありますが、dcu ファイルはバージョン依存ファイルです。dcu ファイルを作成したバージョンの Delphi でしか利用できません。






09_DLL に関数を組み込む - DLL の作成方法と使用方法


以下のコードは DLL のプロジェクトのソースコードです。このコードを、拡張子を .dpr にして、コードの先頭の library の名前で保存します。この場合は TestDll.dpr となります。
保存したファイルを Delphi の IDE で開いて [Shift] + [F9] のキー操作でコンパイルすれば DLL が作成されます。DLL の他にもプロジェクト関係のファイルが複数生成されます。

Delphi で新規にプロジェクトを作成すると、フォーム関係の Unit1.pas, Unit1.dfm ファイルができます。それらのファイルは DLL の作成に必要ありません。削除します

この DLL には関数を 1 つだけ実装しています。関数のコードはこれまでの関数のコードをそのまま使用できますが、関数の最後、末尾に stdcall を追加します。exports 部には、外部から利用する関数名をリストします。それ以外は一般的なユニットで使用する関数の記述と同じです。 
この関数は、引数の配列の各要素に値を代入しているだけです。この関数は、1 次元の配列であれば静的配列でも動的配列でも使用できます。

stdcall は呼出し規約と言うコンパイラ指令です。これは、コンパイラが関数の引数を処理する時の仕様です。Windows 上で作成する DLL はこの呼び出し規約を使用すると思ってください。



リスト14
DLL のプロジェクトのソースコードの例。1 次元の配列処理用
//----------------------------------------------------------------------------
//  DLL の作成方法
//  (1) このコードを TestDll.dpr (library 名 + .dpr) の名前で保存
//  (2) 保存したファイルを Delphi の IDE で開く
//  (3) [Shift] + [F9] でコンパイルする
//  (4) TestDll.dll が作成される
//
//  ※ DLL が作成される場所はプロジェクトの構成によって異なる
//----------------------------------------------------------------------------
library TestDll;

uses
  // 必要なら他のユニットを追加する
  System.SysUtils,
  System.Classes;

//-----------------------------------------------------------------------------
//  1 次元配列を処理する関数の例
//
//  (1) 引数の配列が Double 型の静的配列または動的配列
//  (2) 配列の最初の要素のアドレスを引数にしている
//  と仮定した場合の処理例
//
//  外部から使用する関数には呼び出し規約のコンパイラ指令 stdcall を末尾に追加する
//-----------------------------------------------------------------------------
procedure array_test(ALen: Integer; ADataArr: Pointer); stdcall;
var
  Lindex : Integer;
begin
  // 配列型の引数の型が Pointere になっている
  // 元の Double 型にするために PDouble 型でキャストする
  // 配列用要素の値に対する操作は逆参照

  for Lindex := 0 to ALen - 1 do begin
    PDouble(ADataArr)^ := (Lindex + 1.0) * 100.0;
    inc(PDouble(ADataArr));
  end;
end;

exports
  // 外部から呼び出して使用する関数名のリスト
  array_test;
end.


以下は DLL 内の関数を使用したプログラムのコードです。
ユニット内の関数を使用する時と同じで、関数の使用宣言を記述します。ユニット内の関数を使用する時と違うのは、関数あるいは手続きの宣言文の末尾に次のコードを追加することと、宣言した関数あるいは手続き (ブロージャ) の実現部がユニット内にないことです。


 stdcall; external 'TestDll.dll'


stdcall は呼出し規約です。DLL 内の関数の呼び出し規約と同じにします。external 宣言は、この関数あるいは手続きを、その右に記述したファイルからインポートして使用するという意味です。
DLL 内の関数を使用するこの方法は、Windows API の関数を使用する方法と同じです。Windows API の関数類は DLL で提供されています。

本サンプルで使用する DLL ファイルは、システム的にパスの通った場所に必要です。アプリの EXE が最初に探しにいくのは EXE 自身があるフォルダ内です。
Windows API 関係の DLL は Windows の システム関係フォルダ内にあります。それらのフォルダにはシステム的にパスが通っています。



図5
実行結果
  • 数値は DLL 内の関数が配列要素に代入した値

リスト15
DLL 内の関数を使用するプログラムのコード
unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

// DLL 内の関数の使用宣言

// DLL (この場合は TestDll.dll) はシステム的にパスの通った場所に必要
// アプリが最初に検索する場所はそのアプリの EXE ファイルがあるフォルダ内
// プロジェクト関係ファイルと同じ場所に EXE が作成されるとは限らない 

procedure array_test(ASize: Integer; AArray: Pointer); stdcall; external 'TestDll.dll';

var
  Form1: TForm1;

implementation

{$R *.dfm}


//=============================================================================
//  DLL 内の配列処理関係の関数を使用するコード
//  配列の要素数が多いと実行時にメモリアクセス違反の例外が発生する
//  その場合は配列変数をグローバル変数にする
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LArrayData : array of Double;
  LSize      : Integer;
  LIndex     : Integer;
begin
  Memo1.Lines.Clear;
  Memo1.Lines.BeginUpdate;

  LSize := 5;
  SetLength(LArrayData, LSize);

  // DLL 内の関数を実行
  array_test(LSize, @LArrayData[0]);

  for Lindex := 0 to LSize - 1 do begin
    Memo1.Lines.Add(FloatToStr(LArrayData[LIndex]));
  end;

  Memo1.Lines.EndUpdate;
end;

end.


配列の引数を、以下のように要素のデータ型に応じたポインタ型にすると関数の処理コードが少しはスッキリします。この方法は静的配列で使用できます。動的配列の場合は PNativeUInt 型にします。


procedure array_test(ALen: Integer; ADataArr: PDouble); stdcall;
var
  Lindex : Integer;
begin
  // 配列型の引数の型が PDouble になっている
  // 呼び出し側で汎用の Pointer で渡せば PDouble 型にキャストされる
  // したがって以下のようにそのまま値の取得や代入ができる
  // 配列用要素の値に対する操作は逆参照

  for Lindex := 0 to ALen - 1 do begin
    ADataArr^ := (Lindex + 1.0) * 100.0;
    inc(ADataArr);
  end;
end;


Delphi の IDE から DLL を新規に作成するには、[ファイル] [新規作成] で [その他] を選択して下図のダイアログを表示します。このダイアログの [Delphi ファイル] から [ダイナミックリンクライブラリ] を選択します。

上の DLL のプロジェクトのソースコードを保存して生成されるプロジェクトの構成は以下の記事と同じようになります。その構成の設定内容は IDE から新規に DLL のプロジェクトを作成した場合と少し違います。プロジェクトの構成の設定内容は [プロジェクト] [オプション] で変更できます。



図6
DLL の新規作成゜

  • [ファイル] [新規作成]
    [その他] のダイアログ
  • [Delphi ファイル] から [ダイナミックリンクライブラリ] を選択する




10_DLL に関数を組み込む - 処理コードを別ユニットにする


DLL 内だけで使用する関数も含めて、実装する関数や処理コードが多くなると、ユニットファイルを作成した方が便利です。以下のコードは、処理コードを別のユニットにした場合の DLL のプロジェクトのソースコードです。



リスト16
関数関係のコードを別ユニットした DLL のプロジェクトのソースコード
//----------------------------------------------------------------------------
//  DLL の作成方法
//  (1) このコードを TestDll.dpr (library 名 + .dpr) の名前で保存
//  (2) 保存したファイルを Delphi の IDE で開く
//  (3) [Shift] + [F9] でコンパイルする
//  (4) TestDll.dll が作成される
//
//  ※ DLL が作成される場所はプロジェクトの構成によって異なる

//  コンパイルするには TestDll_sub.pas がライブラリのパスが通った場所に必要
//----------------------------------------------------------------------------
library TestDll;

uses
  // 必要なら他のユニットを追加する
  TestDll_sub;

exports
  // 外部から呼び出して使用する関数名のリスト
  array_test;
end.


作成したユニットのコードです。実装している機能は前項のサンプルと同じです。
外部から使用する関数にコンパイラ指令 stdcall を追加する以外は通常の Delphi のユニットと全く同じですが、DLL 特有の DllProc 等の機能が使用できます。

ユニットは、DLL のプロジェクトを開いた状態で、IDE のメニューの [ファイル] [新規作成] から [ユニット] で追加できます。追加したら名前を付けて保存します。下に示すユニットは、TestDll_sub.pas という名前で保存しています。
この操作で追加したユニットは、プロジェクトのソースコードの uses 部に自動で追加されます。



リスト17
DLL のプロジェクトで使用する関数類のコードのユニット
//----------------------------------------------------------------------------
//  DLL のプロジェクトで使用するユニットのコード
//  このコードを TestDll_sub.pas (unit 名 + .pas) の名前で保存する
//  保存先は DLL のプロジェクトファイル (拡張子は .dpr) があるフォルダ内
//
//  ※ プロジェクトの構成によってはプロジェクトのファイルと DLL ファイルが
//     別フォルダになることがある
//----------------------------------------------------------------------------
unit TestDll_sub;

interface

uses
  // 必要に応じて追加する
  System.SysUtils,System.Classes;

  // 関数の宣言部

// 外部から使用する関数には呼び出し規約のコンパイラ指令 stdcall を末尾に追加する
procedure array_test(ALen: Integer; ADataArr: Pointer); stdcall;

// 外部から使用しない関数にはコンパイラ指令 stdcall は不要
function GetValue(ArrayPtr: Pointer; AIndex: Integer): Double;
procedure SetValue(ArrayPtr: Pointer; AIndex: Integer; AValue: Double);


implementation

  // 以下、関数の実現部

//-----------------------------------------------------------------------------
//  1 次元配列を処理する関数の例
//
//  (1) 引数の配列が Double 型の静的配列または動的配列
//  (2) 配列の最初の要素のアドレスを引数にしている
//  と仮定した場合の処理例
//
//  外部から使用する関数には呼び出し規約のコンパイラ指令 stdcall を末尾に追加する
//-----------------------------------------------------------------------------
procedure array_test(ALen: Integer; ADataArr: Pointer); stdcall;
var
  Lindex : Integer;
  LValue : Double;
begin
  // 配列型の引数の型が Pointere になっている
  // 元の Double 型にするために PDouble 型でキャストが必要
  // 配列用要素の値に対する操作は逆参照

  for Lindex := 0 to ALen - 1 do begin
    LValue := GetValue(ADataArr, Lindex);
    SetValue(ADataArr, Lindex, (Lindex + 1.0) * 100.0);
  end;
end;

//----------------------------------------------------------------------------
//  指定要素の値を取得
//  引数の ArrayPtr は配列の最初の要素のポインタ
//
//  外部から使用しない関数にはコンパイラ指令 stdcall は不要
//----------------------------------------------------------------------------
function GetValue(ArrayPtr: Pointer; AIndex: Integer): Double;
begin
  // 配列の先頭要素からポインタをインクリメント
  inc(PDouble(ArrayPtr), AIndex);
  // そのポインタにおける値を逆参照で取得
  Result := PDouble(ArrayPtr)^;
end;

//----------------------------------------------------------------------------
//  指定要素に値を代入
//
//  外部から使用しない関数にはコンパイラ指令 stdcall は不要
//----------------------------------------------------------------------------
procedure SetValue(ArrayPtr: Pointer; AIndex: Integer; AValue: Double);
begin
  inc(PDouble(ArrayPtr), AIndex);
  PDouble(ArrayPtr)^ := AValue;
end;

end.