Tutorial 12: Memory Management and File I/O

このチュートリアルでは、メモリ管理とファイル入出力操作について学ぶことにする。 加えて、入出力制御として、コモンダイアログボックスも使用する。
 ソース   リソーススクリプト   実行結果 

Theory:

アプリケーション側から見たWin32のメモリ管理は非常に単純である。 それぞれのプロセスは4GBのアドレス空間を持つ。 使用するメモリモデルは「フラットメモリモデル」である。 このモデルでは、全てのセグメントレジスタ(or セレクタ)は同じ開始アドレスを指している。 そして、オフセットは32ビットなので、アプリケーションは自分のアドレス空間なら、 セレクタの値は変換せずにどのメモリでもアクセスすることができる。 非常にメモリ管理を簡単にし、"near" ポインタや "far" ポインタはもはや必要ないのである。

Win16では、2つの主なメモリ関連のAPI関数があり、「グローバル」と「ローカル」だ。 グローバルタイプのAPI関数は、異なるセグメントにメモリを確保するようになっている。 なので、それらの関数を "far" タイプのメモリ関数と呼ぶ。 対して、ローカルタイプのAPI関数はプロセスのローカルヒープ領域に割り当てので、 "naer" タイプのメモリ関数と呼ぶ。
一方、Win32ではこれら2つのタイプの関数は同一のものとなる。 グローバルタイプの関数だろうが、ローカルタイプだろうが結果は同じになる。

メモリの確保とそのメモリの使用方法は以下のような手順となる。

  1. GlobalAlloc関数をCALLしてメモリを確保する。この関数はメモリブロックのハンドルを返す。
  2. GlobalLock関数をCALLしてそのメモリを「ロック」する。メモリブロックのハンドルを引数にとり、メモリブロックへのポインタを返す。
  3. そのポインタを介してメモリへ読み書きできる。
  4. GlobalUnlock関数をCALLしてメモリブロックを「ロック解除」する。この関数はメモリブロックへのポインタを無効にする。
  5. GlobalFree関数をCALLしてメモリブロックを解放する。引数にメモリブロックのハンドルをとる。

LocalAlloc関数やLocalLock関数という「Local」関数でも「Global」関数と同様の結果が得られる。 GlobalAlloc関数をCALLする際に、フラグ GMEM_FIXED を指定すると、さらに簡単になる。 もし、GMEM_FIXEDを使用すれば、GlobalAlloc関数やLocalAlloc関数からの戻り値はメモリブロックへのハンドルではなく、メモリブロックへのポインタとなる。 なので、Global/LocalLock関数を呼ぶ必要がなくなるのである。 でもこのチュートリアルでは、あなたがそのようなソースコードに出会った時に戸惑うことが無いように、「伝統的」な方法を紹介する。

Win32のファイル入出力はDOSのやり方とよく類似している。 必要な手順は同じものであり、DOSで行っていた割り込み制御がAPI関数をCALLすることに変更するだけだ。 具体的には以下のようになる。

  1. CreateFile関数をCALLすることにより、ファイルをオープン、または作成する。この関数は非常に万能で、ファイルの他にポートを開いたりパイプやドライブも扱える。成功すれば、その作成したファイルやデバイスへのハンドルが返ってくる。このハンドルを介してファイルやデバイスに対して操作を行うことができる。
    また、SetFilePointer関数でファイルポインタを任意の位置に移動することができる。
  2. ReadFile関数、WriteFile関数でファイルの読み書きができる。これらの関数はファイルからメモリへ、またはメモリからファイルへとデータを操作する。なので、十分なメモリ領域を確保しておかなければならない。
  3. CloseHandle関数でファイルを閉じる。引数にファイルハンドルをとる。

Content:

以下のプログラムでファイルをオープンするダイアログボックスを表示する。 ユーザにファイルを選択してもらい、クライアントエリアのエディットコントロールにそのファイルの中身を表示する。 ユーザがそのファイルの内容が表示されているエディットコントロールを編集し、セーブすれば、その内容がファイルに書き出される。

