HaskellからCのライブラリを使う
はじめにの前に
これはHaskell Advent Calendar 2021の23日目の記事です。長くなってしまったのと、時間がなくて説明が雑になっているところが多々あるかと。C言語で書かれたライブラリをHaskellから使うときに「ここどうやったらいいかな」というのを自力で解決してきた、いくつかの手法を、ひとつのライブラリをHaskellから使っていくというストーリーのなかで解説してみました。今後また手を入れて洗練させていきたいと思っています。
はじめに
ふだんHaskellで楽していたとしても、もっと楽しいことをやろうとしたら、多くの場合にC言語のライブラリを使うことになる。Cの関数を使うことは楽しくスリリングな作業になる。柵のない屋上でローラースケートを楽しむようなものだ。使い捨てのコードであれば、FFIで取り込んだ関数をそのまま使えばいい。そうではなく、すこし腰をすえて取り組むコードを書くときには、屋上に柵を作りローラースケート用のコースを整備していくことになる。そういった作業のなかで、いろいろな問題を解決してきた。その「問題と解決方法のひとつ」をここで共有したい。
記事を書くうえで、C言語の既存のライブラリを対象とするのには、つぎのような問題がある。
- APIが複雑すぎて1記事にはおさまらなくなる
- 環境によってどのライブラリが使えるかが異なる
これらの問題を解消するために、ここでは新たに単純なC言語によるライブラリを作成し、それをHaskellから使用するようにする。この記事は大きくふたつの部分に分けられる。
- ビルド時に必要なオプションについて
- Cのライブラリを設計して、それをHaskellから使う
既存のパッケージを使うときにビルド時に必要なオプションを説明する小さな部分と、Cの関数や値を実際に使用していく比較的大きな部分とのふたつだ。前者ではLinuxを前提に説明していくが、後者はより広い環境で試せる内容になっている。前者と後者は分かれているので、前者を読み飛ばして、本題となる後者の部分から読んでいくこともできる。
ビルド時に必要なオプションについて
この部分についてはLinux環境のみをターゲットにしている。この部分で大事なことはつぎの2つだ。
-
pkg-config
が使えるライブラリであればpackage.yaml
のpkg-config-dependencies
を追加する - そうでなければ、
include-dirs
,extra-lib-dirs
,extra-libraries
を追加する
Linuxユーザでなければ「Cのライブラリを設計して、それをHaskellから使う」へ飛ぼう。
ダミーのライブラリを作成
ビルドに必要なオプションなどを説明するための小さなコードを作成する。適当なディレクトリを作成し、そこにファイルを配置する。
% cd ~
% mkdir zenn
% mkdir zenn/use_c
% mkdir zenn/use_c/c_source
% cd ~/zenn/use_c/c_source
#include "foo.h"
int
add(int x, int y) { return x + y; }
#ifndef _FOO_H
#define _FOO_H
#define FOO 123
int add(int x, int y);
#endif
定数FOO
と関数add
とを定義している。コンパイルして、ホームディレクトリ下のディレクトリ.zenn/
の下に配置する。
% mkdir ~/.zenn
% mkdir ~/.zenn/include
% mkdir ~/.zenn/lib
% cp foo.h ~/.zenn/include
% gcc --shared -o libfoo.so foo.c
% cp libfoo.so ~/.zenn/lib
C言語から呼んでみる
コンパイルオプションを指定
C言語のmain
からこのプチライブラリを呼んでみよう。
#include <stdio.h>
#include <foo.h>
int
main(int argc, char *argv[])
{
printf("%d\n", FOO);
printf("%d\n", add(3, 8));
}
コンパイルして試してみよう。動的リンクをするので実行時には、環境変数LD_LIBRARY_PATH
の設定が必要になる。
% gcc -I~/.zenn/include -L~/.zenn/lib -lfoo main.c -o use_foo
% LD_LIBRARY_PATH=~/.zenn/lib ./use_foo
123
11
pkg-configを使う
pkg-configとは、ライブラリを利用する際に必要となる各種フラグやパス等を、共通したインターフェースで提供するための手段である。
ライブラリの使用者がひとつひとつ-I/include/directory
-L/lib/directory
-lfoo
のようにオプションを手書きしなくても、つぎのように書けば必要なフラグが指定できるようにしてくれる仕組みだ。
% gcc `pkg-config foo --cflags --libs` main.c -o use_foo
このように使用者の便宜を図るために、ライブラリの作者は.pc
ファイルを作成する必要がある。
% cat foo.pc
prefix=/home/your_user_name/.zenn
libdir=${prefix}/lib
includedir=${prefix}/include
Name: Foo
Description: Foo
version: 0.1
Libs: -L${libdir} -lfoo
Cflags: -I${includedir}
foo.pc
を.zenn/
の下に配置する。
% mkdir ~/.zenn/share
% mkdir ~/.zenn/share/pkgconfig
% cp foo.pc ~/.zenn/share/pkgconfig
そのままだとpkg-config
は.pc
ファイルを見つけるのにディレクトリ/usr/share/pkgconfig
を探す。環境変数PKG_CONFIG_PATH
に指定することで、そのディレクトリを探してくれる。
% PKG_CONFIG_PATH=~/.zenn/share/pkgconfig pkg-config foo --cflags --libs
-I/home/your_user_name/.zenn/include -L/home/youre_user_name/.zenn/lib -lfoo
これで、ちまちまとフラグなどを指定せずにコンパイルができる。
% gcc `PKG_CONFIG_PATH=~/.zenn/share/pkgconfig pkg-config foo --cflags --libs` main.c -o use_foo
Haskellから呼んでみる
コンパイルオプションを指定する
まずはディレクトリのパスなどを直接指定するやりかたで、ライブラリfoo
を使ってみる。パッケージtry-use-foo
を作成する。
% cd ~/zenn/use_c
% stack new try-use-foo
% cd try-use-foo
ファイルpackage.yaml
を編集する。
...
library:
source-dirs: src
include-dirs: /home/your_user_name/.giita/include
extra-lib-dirs: /home/your_user_name/.zenn/lib
extra-libraries: foo
...
ファイルsrc/Lib.hs
の名前をsrc/Lib.hsc
に変更し、編集する。.hsc
ファイルはhsc2hs
で処理される。C言語の定数や構造体をあつかうことができる。
module Lib where
import Foreign.C.Types
#include <foo.h>
foo :: CInt
foo = #{const FOO}
foreign import ccall "add" c_add :: CInt -> CInt -> CInt
#include
でfoo.h
を導入し、#const
で定数FOO
を読み込んでいる。また、foreign import
でC言語の関数add
をc_add
という名前で取り込んでいる。つぎに、ファイルapp/Main.hs
を編集する。
module Main where
import Lib
main :: IO ()
main = do
print foo
print $ c_add 3 8
コンパイルして試してみる。
% stack build
% LD_LIBRARY_PATH=~/.zenn/lib stack exec try-use-foo-exe
123
11
pkg-configを利用する
pkg-config
を利用すれば、もっとスマートに設定できる。
% cd ~/zenn/use_c
% stack new try-use-foo-pkg
% cd try-use-foo-pkg
ファイルpackage.yaml
を編集する。
...
library:
source-dirs: src
pkg-config-dependencies: foo
...
src/Lib.hsc
やapp/Main.hs
はpkg-config
を利用しない版とおなじだ。
% rm src/Lib.hs
% cp ../try-use-foo/src/Lib.hsc src
% cp ../try-use-foo/app/Main.hs app
ビルド時に環境変数PKG_CONFIG_PATH
に~/.zenn/share/pkgconfig
を指定する必要がある。
% PKG_CONFIG_PATH=~/.zenn/share/pkgconfig stack build
% LD_LIBRARY_PATH=~/.zenn/lib stack exec try-use-foo-pkg-exe
123
11
pkg-config
に対応したライブラリなら、package.yaml
でpkg-config-dependencies
を設定してやるだけで、いろいろなフラグをうまく指定してくれるということだ。
Cのライブラリを設計して、それをHaskellから使う
ソースコード
ソースコードは以下にある。
はじめに
CのライブラリとしてCLIの画面に「人間」を描画するライブラリを考える。また、そのライブラリは一定時間ごとにイベントを発生させるものとする。イベントは時間の経過を表すTick
イベントと入力された文字を表すChar
イベントがあるとする。そういうC言語のライブラリを使って、右から流れてくる「敵」を「主人公」が踏んでいくゲームを作る。
.....
.\o..
..A\.
./.\.
.....
これが主人公である。で、
.....
.\O/.
..A..
./.\.
.....
これが敵だ。
実際にこの記事であつかうのは、Cのライブラリの作成と、それをHaskellから使う機構のところまでとする。ゲームそのもののコードはGitHubを参照してほしい。
記事を読む前に「ゲーム」を試してみるには、つぎのようにする。
% git clone https://github.com/YoshikuniJujo/zenn-samples
% cd zenn-samples/zenn-use-c-lib
% stack build
% stack exec try-game
- 'h': 左に歩き出す
- 2回目の'h': 左に走り出す
- 'l': 右に歩き出す
- 2回目の'l': 右に走り出す
- 'j': 立ち止まる
- 'k': ジャンプ
準備
stack new
でプロジェクトを生成して、C言語のソースコードとヘッダファイルを格納するディレクトリを作成する。実戦では既存のCライブラリを使用するが、ここではC言語のライブラリを作りながら、同時にHaskellから使用するためのコードを書いていく。
まずはプロジェクトを作成する。名前はzenn-use-c-lib
とでもしておこうか。
% stack new zenn-use-c-lib
% cd zenn-use-c-lib
ヘッダファイルを格納するディレクトリを作成し、そのディレクトリをpackage.yaml
で設定する。
% mkdir include
% vi package.yaml
...
library:
source-dirs: src
include-dirs: include
...
ディレクトリinclude/
を作成し、設定ファイルpackage.yaml
にinclude-dirs:
の行を追加した。つぎにC言語のソースコードを格納するディレクトリcsrc
と空のソースコードcsrc/human.c
を作成し、設定ファイルに必要な行を追加する。
% mkdir csrc
% touch csrc/human.c
% vi package.yaml
...
library:
source-dirs: src
include-dirs: include
c-sources:
- csrc/human.c
...
ここで一度ビルドを試してみよう。
% stack build
単純な関数
まずは単純な関数をC言語側で定義してHaskell側から使ってみよう。「人間」の位置を引数にとり、その左右上下の位置を返す関数だ。
int hm_left(int x, int y) { return x; }
int hm_right(int x, int y) { return x + 2; }
int hm_top(int x, int y) { return y; }
int hm_bottom(int x, int y) { return y + 2; }
「人間」は以下のような見た目をしている。
\o.
.A\
/.\
「人間の位置」である(x, y)
は左上の位置を示すので、左右上下はそれぞれx
, x + 2
, y
, y + 2
となる。これらの関数にHaskell側からアクセスしてみよう。
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
module Human where
import Foreign.C.Types
foreign import ccall "hm_left" left :: CInt -> CInt -> CInt
foreign import ccall "hm_right" right :: CInt -> CInt -> CInt
foreign import ccall "hm_top" top :: CInt -> CInt -> CInt
foreign import ccall "hm_bottom" bottom :: CInt -> CInt -> CInt
C言語の関数を呼び出すHaskellの関数を定義している。書式はつぎのようになる。
foreign import ccall "Cの関数名" Haskellの関数名 :: 型
Foreign.C.Types.CInt
はC言語の型int
に対応するHaskellの型だ。
ビルドして対話環境で試してみよう。
% stack build
% stack ghci
> left 8 5
8
> right 8 5
10
> top 8 5
5
> bottom 8 5
7
あとで使うので左右上下から「人間の位置」のX座標、Y座標の値を計算する関数も定義しておく。
int hm_x_from_left(int l) { return l; }
int hm_x_from_right(int r) { return r - 2; }
int hm_y_from_top(int t) { return t; }
int hm_y_from_bottom(int b) { return b - 2; }
foreign import ccall "hm_x_from_left" xFromLeft :: CInt -> CInt
foreign import ccall "hm_x_from_right" xFromRight :: CInt -> CInt
foreign import ccall "hm_y_from_top" yFromTop :: CInt -> CInt
foreign import ccall "hm_y_from_bottom" yFromBottom :: CInt -> CInt
定数
「人間」を表示していくフィールドの大きさを定数FIELD_WIDTH
とFIELD_HEIGHT
で定義する。
#ifndef _HUMAN_H
#define _HUMAN_H
#define FIELD_WIDTH 79
#define FIELD_HEIGHT 23
#endif
C言語の定数をHaskellから使うためにhsc2hsを使う。Stackだと拡張子を.hs
から.hsc
にすればいい。
% mv src/Human.hs src/Human.hsc
ヘッダファイルhuman.h
を導入する。
import Foreign.C.Types
#include <human.h>
#include
はhsc2hsによって処理される。導入されたヘッダに定義された定数などがhsc2hsの続く処理に使用される。定数fieldWidth
とfieldHeight
を定義する。
fieldWidth, fieldHeight :: CInt
fieldWidth = #{const FIELD_WIDTH}
fieldHeight = #{const FIELD_HEIGHT}
#{...}
のように書かれた部分はhsc2hsによって処理される。定数の展開の書式はつぎのようになる。
#{const 定数名}
ビルドして試してみよう。
% stack build
% stack ghci
> fieldWidth
79
> fieldHeight
23
ヘッダファイルhuman.h
を作成したので、Cのソースコード内でもincludeしておこう。ついでに、今後使用するヘッダファイルもここでincludeする。
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <sys/select.h>
#include <human.h>
入出力
フィールドを格納する2次元配列を定義する。
typedef char HmFieldArray[FIELD_HEIGHT][FIELD_WIDTH + 1];
typedef char (*HmField)[FIELD_WIDTH + 1];
型HmFieldArray
は配列の領域確保などに使用し、型HmField
はそれに対するアクセスに使用する。また、文字列の終わりを示す'\0'
を格納するために、FIELD_WIDTH
ではなくFIELD_WIDTH + 1
のようにしてある。
まずは、静的に確保されたフィールドとしてhm_field0
を宣言する。
HmFieldArray hm_field0;
この領域を初期化する関数と、領域を標準出力に書き出す関数を定義する。
void
hm_field0_init(void)
{
for (int i = 0; i < FIELD_HEIGHT; i++) {
int j;
for (j = 0; j < FIELD_WIDTH; j++) hm_field0[i][j] = '.';
hm_field0[i][j] = '\0';
}
}
void
hm_field0_draw(void)
{
for (int i = 0; i < FIELD_HEIGHT; i++) printf("%s\n", hm_field0[i]);
}
ごく普通のforループだ。関数hm_field0_init
では、それぞれの行ごとに79個の'.'
とそれに続く、ひとつの'\0'
とを配置している。関数hm_field0_draw
では、それぞれの行を文字列として出力する。Haskell側から使えるようにする。
foreign import ccall "hm_field0_init" field0Init :: IO ()
foreign import ccall "hm_field0_draw" field0Draw :: IO ()
状態変化や入力、出力がからんでくる場合には返り値をIO
値にする。ビルドして試してみる。
% stack build
% stack ghci
> field0Init
> field0Draw
....................
例外処理
静的に確保されたフィールドに「人間」を置いていこうと思う。もし「人間」がフィールドからはみ出るような場合や、完全にフィールドの外に出るような位置を指定された場合のことを考える。そういう場合への対策として、いくつか考えられるが、ここでは「例外」を引き起こすようにしよう。
Cの関数から返すエラーを表す値
まずはC言語の側に「人間を配置しようとした」ときに起こる正常終了やエラーを表す列挙型HmPutHumanResult
を定義する。
typedef enum {
HM_PUT_HUMAN_SUCCESS, HM_PUT_HUMAN_PARTIAL, HM_PUT_HUMAN_OFFSCREEN }
HmPutHumanResult;
つぎのようになる。
-
HM_PUT_HUMAN_SUCCESS
: 正常終了 -
HM_PUT_HUMAN_PARTIAL
: 「人間」が一部はみ出る -
HM_PUT_HUMAN_OFFSCREEN
: 「人間」が全部フィールドの外に出てしまう
列挙型の特徴として上記の3つの値以外の値を取り得ることもることに注意が必要だ。
例外を表す型
Haskell側での例外を表す型を定義しよう。例外を表す型は階層構造にすることができる。たとえば、つぎのような階層があったとする。
AError +- BError
|
+- CError
このときBError
とCError
のどちらかだけを別々に捕捉することもできるが、捕捉する例外としてAError
を指定することでBError
とCError
の両方を捕捉することができる。ここでは、つぎのような階層構造を持つ型を定義する。
PutHumanError +- PutHumanOutOfFieldError +- PutHumanPartialError
| |
| +- PutHumanOffscreenError
+- PutHumanUnknownError
それぞれ、つぎのような意味だ。
- PutHumanError: 「人間」を配置するときに生じるエラー
- PutHumanOutOfFieldError: 「人間」の全体または一部がフィールドの外に出てしまう
- PutHumanPartialError: 「人間」の一部がフィールドの外にはみ出る
- PutHumanOffscreenError: 「人間」の全体がフィールドの外に出てしまう
- PutHumanUnknownError: t 「人間」を配置するときに生じる、その他のエラー
このような階層構造は型クラスの仕組みを使って、巧妙なやりかたで定義できる。定義できるが、いろいろと煩雑なコードを書く必要がある。パッケージexception-hierarchy
のモジュールControl.Exception.Hierarchy
には、TemplateHaskellを使って、この階層構造を定義する仕組みが用意されている。
パッケージexception-hierarchy
をpackage.yaml
の「依存するパッケージ」に追加する。
dependencies:
- base >= 4.7 && < 5
- exception-hierarchy
モジュールHuman.Exception
に例外の型を定義する。
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE PatternSynonyms #-}
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
module Human.Exception where
import Control.Exception
import Control.Exception.Hierarchy
import Data.Typeable
import Data.Word
#include <human.h>
data PutHumanPartialError = PutHumanPartialError deriving (Typeable, Show)
data PutHumanOffscreenError = PutHumanOffscreenError deriving (Typeable, Show)
data PutHumanUnknownError = PutHumanUnknownError #{type HmPutHumanResult}
deriving (Typeable, Show)
exceptionHierarchy Nothing (
ExNode "PutHumanError" [
ExNode "PutHumanOutOfFieldError" [
ExType ''PutHumanPartialError,
ExType ''PutHumanOffscreenError ],
ExType ''PutHumanUnknownError ] )
まずは階層構造の葉になるデータ型を定義している。最後のPutHumanUnknownError
は、C言語の列挙型HmPutHumanResult
の想定されていない値が返ったときのための型だ。hsc2hsは#{type Foo}
のようにすると、C言語上の型FooのHaskell上での対応する型に展開してくれる。関数exceptionHierarchy
は、例外を表す型が、第2引数の木構造に示されるような形の階層構造になるようなソースコードを生成する。
指定した例外を捕捉する関数を書く。新たにモジュールTryHuman
を作成しよう。
{-# LANGUAGE ScopedTypeVariables, AllowAmbiguousTypes #-}
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
module TryHuman where
import Control.Exception
catchAndShow :: forall e . Exception e => IO () -> IO ()
catchAndShow act = act `catch` \(e :: e) -> putStrLn "CATCHED" >> print e
ビルドして試してみる。
% stack build
% stack ghci
> :set -XTypeApplications
> :module + Control.Exception
> catchAndShow @PutHumanPartialError $ throw PutHumanPartialError
CATCHED
PutHumanPartialError
> catchAndShow @PutHumanPartialError $ throw PutHumanOffscreenError
*** Exception: PutHumanOffscreenError
> catchAndShow @PutHumanOffscreenError $ throw PutHumanPartialError
*** Exception: PutHumanPartialError
> catchAndShow @PutHumanOffscreenError $ throw PutHumanOffscreenError
CATCHED
PutHumanOffscreenError
> catchAndShow @PutHumanOutOfFieldError $ throw PutHumanPartialError
CATCHED
PutHumanPartialError
> catchAndShow @PutHumanOutOfFieldError $ throw PutHumanOffscreenError
CATCHED
PutHumanOffscreenError
捕捉する例外をPutHumanPartialError
やPutHumanOffscreenError
にした場合には、それぞれの例外だけを捕捉し、PutHumanOutOfFieldError
を指定した場合には両方の例外が捕捉されているのがわかる。
列挙型
列挙型をHaskellから使う
C言語側では「人間」を置く関数は成功やエラーを列挙型HmPutHumanResult
で返す。この型は以下の値を取り得る。
HM_PUT_HUMAN_SUCCESS
HM_PUT_HUMAN_PARTIAL
HM_PUT_HUMAN_OFFSCREEN
- その他の値
C言語の列挙型をあつかうのに便利な道具として、パッケージc-enum
のForeign.C.Enum.enum
がある。パッケージc-enum
が使えるように、package.yaml
の「依存するパッケージ」にc-enum
を追加する。
...
dependencies:
- base >= 4.7 && < 5
- exception-hierarchy
- c-enum
...
言語拡張TemplateHaskell
とPatternSynonyms
を使うのでモジュールHuman
の先頭にプラグマをつける。
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE PatternSynonyms #-}
...
モジュールForeign.C.Enum
を導入する。また、型として#{type HmPutHumanResult}
を使うが、これはWord32
に展開されるので、モジュールData.Word
を導入する必要がある。
...
import Foreign.C.Types
import Foreign.C.Enum
import Data.Word
...
関数enum
はC言語の列挙型をHaskell側から利用するためのコードを生成する。モジュールHuman
に、つぎのように定義する。
enum "PutHumanResult" ''#{type HmPutHumanResult} [''Show, ''Read] [
("PutHumanResultSuccess", #{const HM_PUT_HUMAN_SUCCESS}),
("PutHumanResultPartial", #{const HM_PUT_HUMAN_PARTIAL}),
("PutHumanResultOffscreen", #{const HM_PUT_HUMAN_OFFSCREEN}) ]
これは、だいたい、つぎのようなコードに展開される。
newtype PutHumanResult = PutHumanResult #{type HmPutHumanResult}
pattern PutHumanResultSuccess :: PutHumanResult
pattern PutHumanResultSuccess <- PutHumanResult #{const HM_PUT_HUMAN_SUCCESS}
where
PUtHumanResultSuccess = PutHumanResult #{const HM_PUT_HUMAN_SUCCESS}
...
これは、たとえばPutHumanResult 0
に別名としてPutHumanResultSuccess
という名前が用意されるということだ。そして、その別名はパターンマッチにも使える。また、クラスShow
やRead
のインスタンス宣言が「いい感じ」に定義される。ビルドして試してみよう。
% stack build
% stack ghci
> PutHumanResultSuccess
PutHumanResultSuccess
> PutHumanResultPartial
PutHumanResultPartial
> PutHumanResult 1
PutHumanResultPartial
> PutHumanResult 123
PutHumanResult 123
> case PutHumanResultPartial of PutHumanResultPartial -> 123; _ -> 456
123
> read "PutHumanResultPartial" :: PutHumanResult
PutHumanResultPartial
> read "PutHumanResult 1" :: PutHumanResult
PutHumanResultPartial
> read "PutHumanResult 123" :: PutHumanResult
PutHumanResult 123
別名が値としてもパターンとしても使えることがわかる。また、クラスShow
のインスタンスとしては「別名」のほうが優先して表示されること、クラスRead
のインスタンスとしては別名でも「もともとの名前」でも読み込むことができることがわかる。
一文字、フィールドに置く
「人間」をフィールドに置くために、まずは文字をひとつフィールドに置く関数を定義する。
void
hm_field0_put_char(int x, int y, char c)
{
if (0 <= x && x < FIELD_WIDTH && 0 <= y && y < FIELD_HEIGHT)
hm_field0[y][x] = c;
}
引数x, yで指定した位置がフィールド内であれば、その位置の文字を指定された値に置き換える。そうでなければ何もしない。
「人間」を置く
「人間」の位置を指定したとき、つぎの3つの状態が考えられる。
- フィールドにおさまる
- フィールドから一部分はみ出る
- 全体がフィルドの外に出てしまう
この3つの状態のうち、どの状態になるかを調べる関数を定義する。
HmPutHumanResult
hm_check_inside(int x, int y)
{
if ( 0 <= hm_left(x, y) && hm_right(x, y) < FIELD_WIDTH &&
0 <= hm_top(x, y) && hm_bottom(x, y) < FIELD_HEIGHT )
return HM_PUT_HUMAN_SUCCESS;
else if (
0 <= hm_right(x, y) && hm_left(x, y) < FIELD_WIDTH &&
0 <= hm_bottom(x, y) && hm_top(x, y) < FIELD_HEIGHT )
return HM_PUT_HUMAN_PARTIAL;
else return HM_PUT_HUMAN_OFFSCREEN;
}
「人間」の左端が0以上で右端がフィールドの幅未満、かつ上端が0以上で下端がフィールドの高さ未満なら、「人間」の全体がフィールドにおさまる。HM_PUT_HUMAN_SUCCESS
を返す。「人間」の右端が0以上で左端が幅未満で、下端が0以上で上端が高さ未満ならば「人間」の一部はフィールド内だ。HM_PUT_HUMAN_PARTIAL
を返す。そうでなければ、 「人間」の全体がフィールドの外にある。HM_PUT_HUMAN_OFFSCREEN
を返す。
関数hm_field0_put_char
, hm_field0_draw
, hm_check_inside
を使って、「人間」を静的に確保されたフィールドに置き、それを標準出力に出力する関数を書く。
HmPutHumanResult
hm_field0_draw_human(int x, int y)
{
hm_field0_put_char(x, y, '\\');
hm_field0_put_char(x + 1, y, 'o');
hm_field0_put_char(x + 1, y + 1, 'A');
hm_field0_put_char(x + 2, y + 1, '\\');
hm_field0_put_char(x, y + 2, '/');
hm_field0_put_char(x + 2, y + 2, '\\');
hm_field0_draw();
return hm_check_inside(x, y);
}
「人間」を構成する、それぞれの文字をフィールドに置き、それを関数hm_field0_draw
で出力する。「人間」がフィールドにおさまったかどうかを返り値として返す。
これをHaskell側から利用できるようにする。まずは、必要な言語拡張LambdaCase
と導入するモジュールControl.Exception
とHuman.Exception
を追加する。あとで必要になるので言語拡張BlockArguments
も追加しておこう。
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE BlockArguments, LambdaCase #-}
{-# LANGUAGE PatternSynonyms #-}
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
module Human where
import Foreign.C.Types
import Foreign.C.Enum
import Control.Exception
import Data.Word
import Human.Exception
...
関数field0DrawHuman
を定義する。
foreign import ccall "hm_field0_draw_human"
c_hm_field0_draw_human :: CInt -> CInt -> IO PutHumanResult
field0DrawHuman :: CInt -> CInt -> IO ()
field0DrawHuman x y = c_hm_field0_draw_human x y >>= \case
PutHumanResultSuccess -> pure ()
PutHumanResultPartial -> throw PutHumanPartialError
PutHumanResultOffscreen -> throw PutHumanOffscreenError
PutHumanResult n -> throw $ PutHumanUnknownError n
C言語側の関数をc_hm_field0_draw_human
という名前で呼び出す。この関数の返り値によって、呼び出しが成功したことにするか、例外を発生させるか、またどの例外を発生させるかを決める。ビルドして試してみよう。
% stack build
% stack ghci
> field0Init
> field0DrawHuman 30 15
...(人間が表示される)...
> field0DrawHuman 30 22
...
***Exception: PutHumanPartialError
> field0DrawHuman 30 25
...
***Exception: PutHumanOffscreenError
GCを使う
ここまでは静的に確保されたフィールドを利用してきた。しかし、より柔軟に、複数のフィールドを使いたくなることもある。フィールドを動的に確保するような関数を定義しよう。動的に確保されたメモリーは解放する必要がある。ここでは、HaskellのGCのシステムを利用して自動でのメモリの解放を試してみよう。
まずはC言語側で、必要な関数を定義する。
void
hm_field_clear(HmField f)
{
for (int i = 0; i < FIELD_HEIGHT; i++) {
int j;
for (j = 0; j < FIELD_WIDTH; j++) f[i][j] = '.';
f[i][j] = '\0'; }
}
HmField
hm_field_new(void)
{
HmField f;
f = (HmField)malloc(sizeof(HmFieldArray));
hm_field_clear(f);
return f;
}
void hm_field_destroy(HmField f) { free(f); }
void
hm_field_draw(HmField f)
{
for (int i = 0; i < FIELD_HEIGHT; i++) printf("%s\n", f[i]);
}
void
hm_field_put_char(HmField f, int x, int y, char c)
{
if (0 <= x && x < FIELD_WIDTH && 0 <= y && y < FIELD_HEIGHT)
f[y][x] = c;
}
HmPutHumanResult
hm_field_put_human(HmField f, int x, int y)
{
hm_field_put_char(f, x, y, '\\');
hm_field_put_char(f, x + 1, y, 'o');
hm_field_put_char(f, x + 1, y + 1, 'A');
hm_field_put_char(f, x + 2, y + 1, '\\');
hm_field_put_char(f, x, y + 2, '/');
hm_field_put_char(f, x + 2, y + 2, '\\');
return hm_check_inside(x, y);
}
6つの関数を定義した。関数hm_field_clear
, hm_field_draw
, hm_field_put_char
の3つは、それぞれ関数hm_field0_clear
, hm_field0_draw
, hm_field0_put_char
を、静的なfield0の代わりに、引数としてとったフィールドを使うように修正したものだ。関数hm_field_put_human
も同様に関数hm_field0_draw_human
を、引数としてとったフィールドを使うように修正したものだが、それだけではなく、標準出力への出力をわけて純粋にフィールドへの書き込みだけに機能をしぼっている。残りの関数hm_field_new
とhm_field_destroy
はそれぞれ、フィールドのメモリの確保と解放を行う。
これらのC言語の関数を使ってHaskell側にフィールドを生成して使用する仕組みを組み立てていく。型Ptr
やForeignPtr
を使うのでモジュールForeign.Ptr
とForeign.ForeignPtr
が必要だ。また、Foreign.Concurrent.newForeignPtr
を使うのでモジュールForeign.Concurrent
も導入する。
...
module Human where
import Foreign.Ptr
import Foreign.ForeignPtr hiding (newForeignPtr)
import Foreign.Concurrent
import Foreign.C.Types
import Foreign.C.Enum
import Control.Exception
import Data.Word
import Human.Exception
...
データ型Field
を定義する。
newtype Field s = Field (ForeignPtr (Field s)) deriving Show
型引数s
はこの時点では必要ないが後の話題で必要になる。今のところは気にしないでほしい。ForeignPtr
はポインタとそのポインタを解放するための処理をまとめたもので、このポインタは参照されなくなったあとに、その処理によって解放される。Field
型の値を生成するには、C言語側の関数hm_field_new
とhm_field_destroy
とを使う。
foreign import ccall "hm_field_new" c_hm_field_new :: IO (Ptr (Field s))
foreign import ccall "hm_field_destroy"
c_hm_field_destroy :: Ptr (Field s) -> IO ()
fieldNewRaw :: IO (Field s)
fieldNewRaw = Field <$> do
p <- c_hm_field_new
newForeignPtr p $ c_hm_field_destroy p
動作fieldNewRaw
の名前の-Raw
の部分は、今は気にしないでほしい。動作c_hm_field_new
によって返されたポインタp
と、それを解放する処理c_hm_field_destroy p
とを関数newForeignPtr
によってまとめることで、ForeignPtr
型の値を生成している。
関数fieldClearRaw
, fieldDrawRaw
, fieldPutHumanRaw
を定義する。
foreign import ccall "hm_field_clear" c_hm_field_clear :: Ptr (Field s) -> IO ()
foreign import ccall "hm_field_draw" c_hm_field_draw :: Ptr (Field s) -> IO ()
foreign import ccall "hm_field_put_human" c_hm_field_put_human ::
Ptr (Field s) -> CInt -> CInt -> IO PutHumanResult
fieldClearRaw :: Field s -> IO ()
fieldClearRaw (Field ff) = withForeignPtr ff c_hm_field_clear
fieldDrawRaw :: Field s -> IO ()
fieldDrawRaw (Field ff) = withForeignPtr ff c_hm_field_draw
fieldPutHumanRaw :: Field s -> CInt -> CInt -> IO ()
fieldPutHumanRaw (Field ff) x y = withForeignPtr ff \pf ->
c_hm_field_put_human pf x y >>= \case
PutHumanResultSuccess -> pure ()
PutHumanResultPartial -> throw PutHumanPartialError
PutHumanResultOffscreen -> throw PutHumanOffscreenError
PutHumanResult n -> throw $ PutHumanUnknownError n
関数withForeignPtr
はForeignPtr a -> (Ptr a -> IO b) -> IO b
型の関数であり、第2引数の関数に対して第1引数がもつポインタを引数としてあたえるという働きをする。第2引数である関数が処理を行っているあいだは、そのポインタは解放されることはない。
ビルドして試してみる。
% stack build
% stack ghci
> f <- fieldNewRaw
> fieldPutHuman f 30 15
> fieldDraw f
.....(「人間」が表示される)...
STモナドとIOモナド
Image
Field
型の値は変化する状態を指し示すタグのようなものと考えられる。変化しない「値そのもの」と見なせる値の型を用意すると、いろいろと便利だ。そのようなImage型を用意する。Field
型と同様に定義するが、なかの状態を変化させないように気をつけることで「値そのもの」と見なすことができる。
まずは、C言語側で型HmImage
とHmField
型の値からHmImage
型の値を生成する関数を定義する。
typedef char HmImageArray[FIELD_HEIGHT][FIELD_WIDTH + 1];
typedef char (*HmImage)[FIELD_WIDTH + 1];
HmImage
hm_field_get_image(HmField f)
{
HmImage img = (HmImage)malloc(sizeof(HmImageArray));
memcpy(img, f, (FIELD_WIDTH + 1) * FIELD_HEIGHT);
return img;
}
void hm_image_destroy(HmImage img) { free(img); }
void
hm_image_draw(HmImage img)
{
for (int i = 0; i < FIELD_HEIGHT; i++) printf("%s\n", img[i]);
}
ここでは、HmField
型の値もHmImage
型の値も、どちらも「char
の2次元配列」なのでわかりにくいが、たとえばウィンドウの描画内容を画像データとして取り出すような場面を想像してほしい。
これをHaskell側から利用する。
newtype Image = Image (ForeignPtr Image) deriving Show
foreign import ccall "hm_field_get_image"
c_hm_field_get_image :: Ptr (Field s) -> IO (Ptr Image)
foreign import ccall "hm_image_destroy" c_hm_image_destroy :: Ptr Image -> IO ()
fieldGetImageRaw :: Field s -> IO Image
fieldGetImageRaw (Field ff) = Image <$> withForeignPtr ff \pf -> do
p <- c_hm_field_get_image pf
newForeignPtr p $ c_hm_image_destroy p
foreign import ccall "hm_image_draw" c_hm_image_draw :: Ptr Image -> IO ()
imageDraw :: Image -> IO ()
imageDraw (Image fi) = withForeignPtr fi c_hm_image_draw
関数fieldGetImageRaw
とimageDraw
とを定義した。ビルドして試してみよう。
% stack build
% stack ghci
> f <- fieldNewRaw
> fieldPutHuman f 30 10
> img <- fieldGetImageRaw f
> imageDraw img
...(「人間」が表示される)...
Field
型の値から出力するのと何の変わりもないように思えるが、感じとしてはウィンドウの表示領域から画像を取り出して、その画像を表示したようなイメージだ。Field
型の値は変化していく「状態」だけど、Image
型の値は変化しない「値」と考える。
STモナド
Field
型の値は変化する。つまり、Field
型の値を変化させる処理はIOモナドのなかでしか使えない。でも、もしField
型の値が特定のスコープのなかで生成されて、そのスコープの外には持ち出せないとしたら、そのスコープを外から見れば状態変化のない純粋な処理に見える。そういう考えかたを仕組みとして用意したものがST
モナドだ。STモナドのなかでのField
型の値を利用を試してみよう。
モジュールControl.Monad.ST
とControl.Monad.ST.Unsafe
が必要だ。
...
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
module Human where
import Foreign.Ptr
import Foreign.ForeignPtr hiding (newForeignPtr)
import Foreign.Concurrent
import Foreign.C.Types
import Foreign.C.Enum
import Control.Monad.ST
import Control.Monad.ST.Unsafe
import Control.Exception
import Data.Word
import Human.Exception
#include <human.h>
...
動作fieldNewSt
と関数fieldPutHumanSt
とfieldGetImageSt
とを定義する。
fieldNewSt :: ST s (Field s)
fieldNewSt = unsafeIOToST fieldNewRaw
fieldPutHumanSt :: Field s -> CInt -> CInt -> ST s ()
fieldPutHumanSt f x y = unsafeIOToST $ fieldPutHumanRaw f x y
fieldGetImageSt :: Field s -> ST s Image
fieldGetImageSt f = unsafeIOToST $ fieldGetImageRaw f
モジュールTryHuman
で純粋なImage
型の値を定義してみる。言語拡張BlockArguments
を使う。また、モジュールControl.Monad.ST
とHuman
とを導入する。
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ScopedTypeVariables, AllowAmbiguousTypes #-}
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
module TryHuman where
import Control.MOnad.ST
import Control.Exception
import Human
...
Image
型の値image1
を定義してみる。
image1 :: Image
image1 = runST do
f <- fieldNewSt
fieldPutHumanSt f 30 10
fieldGetImageSt f
ビルドして試してみる。
% stack build
% stack ghci
> imageDraw image1
...(「人間」が表示される)...
IOモナドとSTモナドをまとめる
ここまでの話ではField
型の値をあつかう処理はIO
モナド用とST
モナド用とを別々に用意しなければならない。パッケージprimitive
で定義されている型クラスPrimMonad
を使えば両方をまとめて定義することができる。型クラスPrimMonad
のメンバ型としてPrimState
が定義されている。モナドIO
とモナドST
とは両方とも、このクラスのインスタンスであり、つぎのようになっている。
type PrimState IO = RealWorld
type PrimState (ST s) = s
型クラスPrimMonad
を使ってField
をあつかう処理のIO
版とST
版とをまとめてみよう。まずは、パッケージprimitive
をpackage.yaml
に追加する。
...
dependencies:
- base >= 4.7 && < 5
- primitive
- exception-hierarchy
- c-enum
...
モジュールControl.Monad.Primitive
を導入する。
...
import Foreign.C.Enum
import Control.Monad.Primitive
import Control.Monad.ST
import Control.Monad.ST.Unsafe
...
動作fieldNew
と関数fieldClear
, fieldPutHuman
, fieldGetImage
とを定義する。
fieldNew :: PrimMonad m => m (Field (PrimState m))
fieldNew = unsafeIOToPrim fieldNewRaw
fieldClear :: PrimMonad m => Field (PrimState m) -> m ()
fieldClear = unsafeIOToPrim . fieldClearRaw
fieldPutHuman :: PrimMonad m => Field (PrimState m) -> CInt -> CInt -> m ()
fieldPutHuman f x y = unsafeIOToPrim $ fieldPutHumanRaw f x y
fieldGetImage :: PrimMonad m => Field (PrimState m) -> m Image
fieldGetImage = unsafeIOToPrim . fieldGetImageRaw
関数fieldDraw
はIO
のみに定義できる。
fieldDraw :: Field RealWorld -> IO ()
fieldDraw = fieldDrawRaw
ビルドして試してみよう。
% stack build
% stack ghci
> f <- fieldNew
> fieldPutHuman f 30 10
> fieldDraw f
...(「人間」が表示される)...
> :set -XBlockArguments
> :module + Control.Monad.ST
> img = runST do f <- fieldNew; fieldPutHuman f 30 10; fieldGetImage f
> imageDraw img
...(「人間」が表示される)...
構造体
「『人間の形』が一種類しかないんじゃつまらない」と思ったとする。左右の手を上下させることができて、さらに頭の大きさを変えられたら(たぶんすこしは)おもしろいんじゃないかな、と。
...........................
....\o......\o/......O.....
.....A\......A....../A\....
..../.\...../.\...../.\....
...........................
そういう「いろいろな形の人間」を表示できるようにしよう。まずは必要な列形体と構造形とをinclude/human.h
に定義する。
typedef enum { HM_SMALL_HEAD, HM_LARGE_HEAD } HmHead;
typedef enum { HM_DOWN_ARM, HM_UP_ARM } HmArm;
typedef struct { HmHead head_size; HmArm left_arm; HmArm right_arm; } HmHuman;
「頭の大きさ」を表す列挙型HmHead
と、「腕の上下」を表す列挙型HmArm
と、それらを使って「人間の形」を表現する構造体HmHuman
を定義した。この構造体HmHuman
を引数としてとり、「色々な形の人間」をフィールドに配置する関数hm_field_put_various_human
を定義する。まずは、頭の大きさによって使う文字を選ぶ関数select_head
と、腕が上か下かによって動作を変える、左右の「腕を配置する」関数put_left_arm
, put_right_arm
とを定義する。
char
select_head(HmHuman *hm)
{
switch (hm->head_size) {
case HM_SMALL_HEAD: return 'o';
case HM_LARGE_HEAD: return 'O';
default: return '.'; }
}
void
put_left_arm(HmField f, HmHuman *hm, int x, int y)
{
switch (hm->left_arm) {
case HM_DOWN_ARM: hm_field_put_char(f, x, y + 1, '/'); return;
case HM_UP_ARM: hm_field_put_char(f, x, y, '\\'); return; }
}
void
put_right_arm(HmField f, HmHuman *hm, int x, int y)
{
switch (hm->right_arm) {
case HM_DOWN_ARM:
hm_field_put_char(f, x + 2, y + 1, '\\'); return;
case HM_UP_ARM: hm_field_put_char(f, x + 2, y, '/'); return; }
}
それぞれswitch
文で「選ぶ文字」や「腕を配置する動作」を選んでいる。これらを使って「いろいろな形の人間」を配置する関数を定義する。
HmPutHumanResult
hm_field_put_various_human(HmField f, HmHuman *hm, int x, int y)
{
hm_field_put_char(f, x + 1, y, select_head(hm));
put_left_arm(f, hm, x, y);
put_right_arm(f, hm, x, y);
hm_field_put_char(f, x + 1, y + 1, 'A');
hm_field_put_char(f, x, y + 2, '/');
hm_field_put_char(f, x + 2, y + 2, '\\');
return hm_check_inside(x, y);
}
この関数をHaskell側から使えるようにしていこう。まずは構造体をHaskell側で生成できるようにする。これはパッケージc-struct
のForeign.C.Struct.struct
を使うと簡潔に書くことができる。パッケージc-struct
を設定ファイルpackage.yaml
に追加する。
...
- exception-hierarchy
- c-enum
- c-struct
...
言語拡張TupleSections
, GeneralizedNewtypeDeriving
, ViewPatterns
を使う。
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE BlockArguments, LambdaCase, TupleSections #-}
{-# LANGUAGE PatternSynonyms, ViewPatterns #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Human where
...
モジュールForeign.C.Struct
を導入する。またForeign.Storable
も必要になる。
...
import Foreign.Concurrent
import Foreign.Storable
import Foreign.C.Types
import Foreign.C.Enum
import Foreign.C.Struct
import Control.Monad.Primitive
...
列挙型HmHead
, HmArm
をHaskell側から使えるようにする。
enum "Head" ''#{type HmHead} [''Show, ''Read, ''Storable] [
("SmallHead", #{const HM_SMALL_HEAD}),
("LargeHead", #{const HM_LARGE_HEAD}) ]
enum "Arm" ''#{type HmArm} [''Show, ''Read, ''Storable] [
("DownArm", #{const HM_DOWN_ARM}), ("UpArm", #{const HM_UP_ARM}) ]
これらをメンバーとして持つデータ型Human
を定義する。
struct "Human" #{size HmHuman}
[ ("headSize", ''Head, [| #{peek HmHuman, head_size} |],
[| #{poke HmHuman, head_size} |]),
("leftArm", ''Arm, [| #{peek HmHuman, left_arm} |],
[| #{poke HmHuman, left_arm} |]),
("rightArm", ''Arm, [| #{peek HmHuman, right_arm} |],
[| #{poke HmHuman, right_arm} |]) ]
[''Show, ''Read]
関数struct
の書式はつぎのようになる。
struct データ型の名前 記憶領域における大きさ
メンバーの定義のリスト
自動でインスタンスにする型のリスト
で、「メンバーの定義」はつぎのようになる。
(メンバーの名前, メンバーの型, メンバーの値の取り出しの処理,
メンバーの値の書き込みの処理)
#{size Foo}
はhsc2hsで処理されて型Foo
のサイズに展開される。#{peek Foo, bar}
は構造体Foo
からの領域bar
の値の取り出しの処理に、#{poke Foo, bar}
は構造体Foo
の領域bar
への書き込みの処理に、それぞれ展開される。
データ型Human
はつぎのように定義されている。
newtype Human = Human_ (ForeignPtr Human)
しかし、「あたかも」つぎのように定義されている「かのように」使える。
data Human = Human { headSize :: Head, leftArm :: Arm, rightArm :: Arm }
ビルドして試してみる。
% stack build
% stack ghci
> Human SmallHead DownArm UpArm
Human {headSize = SmalHead, leftArm = DownArm, rightArm = UpArm}
> Human_ fh = it
> print fh
0x0000000001fcb120
> :type fh
fh :: GHC.ForeignPtr.ForeignPtr Human
これで、「いろいろな形の人間」を表示できるようになる。Haskellの関数fieldPutVariousHuman
を定義する。
foreign import ccall "hm_field_put_various_human"
c_hm_field_put_various_human ::
Ptr (Field s) -> Ptr Human -> CInt -> CInt -> IO PutHumanResult
fieldPutVariousHumanRaw :: Field s -> Human -> CInt -> CInt -> IO ()
fieldPutVariousHumanRaw (Field ff) (Human_ fhm) x y =
withForeignPtr ff \pf -> withForeignPtr fhm \phm ->
c_hm_field_put_various_human pf phm x y >>= \case
PutHumanResultSuccess -> pure ()
PutHumanResultPartial -> throw PutHumanPartialError
PutHumanResultOffscreen -> throw PutHumanOffscreenError
PUtHumanResult n -> throw $ PutHumanUnknownError n
fieldPutVariousHuman ::
PrimMonad m => Field (PrimState m) -> Human -> CInt -> CInt -> m ()
fieldPutVariousHuman f hm x y =
unsafeIOToPrim $ fieldPutVariousHumanRaw f hm x y
Human
型の値をHuman_ fhm
にマッチさせることでForeignPtr Human
型の値fhm
を取り出して、そこから関数withForeignPtr
でポインタを取り出して関数c_hm_field_put_various_human
に第2引数としてあたえている。それ以外については概出の話を組み合わせただけだ。ビルドして試してみる。
% stack build
% stack ghci
> f <- fieldNew
> fieldPutVariousHuman f (Human LargeHead UpArm UpArm) 30 10
> fieldDraw f
...(フィールドのなかに、つぎのような人間が表示される)...
........................................................
........................................................
.....\O/................................................
......A.................................................
...../.\................................................
........................................................
C言語の側で構造体を更新する
「C言語の構造体をHaskellのデータ型で表現できました(パチパチ)」というところまでは良かったのだけど、もしもC言語の関数が構造体の領域への代入によって構造体の内容を変化させたとすると、中身が変わるはずのないHaskellの値(のように見せているもの)が変化してしまい、わけがわからなくなる。多少のオーバーヘッドが生じるが、Haskellのデータ型として利用する領域は変化させずに、コピーを生成したうえで、コピーのほうをいじるほうが安全だ。
よくあるライブラリの作りとして、構造体をコピーする関数を用意してあることがある。そのような「用意されている構造体をコピーする関数」を使って、C言語側で変化させる目的で作られた領域をHaskell側から使うための型や関数を用意してくれる道具がモジュールForeign.C.Struct
から公開されている。
まずは、C言語側に構造体HmHuman
をコピーする関数と、その領域を解放する関数、それに構造体HmHuman
を変化させる関数とを定義する。
HmHuman *
hm_human_copy(HmHuman *hm)
{
HmHuman *dst = (HmHuman *)malloc(sizeof(HmHuman));
dst->head_size = hm->head_size;
dst->left_arm = hm->left_arm;
dst->right_arm = hm->right_arm;
return dst;
}
void hm_human_destroy(HmHuman *hm) { free(hm); }
void
hm_human_flip_head(HmHuman *hm)
{
hm->head_size =
hm->head_size == HM_SMALL_HEAD ? HM_LARGE_HEAD : HM_SMALL_HEAD;
}
void
hm_human_flip_left_arm(HmHuman *hm)
{
hm->left_arm =
hm->left_arm = HM_DOWN_ARM ? HM_UP_ARM : HM_DOWN_ARM;
}
void
hm_human_flip_right_arm(HmHuman *hm)
{
hm->right_arm =
hm->right_arm = HM_DOWN_ARM ? HM_UP_ARM : HM_DOWN_ARM;
}
関数hm_human_copy
はメモリ領域を確保したうえで、引数の構造体のそれぞれの要素をコピーしている。関数hm_human_destroy
はメモリ領域を解放している。残りの3つの関数はそれぞれ頭の大きさ、左腕の上下、右腕の上下を切り換える関数だ。これらをHaskell側から使えるようにする。
foreign import ccall "hm_human_copy"
c_hm_human_copy :: Ptr Human -> IO (Ptr Human)
foreign import ccall "hm_human_destroy" c_hm_human_destroy :: Ptr Human -> IO ()
structPrim "Human" 'c_hm_human_copy 'c_hm_human_destroy [''Show]
関数structPrim
はつぎのような書式になる。
structPrim もとのデータ型の名前 構造体をコピーする関数 構造体の領域を解放する関数
diriveする型クラスのリスト
関数structPrim
によって生成されたコードが展開されることで、つぎのような関数が定義される。
humanFreeze :: PrimMonad m => HumanPrim (PrimState m) -> m Human
humanThaw :: PrimMonad m => Human -> m (HumanPrim (PrimState m))
humanCopy ::
PrimMonad m => HumanPrim (PrimState m) -> m (HumanPrim (PrimState m))
関数humanFreeze
はIOモナドまたはSTモナドの文脈下で「変化しうる状態」を「変化しない値」に固める。関数humanThaw
は「変化しない値」から「変化する状態」を生成する。関数humanCopy
は「変化する状態」から新しいコピーである「変化する状態」を作る。
状態HumanPrim
を変化させる関数humanFlipHead
, humanFlipLeftArm
, humanFlipRightArm
を定義する。
foreign import ccall "hm_human_flip_head"
c_hm_human_flip_head :: Ptr Human -> IO ()
foreign import ccall "hm_human_flip_left_arm"
c_hm_human_flip_left_arm :: Ptr Human -> IO ()
foreign import ccall "hm_human_flip_right_arm"
c_hm_human_flip_right_arm :: Ptr Human -> IO ()
humanFlipHead, humanFlipLeftArm, humanFlipRightArm ::
PrimMonad m => HumanPrim (PrimState m) -> m ()
humanFlipHead (HumanPrim fhm) =
unsafeIOToPrim $ withForeignPtr fhm c_hm_human_flip_head
humanFlipLeftArm (HumanPrim fhm) =
unsafeIOToPrim $ withForeignPtr fhm c_hm_human_flip_left_arm
humanFlipRightArm (HumanPrim fhm) =
unsafeIOToPrim $ withForeignPtr fhm c_hm_human_flip_right_arm
ビルドして試してみる。
% stack build
% stack ghci
> f <- fieldNew
> hm0 = Human SmallHead DownArm DownArm
> hmp <- humanThaw hm0
> humanFlipHead hmp
> hm1 <- humanFreeze hmp
> fieldPutVariousHuman f hm1 10 15
> humanFlipLeftArm hmp
> hm2 <- humanFreeze hmp
> fieldPutVariousHuman f hm2 20 15
> fieldDraw f
...(つぎのような2人の人間が表示される)...
.........................................
.........................................
.....O..........\O.......................
..../A\..........A\......................
..../.\........./.\......................
.........................................
ForeignPtr
によるGCを利用するやりかたが適切でない場合
自分が操作できる「人間」を「主人公」と呼び、「主人公」が踏んでいく「人間」を「敵」と呼ぶことにしよう。「主人公」がジャンプしたり「敵」が動いたりするためには、一定時間ごとに処理を行う必要がある。呼び出すと一定時間待ってから、Tick
イベントを返す関数を使う。また、「主人公」を操作するためにキーボードからの入力を受けとるためのイベントも必要だ。Tick
イベントを返す関数を定義していく。
自前でのメモリ割り当て
ライブラリによっては効率などの理由で一般的なmalloc()
とfree()
ではなく、自前でメモリの確保や解放を実装している場合がある。そのような場合に生じる問題を再現するために、自前でのメモリの確保と解放を行う関数を定義してみよう。
仕様
静的に確保された1024バイトの領域から引数で指定したサイズの未使用の領域のアドレスを返す関数allocate_memory
と、その領域を解放する関数free_memory
とを用意する。領域を確保できないときはNULL
を返す。
アルゴリズム
領域の確保は2の累乗のサイズで行う。また、領域の大きさの最小値は8バイトとする。つまり、確保される領域のサイズは8, 16, 32, 64, 128となる。
実際の領域とは別に「領域が使われているかどうか」という情報を格納する木構造の領域を用意する。木は2分木であり、木の末端がそれぞれの8バイトの領域を表す。木の階層をひとつ上がるごとに2倍の領域を表現する。それぞれのノードに、そのノード以下の使われている領域のバイト数の合計と、そのノードに対応する領域が使われているかどうかの情報が格納される。
たとえば、つぎのような場合を考える。
24, f
|
+--------------------------------------+
| |
8, f 16, f
| |
+------------------+ +----------------+
| | | |
0, f 8, f 16, t 0, f
| | | |
+---------+ +---------+ +--------+ +-------+
| | | | | | | |
0, f 0, f 8, t 0, f 0, f 0, f 0, f 0, f
t: true
f: false
24, f
のノードを階層0とする。また、先頭のアドレスを0とする。上の図では、階層3の左から2番目のノードがtrue
なので、2x8でアドレス16の領域が8バイトぶん確保済みとなる。階層2の左から2番目のノードがtrue
なので2x16でアドレス32の領域が16バイトぶん確保されていることになる。true
となっているノード以下のノードは参照する必要がないので、デフォルトの値のままになっている。
メモリの確保は木の上から確保されたメモリのすくないほうの枝をたどっていき、確保したいサイズと合った階層に達したところのノードをtrue
とする。メモリの解放は一番下の階層からはじめて、true
のノードがあるまで階層を上にたどり、そのtrue
をfalse
にする。メモリの確保、解放のどちらの際にも、「確保されたメモリのサイズの合計」もその都度修正していく。
メモリの使用状態を表現する木構造
配列で2分木を表現するために、インデックスについて「左の子」「右の子」「親」にそれぞれ移動するマクロを定義する。また、それぞれの情報を格納する構造体を定義する。
#ifndef _MEM_ALLOC_LOCAL_H
#define _MEM_ALLOC_LOCAL_H
#include <stdbool.h>
#define TREE_SIZE(h) ((1 << h) - 1)
#define LEFT_CHILD(i) ((i) * 2 + 1)
#define RIGHT_CHILD(i) ((i) * 2 + 2)
#define PARENT(i) (((i) + 1) / 2 - 1)
typedef struct { bool allocated; int used; } AllocInfo;
extern AllocInfo alloc_info[];
#endif
TREE_SIZE
は「木の高さ」から必要なメモリの量を計算するマクロだ。LEFT_CHILD
とRIGHT_CHILD
あるノードを表すインデックスから、それぞれ左の子と右の子とを表すインデックスを返すマクロであり、PARENT
は同様に親のインデックスを返す。型AllocInfo
は、それぞれの「メモリの使用状況」を表すノードに格納される値の型だ。extern
で変数alloc_info
を宣言することで、コード内で定義されるメモリの使用状況を表現する情報にアクセスできるようにしている。
メモリの確保と解放
メモリの確保
確保する領域のサイズと現在の木構造内での深さと現在の木構造を表す配列内でのインデックスなどの情報を引数にして、領域の確保の状況を更新して、確保できたメモリ内でのインデックスを返す関数を定義する。
まずは、package.yaml
に新しいC言語のソースファイルcsrc/mem_alloc.c
とcsrc/mem_alloc_draw.c
とを追加する。
...
library:
source-dirs: src
include-dirs: include
c-sources:
- csrc/human.c
- csrc/mem_alloc.c
- csrc/mem_alloc_draw.c
...
関数get_index
を定義する。
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <mem_alloc_local.h>
AllocInfo alloc_info[TREE_SIZE(8)];
uint64_t memory[1 << 7];
int
get_index(int sz, int dp, int ii, int mi)
{
AllocInfo *inf = &(alloc_info[ii]);
AllocInfo *l = &(alloc_info[LEFT_CHILD(ii)]);
AllocInfo *r = &(alloc_info[RIGHT_CHILD(ii)]);
if (inf->used + sz > (1024 >> dp)) {
while (ii) { ii = PARENT(ii); alloc_info[ii].used -= sz; }
return -1; }
inf->used += sz;
if (sz << dp == 1024) { inf->allocated = true; return mi; }
else if (l->used <= r->used) get_index(sz, dp + 1, LEFT_CHILD(ii), mi);
else get_index(sz, dp + 1, RIGHT_CHILD(ii), mi + (64 >> dp));
}
引数sz
は確保したい領域のサイズだ。単位はバイトで最低8バイトで2の累乗であることが前提となっている。引数dp
は木構造内での深さであり、引数ii
は木構造内でのインデックス。引数mi
は返り値である「メモリ領域内」でのインデックスである。関数のはじめの3行では、それぞれ「このノードがもつ情報」「左の子ノードがもつ情報」「右の子ノードがもつ情報」へのポインタを変数inf
, l
, r
として定義している。
つぎのif
文では、領域が確保できなかった場合の処理だ。inf->used + sz
はそのノードに対応する領域について「使われているバイト数」と「確保したいバイト数」の和であり、1024 >> dp
はそのノードに対応する領域のサイズを示す。while
文では現在のノードの祖先の「使用済みのバイト数」について、これまで「確保済み」にしてきたバイト数ぶんだけ減算することで、もとにもどしている。領域確保の失敗は-1
を返り値とすることにより示す。
そのノードに対応する領域に確保したいバイト数以上の残りがある場合に、残りの処理に進む。使用済みのバイト数に確保したいバイト数を加算する。そのつぎに、確保したいバイト数とそのノードに対応する領域のバイト数とを比較して、等しければそのノードに使用済みのフラグを立てて、メモリのインデックスを返す。そうでなかったときは、左右のノードの「使用済み」のバイト数を比較して、すくないほうについて再帰的に関数get_index
を適用する。
関数get_index
を使えばメモリの確保関数は簡単に定義できる。
int normalize(int sz)
{
if (sz <= 8) return 8;
int rslt = 1, tmp = sz - 1;
while (tmp) { tmp >>= 1; rslt <<= 1; }
return rslt;
}
void *
allocate_memory(int sz)
{
int i = get_index(normalize(sz), 0, 0, 0);
if (i < 0) return NULL; else return memory + i;
}
関数allocate_memory
は、関数normalize
で確保するサイズを8以上の2の累乗の値に変換した値を関数get_index
にわたす。返り値が負(-1
)ならばNULL
を返し、そうでないなら、返されたインデックスをuint64_t
の配列memory
内のアドレスに変換して返す。ここまでの関数を試すためにメモリの確保の状態を可視化する関数を定義して試してみよう。
#include <stdio.h>
#include <mem_alloc_local.h>
void
put_used(int i, int sz, char *buf)
{
if (alloc_info[i].allocated) for (int j; j < sz; j++) buf[j] = '*';
else if (alloc_info[i].used == 0) /* DO NOTHING */;
else { int hf = sz >> 1;
put_used(LEFT_CHILD(i), hf, buf);
put_used(RIGHT_CHILD(i), hf, buf + hf); }
}
void
draw_memory(void)
{
char buf[(1 << 7) + 1];
int i;
for (i = 0; i < (1 << 7); i++) buf[i] = '.';
buf[i] = '\0';
put_used(0, 1 << 7, buf);
printf("%.64s\n", buf);
printf("%s\n", buf + 64);
}
関数draw_memory
は、いろいろと準備したうえで関数put_used
を呼ぶ。関数put_used
はノードを上からたどっていき、ノードに対応する領域が確保済であるかどうかを調べ、確保済みであればそれに対応する表示領域に'*'
を書き込む。
Haskell側で使えるようにして試してみる。
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
module TryMemory where
import Foreign.Ptr
import Foreign.C.Types
foreign import ccall "allocate_memory" allocateMemory :: CInt -> IO (Ptr a)
foreign import ccall "draw_memory" drawMemory :: IO ()
ビルドして試してみる。
% stack build
% stack ghci
> drawMemory
................................................................
................................................................
> allocateMemory 15
0x00007f1ec9ddcba0
> drawMemory
**..............................................................
................................................................
> allocateMemory 100
0x00007f1ec9ddcda0
> drawMemory
**..............................................................
****************................................................
> allocateMemory 150
0x00007f1ec9ddcca0
> drawMemory
**..............................********************************
****************................................................
> allocateMemory 180
0x00007f1ec9ddcea0
> drawMemory
**..............................********************************
****************................********************************
> allocateMemory 180
0x0000000000000000
> drawMemory
**..............................********************************
****************................********************************
> allocateMemory 30
0x00007f1ec9ddcc20
> drawMemory
**..............****............********************************
****************................********************************
メモリの解放
メモリを解放する関数を定義する。
int
free_memory(void *addr)
{
int mi = (uint64_t *)addr - memory;
if (mi < 0 || 127 < mi) return -1;
int i, sz, size; bool flag;
for (i = mi + 127, sz = 8, flag = false; i; i = PARENT(i), sz <<= 1) {
if (alloc_info[i].allocated) {
alloc_info[i].allocated = false;
flag = true; size = sz; }
if (flag )alloc_info[i].used -= size; }
if (flag) alloc_info[i].used -= size;
return 0;
}
変数mi
は確保されたメモリ領域内でのインデックスだ。それに127を足すことで、メモリのアロケーションの状態を示す木alloc_info
の末端での対応する位置へのインデックスになる。対応する位置について親ノードへとたどっていき、allocated
がtrue
であるノードをみつけたら、それをfalse
にしたうえで変数flag
をtrue
にする。そのあとは、親ノードへとたどりながら、解放されたメモリ領域の大きさぶんused
の値を減算していく。
Haskell側でこれにアクセスするために関数freeMemory
を定義する。
...
foreign import ccall "allocate_memory" allocateMemory :: CInt -> IO (Ptr a)
foreign import ccall "free_memory" freeMemory :: Ptr a -> IO CInt
foreign import ccall "draw_memory" drawMemory :: IO ()
ビルドして試してみる。
% stack build
% stack ghci
> drawMemory
................................................................
................................................................
> allocateMemory 15
0x00007f1ce046ec40
> drawMemory
**..............................................................
................................................................
> allocateMemory 15
0x00007f1ce046ee40
> m1 <- allocateMemory 15
> drawMemory
**..............................**..............................
**..............................................................
> allocateMemory 100
0x00007f1ce046ef40
> drawMemory
**..............................**..............................
**..............................****************................
> m2 <- allocateMemory 100
> drawMemory
**..............******************..............................
**..............................****************................
> allocateMemory 100
0x00007f1ce046eec0
> drawMemory
**..............******************..............................
**..............********************************................
> freeMemory m2
0
> drawMemory
**..............................**..............................
**..............********************************................
> freeMemory m1
0
> drawMemory
**..............................................................
**..............********************************................
関数allocate_memory
とfree_memory
とをcsrc/human.c
で使うので、ヘッダファイルを作成しておく。
#ifndef _MEM_ALLOC_H
#define _MEM_ALLOC_H
void *allocate_memory(int sz);
int free_memory(void *addr);
#endif
イベントの定義
Tick
イベントを返す関数を定義していく。まずはイベントを表すデータ構造の定義だ。
typedef enum { HM_EVENT_TYPE_TICK, HM_EVENT_TYPE_CHAR } HmEventType;
typedef struct { HmEventType event_type; } HmEventAny;
typedef struct { HmEventType event_type; int times; } HmEventTick;
typedef struct { HmEventType event_type; char character; } HmEventChar;
typdef union {
HmEventAny event_any; HmEventTick event_tick; HmEventChar event_char; }
HmEvent;
共用体HmEvent
は構造体HmEventAny
またはHmEventTick
またはHmEventChar
のどれかである。構造体HmEventTick
はTick
イベントであり、領域times
はそのTick
イベントが何番目であるかを示す値が格納される。また、構造体HmEventAny
は、すべてのイベントに定義される「イベントの種類」を取り出すのに使われる。構造体HmEventChar
は、あとでキーボードの入力を受けとるのに使う。
Tick
イベントの生成
まずはmem_alloc.h
をインクルードする。
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <sys/select.h>
#include <human.h>
#include <mem_alloc.h>
...
Tick
イベントを生成する。現在何回目のTick
かを記録する関数hm_tick
を定義し、領域を確保しTick
イベントの中身をうめる関数hm_make_event_tick
を定義する。それらを利用して、0.01秒スリープしたあとにTick
イベントを返す関数hm_get_event_only_tick
を定義する。
int
hm_tick(void)
{
static int tms = 0;
return tms++;
}
HmEvent *
hm_make_event_tick(void)
{
int t = hm_tick();
HmEvent *ev = allocate_memory(sizeof(HmEvent));
if (ev != NULL) {
ev->event_tick.event_type = HM_EVENT_TYPE_TICK;
ev->event_tick.times = t; }
return ev;
}
HmEvent *
hm_get_event_only_tick(void)
{
struct timeval tv;
tv.tv_sec = 0; tv.tv_usec = 10000;
select(0, NULL, NULL, NULL, &tv);
return hm_make_event_tick();
}
イベント用の領域を解放する関数を定義する。
void hm_event_destroy(HmEvent *ev) { free_memory(ev); }
ForeignPtrによりHaskellのGCを利用してみる
src/Human/EventGc.hsc
を用意する。
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE BlockArguments, TupleSections #-}
{-# LANGUAGE PatternSynonyms, ViewPatterns #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
module Human.EventGc where
import Foreign.Ptr
import Foreign.ForeignPtr hiding (newForeignPtr)
import Foreign.Concurrent
import Foreign.Storable
import Foreign.C.Types
import Foreign.C.Enum
import Foreign.C.Struct
import Data.Word
import System.IO.Unsafe
#include <human.h>
Haskell側でイベントを表す型Event
と関数getEvent
を定義する。
data Event = Event (ForeignPtr Event) deriving Show
foreign import ccall "hm_get_event_only_tick" c_hm_get_event :: IO (Ptr Event)
foreign import ccall "hm_event_destroy" c_hm_event_destroy :: Ptr Event -> IO ()
getEventGc :: IO Event
getEventGc = Event <$> do
pe <- c_hm_get_event
newForeignPtr pe $ c_hm_event_destroy pe
Tick
イベントを表現するテータ型EventTick
を用意する。またEvent
型の値からEventTick
型の値を取り出すパターンEventEventTickGc
を定義する。
enum "EventType" ''#{type HmEventType} [''Show, ''Storable] [
("EventTypeTick", #{const HM_EVENT_TYPE_TICK}) ]
struct "EventTick" #{size HmEventTick}
[ ("eventType", ''(), [| const $ pure () |],
[| \p _ -> #{poke HmEventTick, event_type}
p EventTypeTick |]),
("times", ''CInt, [| #{peek HmEventTick, times} |],
[| #{poke HmEventTick, times} |]) ]
[''Show]
getEventType :: Event -> EventType
getEventType (Event fev) =
unsafePerformIO $ withForeignPtr fev #{peek HmEventAny, event_type}
getEventTick :: Event -> (Eventtype, EventTick)
getEventTick ev@(Event fev) = (getEventType ev, EventTick_ $ castForeignPtr fev)
pattern EventEventTickGc :: EventTick -> Event
pattern EventEventTickGc evt <- (getEventTick -> (EventTypeTick, evt)) where
EventEventTickGc (EventTick_ fev) = Event $ castForeignPtr fev
ビルドして試してみる。
% stack build
% stack ghci
> e <- getEventGc
> EventEventTickGc et = e
> et
EventTick {eventTickEventType = (), eventTickEventTimes = 0}
> :module + Control.Monad
> replicateM_ 10 getEventGc
> e <- getEventGc
> EventEventTickGc et = e
EventTick {eventTickEventType = (), eventTickEventTimes = 11}
連続してイベントを取得してみる。app/try-event-gc.hs
を作成する。package.yaml
のexecutables
のMain.hs
のところをコピーして、zenn-use-c-lib-test
とMain.hs
を書き換える。
...
executables:
zenn-use-c-lib-exe:
main: Main.hs
source-dirs: app
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- zenn-use-c-lib
try-event-gc:
main: try-event-gc.hs
source-dirs: app
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- zenn-use-c-lib
...
10回連続してイベントを取得して表示する動作を20回くりかえしてみる。
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
module Main where
import Control.Monad
import Human.EventGc
main :: IO ()
main = replicateM_ 20 $ print =<< replicateM 10 getEventGc
ビルドして試してみる。
% stack build
% stack exec try-event-gc
[Event 0x000000000002e440,Event 0x00000000002e4640,...]
...
[Event 0x0000000000000000,Event 0x0000000000000000,...]
最後のほうは0x0000000000000000
つまりNULL
ポインタになっている。これはイベント用の領域が独自の領域確保の仕組みを使っているからだ。独自の領域に空きがなくなってもHaskellはGCを走らせないので、領域が解放されることなく、領域の確保だけ進み空き領域が不足したということだ。
イベントが発生したら、その都度領域を解放するやりかた
解放済みの領域にアクセスさせない仕組み
つまり、一般的なmalloc
やfree
ではないやりかたで領域の確保や解放を実装している関数についてはForeignPtr
でHaskellのGCを利用するやりかたは、うまく動かないということだ。こういう場合、つぎのようなやりかたができる。
withFoo :: (Foo -> IO a) -> IO a
withFoo fun = do
foo <- fooNew
fun foo
fooDestroy foo
ただし、これだと問題が残る。たとえば、つぎのようなことができてしまう。
runBad :: IO ()
runBad = do
q <- withFoo \p -> do
doSomething1 p
pure p
doSomething2 q
関数withFoo
のIO a
の型変数a
を型Foo
にすることができるので、すでに解放された領域へのアクセスを許してまうということだ。そうしないために、型変数のスコープを利用する。
withFoo2 :: (forall s . Foo s -> IO a) -> IO a
withFoo2 fun = do
...
このように型を定義すると型変数s
が丸括弧内のスコープから外に出られないため、型変数a
を型Foo s
にすることができないので、解放済みの領域へのアクセスを防ぐことができる。この考えかたで、スコープを外れるときに領域が解放されるような形でイベントを取得する関数を定義していく。
イベントの取得
package.yaml
の依存するパッケージにstm
を追加する。
...
dependencies:
- base >= 4.7 && < 5
- stm
- primitive
- exception-hierarchy
- c-enum
- c-struct
...
ソースファイルsrc/Human/Event.hsc
を用意して、言語拡張や導入するモジュールを記述する。
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE BlockArguments, TupleSections #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE PatternSynonyms, ViewPatterns #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
module Human.Event where
import Foreign.Ptr
import Foreign.ForeignPtr hiding (newForeignPtr)
import Foreign.Concurrent
import Foreign.Marshal
import Foreign.Storable
import Foreign.C.Types
import Foreign.C.Enum
import Foreign.C.Struct
import Control.Monad
import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception
import Data.Bool
import Data.Word
import System.IO
import System.IO.Unsafe
#include <human.h>
新しくイベントを表す型Event s
とそれを取得する関数withEventOnlyTick
を定義する。
data Event s = Event (Ptr (Event s)) deriving Show
foreign import ccall "hm_get_event_only_tick"
c_hm_get_event_only_tick :: IO (Ptr (Event s))
foreign import ccall "hm_event_destroy"
c_hm_event_destroy :: Ptr (Event s) -> IO ()
withEventOnlyTick :: (forall s . Event s -> IO a) -> IO a
withEventOnlyTick f =
bracket c_hm_get_event_only_tick c_hm_event_destroy (f . Event)
関数bracket
はたとえ例外が起きたとしても、リソースの解放をするために使われる。書式はつぎのようになる。
bracket リソースを生成する リソースを解放する リソースを使う
ビルドして試してみる。
% stack build
% stack ghci
> withEventOnlyTick print
Event 0x00007f8a95075c40
Tick
イベント
イベントの種類と、それを取得する関数を定義する。
enum "EventType" ''#{type HmEventType} [''Show, ''Storable] [
("EventTypeTick", #{const HM_EVENT_TYPE_TICK}),
("EventTypeChar", #{const HM_EVENT_TYPE_CHAR}) ]
eventType :: Event s -> EventType
eventType (Event pev) = unsafePerformIO $ #{peek HmEventAny, event_type} pev
ビルドして試してみる。
% stack build
% stack ghci
> withEventOnlyTick $ print . eventType
EventTypeTick
型EventTick
を定義する。
struct "EventTick" #{size HmEventTick}
[ ("eventType", ''(), [| const $ pure () |],
[| \p _ -> #{poke HmEventTick, event_type}
p EventTypeTick |]),
("times", ''CInt, [| #{peek HmEventTick, times} |],
[| #{poke HmEventTick, times} |]) ]
[''Show]
このように定義した型EventTick
だと型引数s
を持たないため、領域の解放後もアクセスできてしまうという問題がある。そのため、値をスコープ内にとじこめるために使う型Sealed s
を定義する。
newtype Sealed s a = Sealed a deriving Show
Event s
型の値からSealed s EventTick
型の値を取り出すパターンEventEventTick
を定義する。
getEventTick :: Event s -> (EventType, Sealed s EventTick)
getEventTick ev@(Event pev) =
(eventType ev, Sealed . EventTick_ . noFinalizer $ castPtr pev)
noFinalizer :: Ptr a -> ForeignPtr a
noFinalizer = unsafePerformIO . (`newForeignPtr` pure ())
pattern EventEventTick :: Sealed s EventTick -> Event s
pattern EventEventTick evt <- (getEventTick -> (EventTypeTick, evt))
イベントの領域はwithEventOnlyTick
内で解放されるので、finalizerは必要ない。なので、何もしないfinalizerをつけてForeignPtr
型の値を作る。
Tick
イベントから「回数」を取り出す関数を定義する。この値は整数値であり、領域の解放後にアクセスしても安全だ。
eventTickToTimes :: Sealed s EventTick -> CInt
eventTickToTimes (Sealed evt) = eventTickTimes evt
ビルドして試してみる。
% stack build
% stack ghci
> :set -XBlockArguments
> :{
| withEventOnlyTick \ev -> do
| let EventEventTick evt = ev
| print evt
| return $ eventTickToTimes evt
:}
Sealed EventTick {eventTickEventType = (), eventTickTimes = 0}
0
Haskellの関数をC言語側にわたす
withEvent
関数「ゲーム」をするには、時間の経過だけではなく何らかのインプットが必要だ。文字入力を一文字ずつイベントにして返すようにしよう。C言語のライブラリとしては、一文字入力を取得する関数の関数ポインタを引数として取って、イベントを返すような形にする。
HmEvent *
hm_make_event_char(char c)
{
HmEvent *ev = allocate_memory(sizeof(HmEvent));
if (ev != NULL) {
ev->event_char.event_type = HM_EVENT_TYPE_CHAR;
ev->event_char.character = c; }
return ev;
}
HmEvent *
hm_get_event(char (*get_char)())
{
char c = get_char();
if (c != '\0') return hm_make_event_char(c);
struct timeval tv;
tv.tv_sec = 0; tv.tv_usec = 10000;
select(0, NULL, NULL, NULL, &tv);
return hm_make_event_tick();
}
これをHaskell側で呼び出す関数はつぎのようになる。
foreign import ccall "hm_get_event"
c_hm_get_event :: FunPtr (IO CChar) -> IO (Ptr (Event s))
C言語の関数ポインタはHaskellではFunPtr a
型の値として表現される。このままではHaskellの動作や関数を、引数としてC言語の関数にわたすことはできない。そこでwrapper
という「C言語側の関数」が用意されている。
foreign import ccall "wrapper"
wrap_getCChar :: IO CChar -> IO (FunPtr (IO CChar))
このようにすることで、CChar
型の値を返すHaskellの動作から、C言語の関数ポインタを生成することができる。ここで、関数ポインタとしてc_hm_get_event
の引数とするHaskell側の動作については、「入力がない場合に'\0'を返す」という処理のために「チャンネル」を使うことにする。関数hGetBufNonBlocking
につぎのような記述があったので。
NOTE: on Windows, this function does not work correctly; it behaves identically to hGetBuf
関数Control.Concurrent.STM.TChan.isEmptyTChan
を使えば、ブロックすることなく「入力があったかどうか」を判定できる。
(「入力があったかどうか」をブロックなしに判定するのって実はけっこう難しいようで、MVar
を利用したControl.Concurrent.Chan.isEmptyChan
はブロックしてしまう動作が修正できずdeprecated
になったうえで、現在は削除されている)
withEvent :: TChan CChar -> (forall s . Event s -> IO a) -> IO a
withEvent ch f = bracket (c_hm_get_event =<< wrap_getCChar getc)
c_hm_event_destroy (f . Event)
where
getc = atomically $ bool (readTChan ch) (pure 0) =<< isEmptyTChan ch
TChan CChar
型のチャンネルを生成して、入力ハンドルからの値を送り続ける関数hGetAndPushCChar
を定義する。
hGetAndPushCChar :: Handle -> IO (TChan CChar)
hGetAndPushCChar h = atomically newTChan >>= \ch ->
ch <$ forkIO (forever $ getcs >>= mapM_ (atomically . writeTChan ch))
where getcs = allocaBytes 64 \cstr -> do
cnt <- hGetBufSome h cstr 64
when (cnt == 0) $ error "EOF"
peekArray cnt cstr
64バイトの領域を確保して、そこに関数hGetBufSome
でバイト列を書き込む。その領域を関数peekArray
でC言語のchar
のリストにして返す。forkIO
で別スレッドにしたうえで、永遠にそのchar
をチャンネルに一文字ずつ送り込んでいる。試してみよう。実際にゲームをするときにはハンドルstdin
を使うが、ここではテキストファイルを用意して、そこから読み込んでみる。
% stack build
% echo 'abc' > dummy.txt
% stack ghci
> :module + System.IO
> h <- openFile "dummy.txt" ReadMode
> ch <- hGetAndPushCChar h
<interactive>: EOF
CallStack (from HasCallStack):
error, called at src/Human/Event.hsc:88:35 in main:Human.Event
> withEvent ch $ pure . eventType
EventTypeChar
> withEvent ch $ pure . eventType
EventTypeChar
> withEvent ch $ pure . eventType
EventTypeChar
> withEvent ch $ pure . eventType
EventTypeChar
> withEvent ch $ pure . eventType
EventTypeTick
> withEvent ch $ pure . eventType
EventTypeTick
関数hGetAndPushCChar
を呼び出したときにエラーが生じる。これは、この関数がハンドルstdin
などの「終わりのないハンドル」を想定しているからだ。エラーが生じても'a', 'b', 'c', '\n'はちゃんとチャンネルに送られている。
文字入力イベント
文字入力イベントを表す型を定義してEvent s
型の値からそれを取り出せるようにする。
struct "EventChar" #{size HmEventChar}
[ ("eventType", ''(), [| const $ pure () |],
[| \p _ -> #{poke HmEventChar, event_type}
p EventTypeChar |]),
("character", ''CChar, [| #{peek HmEventChar, character} |],
[| #{poke HmEventChar, character} |]) ]
[''Show]
getEventChar :: Event s -> (EventType, Sealed s EventChar)
getEventChar ev@(Event pev) =
(eventType ev, Sealed . EventChar_ . noFinalizer $ castPtr pev)
pattern EventEventChar :: Sealed s EventChar -> Event s
pattern EventEventChar evc <- (getEventChar -> (EventTypeChar, evc))
eventCharToCharacter :: Sealed s EventChar -> CChar
eventCharToCharacter (Sealed evc) = eventCharCharacter evc
Tick
イベントの定義と同様に定義した。
% stack build
% stack ghci
> :set -XLambdaCase
> :module + System.IO
> h <- openFile "dummy.txt" ReadMode
> ch <- hGetAndPUshChar h
<interactive>: EOF
CallStack (from HasCallStack):
error, called at src/Human/Event.hsc:88:35 in main:Human.Event
> :{
| f = \case
EventEventTick evt -> Left $ eventTickToTimes evt
EventEventChar evc -> Right $ eventCharToCharacter evc
> :}
> withEvent ch $ pure . f
Right 97
> withEvent ch $ pure . f
Right 98
> withEvent ch $ pure . f
Right 99
> withEvent ch $ pure . f
Right 10
> withEvent ch $ pure . f
Left 0
> withEvent ch $ pure . f
Left 1
C言語の関数ポインタからHaskellの関数を作る
% stack ghci
> f <- fieldNew
> fieldPUtHuman f 30 15
> fieldDraw f
.............................................
...............(「人間」が表示される)........
.............................................
ここまで作ってきた関数だと「人間」の背景は'.'でうめられる。これを'.'ではなく' 'にしたいとする。
void
hm_field_clear_bg_space(HmField f)
{
for (int i = 0; i < FIELD_HEIGHT; i++) {
int j;
for (j = 0; j < FIELD_WIDTH; j++) f[i][j] = ' ';
f[i][j] = '\0'; }
}
HmField
hm_field_new_bg_space(void)
{
HmField f = (HmField)malloc(sizeof(HmFieldArray));
hm_field_clear_bg_space(f);
return f;
}
これらの背景が' 'である関数と'.'である関数とをブール値で切り換えられるようにする。
void
(*hm_field_clear_background(bool b)) (HmField)
{
return b ? hm_field_clear : hm_field_clear_bg_space;
}
HmField
(*hm_field_new_background(bool b)) (void)
{
return b ? hm_field_new : hm_field_new_bg_space;
}
これらは引数の真偽値によって、返す関数ポインタを選ぶ関数だ。これをHaskellから呼び出す。
foreign import ccall "hm_field_new_background"
c_hm_field_new_background :: #{type bool} -> FunPtr (IO (Ptr (Field s)))
foreign import ccall "hm_field_clear_background"
c_hm_field_clear_background :: #{type bool} -> FunPtr (Ptr (Field s) -> IO ())
bool
型の値をとって関数ポインタを返す関数だ。しかし、関数ポインタのままではHaskell側からは使えない。Haskellの関数を関数ポインタにするにはwrapper
が用意されていた。おなじように関数ポインタからHaskellの関数や動作を取得するのに、「C言語の関数」であるdynamic
が用意されている。
foreign import ccall "dynamic"
mkFieldNewBg :: FunPtr (IO (Ptr (Field s))) -> IO (Ptr (Field s))
foreign import ccall "dynamic" mkFieldClearBg ::
FunPtr (Ptr (Field s) -> IO ()) -> Ptr (Field s) -> IO ()
これで関数ポインタが指す関数をHaskell側から使えるようになる。
fieldNewBackgroundRaw :: Bool -> IO (Field s)
fieldNewBackgroundRaw (boolToCBool -> b) = Field <$> do
pf <- mkFieldNewBg $ c_hm_field_new_background b
newForeignPtr pf $ c_hm_field_destroy pf
fieldClearBackgroundRaw :: Bool -> Field s -> IO ()
fieldClearBackgroundRaw (boolToCBool -> b) (Field ff) =
withForeignPtr ff $ mkFieldClearBg (c_hm_field_clear_background b)
boolToCBool :: Bool -> #{type bool}
boolToCBool = \case False -> #{const false}; True -> #{const true}
C言語の型bool
を使うのでヘッダstdbool.h
を導入する。
...
import Human.Exception
#include <human.h>
#include <stdbool.h>
...
前にやったようにIO a
ではなくPrimMonad m => m a
を使うようにする。
fieldNewBackground :: PrimMonad m => Bool -> m (Field (PrimState m))
fieldNewBackground = unsafeIOToPrim . fieldNewBackgroundRaw
fieldClearBackground :: PrimMonad m => Bool -> Field (PrimState m) -> m ()
fieldClearBackground b = unsafeIOToPrim . fieldClearBackgroundRaw b
ビルドして試してみる。
% stack build
% stack ghci
> f <- fieldNewBackground False
> fieldPutHuman f 30 15
> fieldDraw f
(スペースの背景に「人間」が表示される)
> g <- fieldNewBackground True
> fieldPutHuman g 30 15
> fieldDraw g
.....('.'の背景に「人間」が表示される)...
> fieldClearBackground False f
> fieldDraw f
(スペースの背景が表示される)
> fieldClearBackground True g
> fieldDraw g
('.'の背景が表示される)
ポインタを含む構造体
ゲームには主人公と敵キャラだけでなく文字列も表示したくなる。現在のポインタを見たいだろうし、失敗したら「GAME OVER」とか表示したい。位置と文字列とをもつ構造体を引数にとり、メッセージを表示する関数を定義する。まずは、構造体を定義しよう。
typedef struct { int x; int y; } HmPosition;
typedef struct { HmPosition *position; char *message; } HmMessage;
関数hm_field_put_message
を定義する。
void
hm_field_put_message(HmField f, HmMessage *msg)
{
int x = msg->position->x;
int y = msg->position->y;
char *m = msg->message;
for (; *m; x++, m++) hm_field_put_char(f, x, y, *m);
}
X座標の値を増やしつつ文字列のなかの文字を指すポインタm
の値を増やしながら、関数hm_field_put_char
を呼び出している。これをHaskell側から使ってみる。
型Foreign.C.String.CString
を使うのでモジュールを導入する。
...
import Foreign.Storable
import Foreign.C.Types
import Foreign.C.String
import Foreign.C.Enum
...
C言語の構造体HmPosition
とHmMessage
に対応する型を定義する。
struct "Position" #{size HmPosition}
[ ("x", ''CInt, [| #{peek HmPosition, x} |],
[| #{poke HmPosition, x} |]),
("y", ''CInt, [| #{peek HmPosition, y} |],
[| #{poke HmPosition, y} |]) ]
[''Show]
type PtrPosition = Ptr Position
struct "CMessage" #{size HmMessage}
[ ("position", ''PtrPosition, [| #{peek HmMessage, position} |],
[| #{poke HmMessage, position} |]),
("message", ''CString, [| #{peek HmMessage, message} |],
[| #{poke HmMessage, message} |]) ]
[''Show]
構造体HmMessage
はほかの構造体や文字列へのポインタを含む構造体だ。この構造体をそのまま使うとなると、いつGCするべきかについて問題が出てくる。関数addForeignPtrFinalizer
とtouchForeignPtr
を使って、そのあたりを制御することもできるが、Haskellネイティブなデータ型を作って、上で定義したCMessage
型の値をC言語の関数を使うときだけ使う一時的なものとしたほうが、話は単純になる。
data Message = Message { messagePosition :: Position, messageMessage :: String }
deriving Show
foreign import ccall "hm_field_put_message"
c_hm_field_put_message :: Ptr (Field s) -> Ptr CMessage -> IO ()
fieldPutMessageRaw :: Field s -> Message -> IO ()
fieldPutMessageRaw (Field ff)
Message { messagePosition = Position_ fp, messageMessage = msg } =
withForeignPtr fp \pp -> withCString msg \cmsg -> do
let CMessage_ fmsg = CMessage {
cMessagePosition = pp,
cMessageMessage = cmsg }
withForeignPtr ff $ withForeignPtr fmsg . c_hm_field_put_message
値構築子CMessage
で生成したCMessage
型の値を、値構築子CMessage_
でマッチすることによって、ForeignPtr
型の値を取得し、そこから取り出したポインタを関数`c_hm_field_put_messageにわたしている。
fieldPutMessage :: PrimMonad m => Field (PrimState m) -> Message -> m ()
fieldPutMessage f = unsafeIOToPrim . fieldPutMessageRaw f
IO a
ではなくPrimMonad m => m a
を使うfieldPutMessage
を定義した。試してみる。
% stack build
% stack ghci
> f <- fieldNew
> fieldPutMessage f $ Message (Position 10 15) "Hello, world!"
> fieldDraw f
....(Hello, world!が表示される)...
これでC言語のライブラリと、それにアクセスするHaskell側の機構は完成した。これらを使ってゲームを作っていく。
mainLoop
関数関数withEvent
をくりかえし実行するよりも関数mainLoop
を定義しておいたほうが便利だ。
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE RankNTypes #-}
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
module Human.MainLoop where
import Control.Monad.Fix
import Control.Exception
import Data.Bool
import Ssytem.IO
import Human.Event
mainLoop :: (forall s . Event s -> IO Bool) -> IO ()
mainLoop f = hGetBuffering stdin >>= \bm -> finally (hSetBuffering stdin bm) do
hSetBuffering stdin NoBuffering
go . (`withEvent` f) =<< hGetAndPushCChar stdin
where go = fix \l act -> bool (pure ()) (l act) =<< act
いろいろと難しいことをやっていそうに見えるが、関数f
の動作が返す値がTrue
のあいだ関数withEvent
を呼び出し続けているだけだ。関数hSetBuffering
で標準入力のバッファリングを無効にしているのと、finally
によって最後にバッファリングをもとにもどしている。
試してみよう。まずはpackage.yaml
にtry-main-loop
を追加する。
...
try-main-loop:
main: try-main-loop.hs
source-dirs: app
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
dependencies:
- zenn-use-c-lib
...
app/try-main-loop.hs
を作成する。
{-# LANGUAGE BlockArguments, LambdaCase #-}
{-# LANGUAGE RankNTypes #-}
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
module Main where
import Human.MainLoop
import Human.Event
main :: IO ()
main = mainLoop \case
EventEventTick evt -> True <$ print evt
EventEventChar evc -> (eventCharToCharacter evc /= 113) <$ print evc
ev -> False <$ print ev
ビルドして試してみる。
% stack build
% stack exec try-main-loop
(いろいろな文字を入力してみる)
('q'を入力すると終わる)
ゲームを作る
ここの部分は主題ではなく、さらにすでに分量も多く、時間もないので「GitHubを見て」とだけ...。
-
src/Game.hs
を作成 -
package.yaml
にtry-game
を追加 -
app/try-game.hs
を作成
% stack build
% stack exec try-game
- 'h': 左に歩き出す
- 2回目の'h': 左に走り出す
- 'l': 右に歩き出す
- 2回目の'l': 右に走り出す
- 'j': 立ち止まる
- 'k': ジャンプsrc
まとめ
なんか長くなってしまったし疲れたし、ぎりぎりでなんとかまにあったという感じ。
Discussion