.386
.model flat,stdcall
option casemap:none
WinMain proto :DWORD,:DWORD,:DWORD,:DWORD
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comdlg32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comdlg32.lib

.const
IDM_OPEN equ 1
IDM_SAVE equ 2
IDM_EXIT equ 3
MAXSIZE equ 260
MEMSIZE equ 65535

EditID equ 1                           ; ID of the edit control

.data
ClassName db "Win32ASMEditClass",0
AppName db "Win32 ASM Edit",0
EditClass db "edit",0
MenuName db "FirstMenu",0
ofn  OPENFILENAME <>
FilterString db "All Files",0,"*.*",0
            db "Text Files",0,"*.txt",0,0
buffer db MAXSIZE dup(0)

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?
hwndEdit HWND ?                              ; Handle to the edit control
hFile HANDLE ?                                  ; File handle
hMemory HANDLE ?                           ;handle to the allocated memory block
pMemory DWORD ?                           ;pointer to the allocated memory block
SizeReadWrite DWORD ?                  ; number of bytes actually read or write

.code
start:
   invoke GetModuleHandle, NULL
   mov   hInstance,eax
   invoke GetCommandLine
    mov CommandLine,eax
   invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
   invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:SDWORD
   LOCAL wc:WNDCLASSEX
   LOCAL msg:MSG
   LOCAL hwnd:HWND
   mov  wc.cbSize,SIZEOF WNDCLASSEX
   mov  wc.style, CS_HREDRAW or CS_VREDRAW
   mov  wc.lpfnWndProc, OFFSET WndProc
   mov  wc.cbClsExtra,NULL
   mov  wc.cbWndExtra,NULL
   push hInst
   pop  wc.hInstance
   mov  wc.hbrBackground,COLOR_WINDOW+1
   mov  wc.lpszMenuName,OFFSET MenuName
   mov  wc.lpszClassName,OFFSET ClassName
   invoke LoadIcon,NULL,IDI_APPLICATION
   mov  wc.hIcon,eax
   mov  wc.hIconSm,eax
   invoke LoadCursor,NULL,IDC_ARROW
   mov  wc.hCursor,eax
   invoke RegisterClassEx, addr wc
   invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
          WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
          CW_USEDEFAULT,300,200,NULL,NULL,\
          hInst,NULL
   mov  hwnd,eax
   invoke ShowWindow, hwnd,SW_SHOWNORMAL
   invoke UpdateWindow, hwnd
   .WHILE TRUE
       invoke GetMessage, ADDR msg,NULL,0,0
       .BREAK .IF (!eax)
       invoke TranslateMessage, ADDR msg
       invoke DispatchMessage, ADDR msg
   .ENDW
   mov    eax,msg.wParam
   ret
WinMain endp

WndProc proc uses ebx hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
   .IF uMsg==WM_CREATE
       invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\
                  WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\
                  ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\
                  0,0,0,hWnd,EditID,\
                  hInstance,NULL
       mov hwndEdit,eax
       invoke SetFocus,hwndEdit
;==============================================
;       Initialize the members of OPENFILENAME structure
;==============================================
       mov ofn.lStructSize,SIZEOF ofn
       push hWnd
       pop ofn.hWndOwner
       push hInstance
       pop ofn.hInstance
       mov ofn.lpstrFilter, OFFSET FilterString
       mov ofn.lpstrFile, OFFSET buffer
       mov ofn.nMaxFile,MAXSIZE
   .ELSEIF uMsg==WM_SIZE
       mov eax,lParam
       mov edx,eax
       shr edx,16
       and eax,0ffffh
       invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE
   .ELSEIF uMsg==WM_DESTROY
       invoke PostQuitMessage,NULL
   .ELSEIF uMsg==WM_COMMAND
       mov eax,wParam
       .if lParam==0
           .if ax==IDM_OPEN
               mov ofn.Flags, OFN_FILEMUSTEXIST or \
                               OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
                               OFN_EXPLORER or OFN_HIDEREADONLY
               invoke GetOpenFileName, ADDR ofn
               .if eax==TRUE
                   invoke CreateFile,ADDR buffer,\
                               GENERIC_READ or GENERIC_WRITE ,\
                               FILE_SHARE_READ or FILE_SHARE_WRITE,\
                               NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
                               NULL
                   mov hFile,eax
                   invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
                   mov hMemory,eax
                   invoke GlobalLock,hMemory
                   mov pMemory,eax
                   invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL
                   invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory
                   invoke CloseHandle,hFile
                   invoke GlobalUnlock,pMemory
                   invoke GlobalFree,hMemory
               .endif
               invoke SetFocus,hwndEdit
           .elseif ax==IDM_SAVE
               mov ofn.Flags,OFN_LONGNAMES or\
                               OFN_EXPLORER or OFN_HIDEREADONLY
               invoke GetSaveFileName, ADDR ofn
                   .if eax==TRUE
                       invoke CreateFile,ADDR buffer,\
                                               GENERIC_READ or GENERIC_WRITE ,\
                                               FILE_SHARE_READ or FILE_SHARE_WRITE,\
                                               NULL,CREATE_NEW,FILE_ATTRIBUTE_ARCHIVE,\
                                               NULL
                       mov hFile,eax
                       invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
                       mov hMemory,eax
                       invoke GlobalLock,hMemory
                       mov pMemory,eax
                       invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory
                       invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL
                       invoke CloseHandle,hFile
                       invoke GlobalUnlock,pMemory
                       invoke GlobalFree,hMemory
                   .endif
                   invoke SetFocus,hwndEdit
               .else
                   invoke DestroyWindow, hWnd
               .endif
           .endif
       .ELSE
           invoke DefWindowProc,hWnd,uMsg,wParam,lParam
           ret
.ENDIF
xor   eax,eax
ret
WndProc endp
end start

Analysis:

invoke CreateWindowEx,NULL,ADDR EditClass,NULL,\
           WS_VISIBLE or WS_CHILD or ES_LEFT or ES_MULTILINE or\
           ES_AUTOHSCROLL or ES_AUTOVSCROLL,0,\
           0,0,0,hWnd,EditID,\
           hInstance,NULL
mov hwndEdit,eax

WM_CREATEセクションでは、エディットコントロールを作成する。 後で、親ウィンドウのクライアントエリア全体に領域を広げるため、ここでは座標値やサイズは全部 0 にしておく(後で結局変更するので)。 またWS_VISIBLEスタイルも指定しているので、エディットコントロールを表示するためにShowWindows関数をCALLする必要はない。


ちなみに、このトリックは親ウィンドウでも使用できる。

;==============================================
;       Initialize the members of OPENFILENAME structure
;==============================================
       mov ofn.lStructSize,SIZEOF ofn
       push hWnd
       pop ofn.hWndOwner
       push hInstance
       pop ofn.hInstance
       mov ofn.lpstrFilter, OFFSET FilterString
       mov ofn.lpstrFile, OFFSET buffer
       mov ofn.nMaxFile,MAXSIZE

エディットコントロールを作成し終わったら、今度はofn構造体を初期化する。 ダイアログボックスのセーブコマンドでもofnを再使用するので、 GetOpenFileName関数とGetSaveFileName関数で使用するときに「共通」のメンバの値だけをセットしておく。
WM_CREATEセクションは初期化するにはもってこいの場所だ。

.ELSEIF uMsg==WM_SIZE
    mov eax,lParam
    mov edx,eax
    shr edx,16
    and eax,0ffffh
    invoke MoveWindow,hwndEdit,0,0,eax,edx,TRUE

親ウィンドウのサイズが変更したときに、WM_SIZEメッセージが飛んでくる。 このメッセージはウィンドウが作られたときにも飛んでくるのだが、 このメッセージを受け取るには、CS_VREDRAWとCS_HREDRAWスタイルをウィンドウクラスに設定しておかなければならない。
このメッセージを受け取ったときに、同時にエディットコントロールのサイズも変更するのだが、 まずは親ウィンドウのクライアントエリアにおける現在の幅と高さを知る必要があり、 その情報は lParam から取得できる。 lParamの上位ワードにはクライアントエリアの高さが、下位ワードには幅が格納されている。 なので、その情報からMoveWindow関数をCALLすることによりエディットコントロールのサイズを変更し、 ウィンドウの座標も変更する。

.if ax==IDM_OPEN
    mov ofn.Flags, OFN_FILEMUSTEXIST or \
                    OFN_PATHMUSTEXIST or OFN_LONGNAMES or\
                    OFN_EXPLORER or OFN_HIDEREADONLY
    invoke GetOpenFileName, ADDR ofn

ユーザがFile→Openとメニューを選択したら、 ofn構造体の必要なメンバをセットし、ファイルオープンダイアログボックスを表示するためにGetOpenFileName関数をCALLする。

.if eax==TRUE
    invoke CreateFile,ADDR buffer,\
                GENERIC_READ or GENERIC_WRITE ,\
                FILE_SHARE_READ or FILE_SHARE_WRITE,\
                NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,\
                NULL
    mov hFile,eax

ファイルを選択した後、CreateFile関数でファイルをオープンするのだが、 ファイルをオープンするのか作成するのかを指定しなければならない。 ファイルをオープンした後、そのファイルハンドルは将来色々と使用するのでグローバル変数にセットする。
CreateFile関数のプロトタイプは以下のようになっている。

CreateFile proto lpFileName:DWORD,\
                          dwDesiredAccess:DWORD,\
                          dwShareMode:DWORD,\
                          lpSecurityAttributes:DWORD,\
                          dwCreationDistribution:DWORD\,
                          dwFlagsAndAttributes:DWORD\,
                          hTemplateFile:DWORD

  1. dwDesiredAccess
    ファイルに対してどのような処理を行いたいかを記述する。
    • 0 : オープンするファイルの属性を問い合わせる
    • GENERIC_READ : 読み取りでファイルをオープンする
    • GENERIC_WRITE : 書き込みでファイルをオープンする
  2. dwShareMode
    他のプロセスがアクセスしたときにどうするかを記述する。
    • 0 : 他のプロセスは操作できない
    • FILE_SHARE_READ : 読み取り可能
    • FILE_SHARE_WRITE : 書き込み可能
  3. lpSecurityAttributes
    Win95では無意味だが、子プロセスへの継承を許可するかどうか
  4. dwCreationDistribution
    記述したファイルが存在していたとき、もしくは存在していなかったときにどう処理するかを記述する
    • CREATE_NEW : 新規にファイルを作成する。ファイルが既に存在していたらこの関数は失敗する。
    • CREATE_ALWAYS : 新規にファイルを作成する。既存のファイルを上書きする。
    • OPEN_EXISTING : ファイルをオープンする。ファイルが存在していたらこの関数は失敗する。
    • OPEN_ALWAYS : もしファイルが存在していたらオープンする。ファイルがなければ新規に作成してオープンする。
    • TRUNCATE_EXISTING : ファイルをオープンするのだがオープンしたあと、ファイルのサイズを0バイトする(削除してオープンする)。少なくとも、dwDesiredAccessパラメータで、GENERIC_WRITEを選択していなければならない。ファイルがなければ失敗する。
  5. dwFlagsAndAttributes
    ファイルの属性を指定する。
    • FILE_ATTRIBUTE_ARCHIVE : アーカイブファイルを対象とする。
    • FILE_ATTRIBUTE_COMPRESSED : 圧縮されたファイル、もしくはディレクトリ。ファイルの場合、ファイル全体が圧縮されていることを意味し、ディレクトリの場合は新規に作成されるファイルやサブディレクトリを意味する。
    • FILE_ATTRIBUTE_NORMAL : 特別な属性を持たないもので、この属性は単独でしか使用できない。
    • FILE_ATTRIBUTE_HIDDEN : 隠しファイル。これは普通のディレクトリ表示に出てこない。
    • FILE_ATTRIBUTE_READONLY : 読み取り専用。アプリケーションは読み込み可能だが、書き込みはできない。
    • FILE_ATTRIBUTE_SYSTEM : OSのファイル、もしくは一部

invoke GlobalAlloc,GMEM_MOVEABLE or GMEM_ZEROINIT,MEMSIZE
mov hMemory,eax
invoke GlobalLock,hMemory
mov pMemory,eax

ファイルがオープンされるときに、ReadFile関数、WriteFile関数で使用するためのメモリを確保する。 このとき、GMEM_MOVEABLEフラグを指定して、Windowsにメモリの管理を頼めるように、移動可能メモリを割り当てる。 GMEM_ZEROINITフラグは、0で初期化されたメモリブロックを作成する。

GlobalAlloc関数が成功すれば、eaxレジスタにメモリブロックのハンドルが戻り値としてセットされている。 このハンドルを今度はGlobalLock関数に渡し、メモリブロックへのポインタをもらうようにする。

invoke ReadFile,hFile,pMemory,MEMSIZE-1,ADDR SizeReadWrite,NULL
invoke SendMessage,hwndEdit,WM_SETTEXT,NULL,pMemory

メモリブロックの使用準備が整えば、ReadFile関数により、ファイルからデータを読み取ることができる。 ファイルが開かれるか、作成されてすぐの状態は、ファイルポインタはファイルの先頭になっている。 なのでこの場合、ファイルの先頭から読んでいくことになる。 ReadFile関数の最初の引数はファイルハンドルで、2番目はメモリブロックへのポインタ、3番目はファイルから何バイト読み込むかを指定し、 4番目の引数はDWORD型の変数で、実際にファイルから何バイト読み込んだかが格納される。

メモリにデータが格納された後、エディットコントロールにファイルの内容を書き出すため、 そのメモリへのポインタをlParamにセットして、WM_SETTEXTメッセージを送信する。 この関数をCALLした後、エディットコントロールはクライアントエリアに表示する。

    invoke CloseHandle,hFile
    invoke GlobalUnlock,pMemory
    invoke GlobalFree,hMemory
.endif

ユーザが何らかの編集をして、セーブするときは違うファイルにセーブするので、 ここまでくれば、このファイルをオープンしておくことは無意味なので、CloseHandle関数で閉じることにする。 次に、メモリブロックのロックを解除し、解放する。 実際には、後でセーブするときにメモリを使用するので、ここでメモリを解放する必要は無いのだが、 デモンストレーションという意味で、あえてここで解放している。

invoke SetFocus,hwndEdit

ファイルオープンダイアログボックスが画面に表示され、フォーカスをそれに移し、 ダイアログボックスが閉じられたらエディットコントロールにフォーカスを移動しなければならない。

これで、ファイルの読み取り操作は終了だ。ここで、ユーザがエディットコントロールに対して何らかの編集を行い、 そのデータを別のファイルにセーブするときには、「File」→「Save」を選択したときにファイルをセーブするダイアログボックスが表示される。 このファイルをセーブするダイアログボックスは、ファイルをオープンするダイアログボックスとほとんど違いがない。 実際に違うところは、関数名だけで、GetOpenFileName に対して、GetSaveFileName という関数名になっている。 なので、ofn構造体のフラグメンバをのぞいてほとんどのメンバを再使用できる。

mov ofn.Flags,OFN_LONGNAMES or OFN_EXPLORER or OFN_HIDEREADONLY

この場合、新規ファイルを作成したいので、OFN_FILEMUSTEXIST と OFN_PATHMUSTEXIST を指定していなければならない。 さもないとダイアログボックスから存在していないファイルを作成することができなくなってしまう。
CreateFile関数のdwCreationDistributionパラメータは新規ファイルを作成するので、CREATE_NEWに変更しなければならない。

残りのコードは以下の部分を除いてファイルをオープンするコードと同じであるので、以下の部分だけを説明する。

invoke SendMessage,hwndEdit,WM_GETTEXT,MEMSIZE-1,pMemory
invoke WriteFile,hFile,pMemory,eax,ADDR SizeReadWrite,NULL

WM_GETTEXTメッセージをエディットコントロールに送信し、エディットコントロールのデータをメモリにコピーする。 戻り値のeaxレジスタにバッファの大きさが返ってくる。 メモリにデータが収まったら、新規に作成したファイルに書き出す。


[戻る]