C言語とLuaの連携 -配列のやりとり-

やりたいこと

C言語Lua間で配列のやり取りをしたい。 具体的にはCからLuaに配列を渡し、Luaで配列を加工してC言語に返したい。

C言語のコード

array.c

#include <stdio.h>
#include <stdlib.h>
#include "include/lua.h"
#include "include/lualib.h"
#include "include/lauxlib.h"

#include "luadebug.h"

int main () {
    const char *fn = "double.lua";
    lua_State *lua = luaL_newstate();
    luaL_openlibs(lua);
    if ( luaL_dofile(lua, fn) != 0 ) {
        printf("failed execute %s\n", fn);
        exit(1);
    }

    lua_getglobal(lua, "double");
    lua_newtable(lua);

    printStack(lua);

    int arr[] = { 3, 6, 9, 12, 15 };
    int i;
    for ( i = 0; i < 5; i++ ) {
        lua_pushnumber(lua, arr[i]);
        printStack(lua);
        lua_rawseti(lua, 2, i + 1);

        printStack(lua);
    }

    lua_call(lua, 1, 1);

    int value;
    for( i = 0; i < 5; i++ ) {
        lua_rawgeti(lua, -1, i + 1);
        value = lua_tonumber(lua, -1);

        printStack(lua);

        lua_pop(lua, 1);

        printf("%d: %d\n", i, value);
    }

    lua_close(lua);

    return 0;
}

printStackはその名の通りスタックの中身を表示する関数なので通常動かす分には不要です。

Luaのコード

double.lua

function double (arr)
    for i = 1, #arr do
        arr[i] = arr[i] * 2
    end

    return arr
end

解説

C言語で配列を作り、Luaの関数doubleに引数として渡し、Lua側で加工した値を配列として受け取って標準出力に表示します。

Luaの関数に配列を引数として渡す

まず呼び出す関数doubleをスタックに載せます。

lua_getglobal(lua, "double");

次に引数として渡す配列を作り配列に載せます。(この時点では配列の中身は空です)

lua_newtable(lua);

この時点でのスタックは次のようになります。

-----------------------------
002(-001): TABLE
001(-002): FUNCTION
-----------------------------

先ほど述べたように現時点では配列は空です。 Luaに値を渡すためにこの配列に値を詰めていきます。 そのために、lua_pushnumberで値をスタックに載せます。

lua_pushnumber(lua, arr[i]);

この時点でのスタックは次のようになります。

-----------------------------
003(-001): NUMBER 3.000000
002(-002): TABLE
001(-003): FUNCTION
-----------------------------

lua_rawsetiを呼び出してスタックに載せた値を配列に詰めます。

lua_rawseti(lua, 2, i + 1);

引数として渡す配列は2番目に入っているので第2引数の"2"を指定しています。 第3匹数は値を詰める先の配列の添字です。Luaでは配列の添字は1から始まるので+1しています。

この時点でのスタックは次のようになります。

-----------------------------
002(-001): TABLE
001(-002): FUNCTION
-----------------------------

一番上にあったスタックの値が消えています。これはスタックに載った値が配列に移されたことを表しています。

このように、この2行は table[i+1] = arr[i] に相当します。

これをarrの要素数分だけ繰り返して、Luaの関数doubleに引数として渡す配列を組み立てます。

関数を実行して実行結果の配列を受け取る

ここでlua_callを呼び出します。引数は配列1つ、戻り値も配列1つなので、下記のようになります。

lua_call(lua, 1, 1);

この時点でのスタックは下記のようになります。

-----------------------------
001(-001): TABLE
-----------------------------

先ほどの001:FUNCTIONに002:TABLEが渡され両者ともスタックから消され、関数の実行結果として得られた配列が新たにスタックに載っています。

この配列の中身を取り出して表示していきます。 まず、lua_rawgetiを呼び出して、配列の1要素目をスタックに載せます。

lua_rawgeti(lua, -1, i + 1);

第2引数は実行結果の配列が格納されているスタックの位置です。 第3匹数は配列の中から取り出す対象要素の添字です。先程述べたようにLuaの配列は1から始まるのでここでも+1しています。

この時点でのスタックは次の通りです。

-----------------------------
002(-001): NUMBER 6.000000
001(-002): TABLE
-----------------------------

スタックのトップに取り出した要素が載っています。これをlua_tonumberで取り出します。 その後、スタック上のトップにある値は不要なのでlua_popにより取り出します。

lua_pop(lua, 1);

この時点でのスタックは次の通りです。

-----------------------------
001(-001): TABLE
-----------------------------

これを要素数分繰り返していきます。

プログラムの最終的な実行結果は次のようになります。

0: 6
1: 12
2: 18
3: 24
4: 30

サンプル

https://github.com/sota2502/lua_sample3/

DelphiとLuaの連携

やりたいこと

Delphiのプログラム上でスプライトオブジェクトを動かします。ただし、このスプライトオブジェクトの動作をある程度動的に変更できるように、スプライトオブジェクトの動作をLuaで定義します。

準備

八角研究所:高速スクリプト言語「Lua」を始めよう!(7) - Delphi2009 から Lua を使う を参考に必要なライブラリを揃えます。

Delphiのコード

Unit1.pas

unit Unit1;

interface

uses
    Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
    Dialogs, ExtCtrls, Lua, LuaUtils;

type
    TCard = class
    private
        FHeart:   TBitmap;
        FSpade:   TBitmap;
        FCurrentImage: TBitmap;
        FX, FY:   Double;
        FVX, FVY: Double;
        FLuaState: Plua_State;
    public
        constructor Create(PosX, PosY: Double);
        destructor Destroy; override;

        procedure ChangeImage;
        procedure Move;

        property CurrentImage: TBitmap read FCurrentImage;
        property X: Double read FX;
        property Y: Double read FY;
        property VX: Double read FVX;
        property VY: Double read FVY;
    end;

type
    TForm1 = class(TForm)
        Timer1: TTimer;
        procedure FormCreate(Sender: TObject);
        procedure FormDestroy(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
    private
        { Private 錾 }
        FCard: TCard;
        FBuffer: TBitmap;
    public
        { Public 錾 }

        property Card: TCard read FCard;
    end;

var
    Form1: TForm1;

const
    CANVAS_WIDTH  = 320;
    CANVAS_HEIGHT = 240;

function ChangeImageGlue(L: Plua_State): Integer; cdecl;


implementation

{$R *.dfm}

function ChangeImageGlue(L: Plua_State): Integer;
begin
    Form1.Card.ChangeImage;
    Result := 1;
end;

constructor TCard.Create(PosX, PosY: Double);
begin
    FX := PosX;
    FY := PosY;
    FVX := 1;
    FVY := 1;

    FHeart := TBitmap.Create;
    FHeart.LoadFromFile('heart.bmp');

    FSpade := TBitmap.Create;
    FSpade.LoadFromFile('spade.bmp');

    FCurrentImage := FHeart;

    FLuaState := luaL_newstate;
    luaL_openlibs(FLuaState);
    luaL_dofile(FLuaState, 'operate.lua');
    lua_register(FLuaState, 'ChangeImageGlue', ChangeImageGlue);
end;

destructor TCard.Destroy;
begin
    FHeart.Free;
    FSpade.Free;

    lua_close(FLuaState);
end;

procedure TCard.ChangeImage;
begin
    if FCurrentImage = FHeart then
        FCurrentImage := FSpade
    else
        FCurrentImage := FHeart;
end;

procedure TCard.Move;
begin
    lua_getglobal(FLuaState, 'move_for_lua');
    lua_pushnumber(FLuaState, FX);
    lua_pushnumber(FLuaState, FY);
    lua_pushnumber(FLuaState, FVX);
    lua_pushnumber(FLuaState, FVY);

    lua_call(FLuaState, 4, 4);

    FX  := lua_tonumber(FLuaState, -4);
    FY  := lua_tonumber(FLuaState, -3);
    FVX := lua_tonumber(FLuaState, -2);
    FVY := lua_tonumber(FLuaState, -1);
end;

procedure TForm1.FormCreate(Sender: TObject);
var x, y: Double;
begin
    Self.ClientWidth  := CANVAS_WIDTH;
    Self.ClientHeight := CANVAS_HEIGHT;

    Randomize;
    x := Random(CANVAS_WIDTH);
    y := Random(CANVAS_HEIGHT);

    FCard := TCard.Create(x, y);

    FBuffer                    := TBitmap.Create;
    FBuffer.Canvas.Brush.Color := clWhite;
    FBuffer.Width              := CANVAS_WIDTH;
    FBuffer.Height             := CANVAS_HEIGHT;
end;

procedure TForm1.FormDestroy(Sender: TObject);
begin
    FCard.Free;
    FBuffer.Free;
end;

procedure TForm1.Timer1Timer(Sender: TObject);
var x, y: Integer;
begin
    FCard.Move;

    x := Trunc(FCard.X);
    y := Trunc(FCard.Y);
    FBuffer.Canvas.FillRect(Rect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT));
    FBuffer.Canvas.Draw(x, y, FCard.CurrentImage);

    Self.Canvas.Draw(0, 0, FBuffer);
end;

end.

ポイントとなる箇所を説明していきます。

    luaL_openlibs(FLuaState);
    luaL_dofile(FLuaState, 'operate.lua');

まず、TCardクラスのコンストラクタの中でPLua_Stateオブジェクトを初期化しメンバーとして持っておきます。

    lua_getglobal(FLuaState, 'move_for_lua');
    lua_pushnumber(FLuaState, FX);
    lua_pushnumber(FLuaState, FY);
    lua_pushnumber(FLuaState, FVX);
    lua_pushnumber(FLuaState, FVY);

    lua_call(FLuaState, 4, 4);

    FX  := lua_tonumber(FLuaState, -4);
    FY  := lua_tonumber(FLuaState, -3);
    FVX := lua_tonumber(FLuaState, -2);
    FVY := lua_tonumber(FLuaState, -1);

Timerで一定時間ごとにTCardオブジェクトのMoveが呼び出され新たな座標が算出されオブジェクトが移動します。この新たな座標の算出をLuaで行います。

move_for_luaを呼び出すために引数に合わせて順番に値をスタックに載せていきます。

lua_callにmove_for_luaの引数の数、戻り値の数を渡して呼び出します

その後、move_for_luaの計算結果を取得するためスタックから順番に値を取り出します。

 

function ChangeImageGlue(L: Plua_State): Integer; cdecl;

function ChangeImageGlue(L: Plua_State): Integer;
begin
    Form1.Card.ChangeImage;
    Result := 1;
end;

lua_register(FLuaState, 'ChangeImageGlue', ChangeImageGlue);

スプライトオブジェクトの画像を変更するグローバル関数をグルーコードとして用意します。

グルーコードの定義の形は決まっています。引数にPLua_Stateオブジェクトをとり、Intergerを返します。呼び出し規約にはcdeclを指定します。

このグルーコードをLuaから呼び出せるようにlua_registerで登録します。

Luaのコード

operate.lua

local CANVAS_WIDTH  = 320
local CANVAS_HEIGHT = 240
local ICON_WIDTH    = 32
local ICON_HEIGHT   = 32

function move_for_lua(x, y, vx, vy)
    if ( x < 0 ) then
        vx = 1
        ChangeImageGlue();
    elseif ( x + ICON_WIDTH > CANVAS_WIDTH ) then
        vx = -1
        ChangeImageGlue();
    end

    if ( y < 0 ) then
        vy = 1
        ChangeImageGlue();
    elseif ( y + ICON_HEIGHT > CANVAS_HEIGHT ) then
        vy = -1
        ChangeImageGlue();
    end

    x = x + vx
    y = y + vy

    return x, y, vx, vy
end

画面内を直線的に跳ね回る動きを定義しています。

移動ベクトルが変わる度に表示する画像を変えるためChangeImageGlueを呼び出しています。

move_for_luaでは新しい座標(x,y)と移動ベクトル(vx,vy)を返し、これをDelphi側で受け取りスプライトオブジェクトに反映させています。

サンプル

https://github.com/sota2502/LuaSampleForDelphi

続・C言語とLuaの連携

やりたいこと

前回の積分をpthreadで並列化します。

例えば[0.0, 1.0]の区間で2スレッドで積分する場合、1スレッド目は[0.0, 0.5]区間を、2スレッド目は[0.5, 1.0]区間を同時に積分して合算します。

準備

基本的には前回と一緒です。

pthreadはだいたいの環境に入っていると思いますが、場合によっては別途インストールする必要があります。

C言語のコード

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>
#include <semaphore.h>
#include "include/lualib.h"
#include "include/lauxlib.h"

#define MAX_THREAD_NUM  (1)
#define DIVISION_COUNT 1000

typedef struct segment_t {
    double start;
    double end;
    double interval;
    char*  lua_file;
} segment_t;

/* グローバル変数 (全スレッド共有) */
sem_t sem;
double sum = 0;

void init_segment(segment_t** segments, double start, double end, int thread_num, char* lua_file) {
    *segments = (segment_t*)malloc(sizeof(segment_t) * thread_num);
    int i;
    double interval = (end - start) / DIVISION_COUNT;
    double width    = (end - start) / thread_num;

    segment_t* array = *segments;
    for ( i = 0; i < thread_num; i++ ) {
        array[i].start    = ( i > 0 ) ? array[i - 1].end : start;
        array[i].end      = ( i < thread_num - 1 ) ? array[i].start + width : end;
        array[i].interval = interval;
        array[i].lua_file = lua_file;
    }
}

void thread_func(void *arg) {
    segment_t* segment = (segment_t*)arg;

    lua_State *L = luaL_newstate();
    luaL_openlibs(L);
    (void)luaL_dofile(L, segment->lua_file);

    double result = 0;
    double x;
    int i;

    for( i = 0, x = segment->start + segment->interval / 2; x < segment->end; i++, x += segment->interval) {
        lua_getglobal(L, "calc");
        lua_pushnumber(L, x);
        lua_call(L, 1, 1);
        double y = lua_tonumber(L, -1);
        result += y * segment->interval;
    }

    /* セマフォによる排他制御の開始と終了 */
    sem_wait(&sem);
    sum += result;
    sem_post(&sem);

    lua_close(L);
}

int main(int argc, char* argv[]) {
    int i;
    if( argc < 5 ) {
        printf("usage: integrate START END THREAD_NUM LUAFILE\n");
        return 0;
    }
    double start   = atof(argv[1]);
    double end     = atof(argv[2]);
    int thread_num = atoi(argv[3]);
    char* lua_file = argv[4];
    segment_t* segments;

    init_segment(&segments, start, end, thread_num, lua_file);
    pthread_t* handle = (pthread_t*)malloc(sizeof(pthread_t) * thread_num);
    /* セマフォ変数の初期化 */
    sem_init(&sem, 0, MAX_THREAD_NUM);

    for (i = 0; i < thread_num; ++i)
        pthread_create(&handle[i], NULL, (void *)thread_func, (void *)&segments[i]);

    for (i = 0; i < thread_num; ++i) 
        pthread_join(handle[i], NULL);

    printf("result: %.16f\n", sum);

    /* セマフォ変数の破棄 */
    sem_destroy(&sem);

    free(handle);
    free(segments);

    return 0;
}

ポイントとしてはLuaステートをスレッド間で別々に持っておくことです。

スレッド間で共有しようとすると競合して落ちるようです。

その分メモリは消費しますが、コア数から考えてそんなに大量に生成するものでもないので気にする必要はないと考えています。

Luaのコード

この前と変わりありません。

実行結果

[0.0, 1.0]区間で4スレッドでpi.luaで与えられる関数を積分

# ./integrate 0 1 4 pi.lua
result: 3.1415927369231267

サンプル

https://github.com/sota2502/lua_sample2

余談

この程度の計算だとスレッドに分割するコストの方が積分コストに比べて大きいので並列化するメリットがありません…。(むしろデメリット)

C言語とLuaの連携

やりたいこと

C言語積分するのを目的とします。ただし、積分対象となる関数は入れ替え可能なようにLuaで与えることにします。

具体的な例題として、1/(1 + x^2) を [0, 1] の区間で積分し、4をかけて円周率Piを算出します。

準備

gccLuaのインストールはされているものとして省略します。

C言語Luaを組み込むためのライブラリを次のサイトからダウンロードしてきます。

http://luabinaries.sourceforge.net/download.html

今回、MacOSX で開発したので、ua-5.2.1_MacOS107_lib.tar.gz ダウンロードしてきました。

解凍するといくつかファイルが存在しますが、今回必要なのはincludeディレクトリ内のヘッダーファイル群です。

C言語のコード

積分するC言語のコードです。やってることは単純な区分求積です。

integrate.c
#include <stdio.h>
#include <stdlib.h>
#include "include/lualib.h"
#include "include/lauxlib.h"

#define DIVISION_COUNT   1000

int main (int argc, char* argv[]) {
    lua_State *L = luaL_newstate();
    if ( argc < 4 ) {
        printf("usage: integrate start end calc.lua\n");
        return 1;
    }

    double start      = atof(argv[1]);
    double end        = atof(argv[2]);
    if ( start >= end ) {
        printf("end should be larger than start");
        return 1;
    }

    char* lua_file = argv[3];
    double width   = (end - start) / (double)DIVISION_COUNT;
    double delta   = width / 2.0;

    printf("DIVISION_COUNT       : %d\n", DIVISION_COUNT);
    printf("integration interval : %f -> %f\n", start, end );
    printf("width                : %f\n", width);
    printf("delta                : %f\n", delta);
    printf("calc                 : %s\n", lua_file);

    luaL_openlibs(L);
    (void)luaL_dofile(L, lua_file);

    double sum;
    double x;
    for( x = start + delta; x < end; x += width ) {
        lua_getglobal(L, "calc");

        lua_pushnumber(L, x);
        lua_call(L, 1, 1);

        double y = lua_tonumber(L, -1);
        sum += y * width;
    }

    printf("%f\n", sum);

    lua_close(L);

    return 0;
}

gccでコンパイルします。

gcc -Wall -o integrate -llua integrate.c

積分対象の関数を書いたLuaコード

積分対象となる関数をcalc関数として書いています。

pi.lua 
function calc(x)
    local y = 1.0 / (1 + x * x);
    return y * 4;
end

実行結果

./integrate 0 1 pi.lua

DIVISION_COUNT       : 1000
integration interval : 0.000000 -> 1.000000
width                : 0.001000
delta                : 0.000500
calc                 : pi.lua
3.141593

 

サンプル

https://github.com/sota2502/lua_sample1

技術的負債返済に対する論理回路的アプローチ

CCとは

Cyclomatic Complexityの略で関数の簡潔さ/複雑さを測定する指標の一つです。
CC、複雑さの状態、バグ混入率は以下のようになるそうです。
http://d.hatena.ne.jp/szk-takanori/20111219/p1







循環的複雑度複雑さの状態バグ混入確率
10以下非常に良い構造25%
30以上構造的なリスクあり40%
50以上テスト不可能70%
75以上いかなる変更も誤修正を生む98%

以下ではPerlを例に実際のCCの計算方法を書いています。
以下のような構成でコードを書いているとします。

./
└sample
  └sample1.pl

sample1.pl

#!/usr/bin/perl 
use strict;
use warnings;

sub func1 {
    my $input = shift;

    my $output;
    if( $input eq 'a' ) {
        $output = 'A';
    } else {
        $output = 'B';
    }

    print "output: $output\n";
}

my $input = <>;
chomp($input);
func1($input);

この場合、Perl::Metrics::Simple を用いて調べることができます。

#!/usr/bin/perl 
use strict;
use warnings;

use Perl::Metrics::Simple;
use Data::Dumper;

my $analyzer = Perl::Metrics::Simple->new;
my $analysis = $analyzer->analyze_files('sample');

print Dumper $analysis->file_stats;
print "\n";
print Dumper $analysis->subs;
print "\n";

出力結果

$VAR1 = [
          {
            'path' => 'sample/sample1.pl',
            'main_stats' => {
                              'mccabe_complexity' => 1,
                              'name' => '{code not in named subroutines}',
                              'path' => 'sample/sample1.pl',
                              'lines' => 5
                            }
          }
        ];

$VAR1 = [
          {
            'mccabe_complexity' => 4,
            'name' => 'func1',
            'lines' => 10,
            'path' => 'sample/sample1.pl'
          }
        ];

ここの mccabe_complexity がCCにあたります。

CCの例

sample1.pl の func1では入力がaであるときにはA、それ以外のときにはBを返すように実装されていました。
この時点でのCCは4です。

ここで仕様の追加がありました。
入力がbのときはBを、それ以外はCを返すように修正します。

#!/usr/bin/perl 
use strict;
use warnings;

sub func1 {
    my $input = shift;

    my $output;
    if( $input eq 'a' ) {
        $output = 'A';
    } elsif( $input eq 'b' ) {
        $output = 'B';
    } else {
        $output = 'C';
    }

    print "output: $output\n";
}

my $input = <>;
chomp($input);
func1($input);

この時点でCCは6に上がります。

このように仕様追加に応じて設計を見直さず、ifブロックを肥大化させていくとそれに応じてCCも増加し、メンテのしづらいコードとなります。

CCを下げる

func1を見る限り、$inputから$outputを得られるように分割すれば良さそうです。
なので、この部分をメソッドに切り出します。

sub get_character {
    my $input = shift;

    if( $input eq 'a' ) {
        return 'A';
    } elsif( $input eq 'b' ) {
        return 'B';
    } else {
        return 'C';
    }
}

sub func1 {
    my $input = shift;

    my $output = get_character($input);

    print "output: $output\n";
}

この場合の先ほどのCCの計測結果は次のようになります。

$VAR1 = [
          {
            'path' => 'sample/sample1.pl',
            'main_stats' => {
                              'mccabe_complexity' => 1,
                              'name' => '{code not in named subroutines}',
                              'path' => 'sample/sample1.pl',
                              'lines' => 5
                            }
          }
        ];

$VAR1 = [
          {
            'mccabe_complexity' => 6,
            'name' => 'get_character',
            'lines' => 10,
            'path' => 'sample/sample1.pl'
          },
          {
            'mccabe_complexity' => 1,
            'name' => 'func1',
            'lines' => 5,
            'path' => 'sample/sample1.pl'
          }
        ];

func1のCCは1に減りました。
一方切り出したget_characterのCCは6のままです。これを減らすように努めてみます。
まずは、最後のelseはなくても論理的に等価なのでelseを取り除きます。

sub get_character {
    my $input = shift;

    if( $input eq 'a' ) {
        return 'A';
    } elsif( $input eq 'b' ) {
        return 'B';
    }

    return 'C';
}

するとCCは1下がって5になりました。

もう少し見てみましょう。
実はifブロックはa→A、b→Bにマッピングしてるだけです。なので、これをハッシュで定義してelsifをなくします。

my %map = (
    a => 'A',
    b => 'B',
);
sub get_character {
    my $input = shift;

    if ( defined $map{$input} ) {
        return $map{$input};
    }

    return 'C';
}

するとCCは2になりました。

今回は非常にシンプルなパターンでした。
各条件に対してもっと複雑な処理が必要な場合、%mapで各処理用のメソッドをマッピングするとよいです。

my %map = (
    a => \&do_a,
    b => \&do_b,
);
sub do_a { ... }
sub do_b { ... }
sub do_character {
    my $input = shift;

    my $method = $map{$input};
    if ( defined $method ) {
        &$method();
    }
    ...
}

もしくはクラス設計を見直し、Templateパターンで分割するとよいです。

このようにしてまとまったブロックは別メソッドに切り出して行くことで元のメソッドのCCを下げていくことができます。

カルノー

今まではぱっと見で分かるような簡単な論理式でした。
しかし、もし、あなたの目の前に入力-出力は分かっている(幸いにもテストが存在する!)が内部に複雑な論理評価を擁するメソッドが与えられ、このリファクタの必要に迫られたとします。
できるだけ論理式を簡潔にまとめたい…そんなときカルノー図を利用して論理式を分析&簡略化してみます。

カルノー図とは論理変数とその出力結果を下記のように並べたものです。

カルノー図は主に論理回路の設計時に論理式を簡単化したい場合に用いられます。

カルノー図を利用した論理式の簡略化

次の論理式が与えられたとします。

Z = A・B・C + A・B・¬C + ¬A・B・C + ¬A・¬B・C + ¬A・¬B・¬C

この論理式を正直にコードに書起すと以下のようになります。

sub func1 {
    my ($a, $b, $c) = @_;

    if ( $a && $b && $c ) {
        return 1;
    } elsif ( $a && $b && !$c ) {
        return 1;
    } elsif ( !$a && $b && $c ) {
        return 1;
    } elsif ( !$a && !$b && $c ) {
        return 1;
    } elsif ( !$a && !$b && !$c ) {
        return 1;
    }

    return 0;
}

この時点でCCは23です。

この論理式をカルノー図に書起すと下記のようになります。

この表の中で、1が書かれている部分を1辺が2^Nの長方形で囲っていきます。
このとき、一括りに囲まれた部分は論理式を簡略化することができます。

例えば A・B・C と A・B・¬C の部分を見ると、Cに依存せず1になることが分かります。
これを繰り返してもれなく正方形の部分を拾って行くと先ほどの論理式は下記のようになります。

Z = ¬A・¬B + A・B + ¬A・C

もう少しまとめると以下のようになります。

Z = ¬A(¬B + C) + A・B

コードに書起すと以下のようになります

sub func1 {
    my ($a, $b, $c) = @_;

    if ( !$a ) {
        if ( !$b ) {
            return 1;
        }
        if ( $c ) {
            return 1;
        }
    }

    if ( $a && $b ) {
        return 1;
    }

    return 0;
}

この時点でCCは8まで下がりました。

まとめ

コードのリファクタにカルノー図を利用することを試みてみました。
ただ、実際には一つ一つの論理評価のコストが等価ではなく、ある特定の部分がボトルネックになるような場合は無理して簡単化するとパフォーマンスが一気に悪化してしまうことがあるので慎重に行ってください。

そもそも、普通に幸せな生活を送っていれば、こんなニッチなテクニックは一生使うことないと思います。。

mixiページについたイイネ/コメントを通知してくれるデスクトップアプリ(2)

前回からの続き

前回はmixi Graph APIを利用してmixiページのページ情報やフィード情報を取得するコードを紹介しました。
今回はこれらのAPIを一定間隔でコールし、前回の呼び出し結果の差分を用いて、ユーザーに通知するアプリを作りました。

ソースコード

githubに上げています。ただ、まだ細かいところは詰め切れていません。
https://github.com/sota2502/mpstation

動作の確認方法

1. mixiページを作成します
2. mixiページアプリを登録します
3. 先ほど作成したmixiページに登録したmixiページアプリをインストールします
4. githubのソースコードをダウンロードし、2.で登録したmixiページアプリのConsumerKey、ConsumerSecretをSettings.pasに記述しコンパイルします。
5. pages.txtに作成したページのIDを記載します
6. コンパイルしたプログラムを実行します

実行するとタスクトレイにアイコンが表示されます。
右クリックでコンテキストメニューが表示され、「更新をチェック」を押下すると「フォロワーが増えた」「イイネ・コメントがついた」かどうかをチェックしてメッセージを表示します。
また、60秒ごとにTimerでのチェックも行っていますので、タスクトレイに常駐させたまま放っておくと定期的に更新があったかどうかをお知らせしてくれます。

mixiページについたイイネ/コメントを通知してくれるデスクトップアプリ

はじめに

mixiページを運用する上で、フォロワーからのイイネ/コメントに対して、管理者は迅速に対応することが求められます。
しかし、現状ではmixiページにイイネ/コメントがついても管理者アカウントのホーム画面に通知されるのみで、フォロワーからのレスポンスがあったことを素早く知る術がありません。
そこで、Windowsのタスクトレイに常駐し、mixiページに対してフォロワーからのレスポンスがあった際に瞬時に知らせてくれるデスクトップアプリを開発し、管理者とフォロワーのコミュニケーションを手助けすることを目指します。

クライアント認証されたアクセストークン

mixi Graph APIによりページの情報を取得する際にはクライアント認証されたアクセストークンを使用します。
ここではクライアント認証されたアクセストークンの取得方法を説明します。

まず事前準備として、Partner Dashboardからmixiページアプリを登録し、Consumer KeyとConsumer Secretを取得します。
そして、登録したmixiページアプリを自身が運営するページにインストールします。
ページアプリからmixi Graph APIを利用してページの情報を取得するには、そのページアプリが対象のページにインストールされていることが求められるからです。

クライアンt認証されたアクセストークンの取得には、前回同様、IndyIndy OpenSSL 0.9.8h IntraWeb Edition、superobjectを利用しています。

var
    http: TIdHttp;
    params: TStringList;
    ssl: TIdSSLIOHandlerSocketOpenSSL;
    token_res, access_token, refresh_token: String;
    token_json: ISuperObject;

const
    CONSUMER_KEY = '[YOUR CONSUMER KEY]';
    CONSUMER_SECRET = '[YOUR CONSUMER SECRET]';

    HTTP_TIMEOUT = 300;
    TOKEN_ENDPOINT = 'https://secure.mixi-platform.com/2/token’;

begin
    http := TIdHttp.Create;
    ssl := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
    params := TStringList.Create;

    //SSLの設定
    ssl.SSLOptions.Method := sslvTLSv1;
    ssl.SSLOptions.Mode := sslmUnassigned;
    ssl.SSLOptions.VerifyMode := [];
    ssl.SSLOptions.VerifyDepth := 0;

    //Timeoutを設定しないとエラーになる
    http.ReadTimeout := HTTP_TIMEOUT;
    http.ConnectTimeout := HTTP_TIMEOUT;
    http.IOHandler := ssl;

    //Token取得用のリクエストパラメータの設定
    params.Values['grant_type']    := 'client_credentials';
    params.Values['client_id']     := CONSUMER_KEY;
    params.Values['client_secret'] := CONSUMER_SECRET;

    //Tokenのリクエストを取得
    token_res := http.Post(TOKEN_ENDPOINT, params);

    //レスポンスのJSONをパースしてaccess_token, refresh_tokenを取り出す
    token_json    := SO(token_res);
    access_token  := token_json['access_token'].AsString;
    refresh_token := token_json['refresh_token'].AsString;
end;

ページ基本情報の取得

上記で取得したクライアント認証されたアクセストークンを用いてページ基本情報を取得します。

var
    response: String;
    json: ISuperObject;
begin
    response := http.Get(
        'http://api.mixi-platform.com/2/pages/1234?access_token='
        + access_token
    );
    json := SO(Utf8ToAnsi(response));

    WriteLn(json['entry.id'].AsInteger);           //pageId
    WriteLn(json['entry.displayName'].AsString);   //ページ名
    WriteLn(json['entry.followerCount']AsInteger); //フォロワー数
end;

フィードの一覧取得

同様にしてフィードの一覧取得を行います。

var
    response: String;
    json, feed: ISuperObject;
    i, count: Integer;
begin
    response := http.Get(
        'http://api.mixi-platform.com/2/pages/1234/feeds?access_token='
        + access_token
    );
    json := SO(Utf8ToAnsi(resopnse));

    count := json['entry'].AsArray.Length;

    for i := 0 to count - 1 do
    begin
        feed := json['entry'].AsArray.O[i];

        WriteLn(feed['contentUri'].AsString);     //コンテンツのURI
        WriteLn(feed['title'].AsString);          //フィードのタイトル
        WriteLn(feed['body'].AsString);           //フィードの本文
        WriteLn(feed['favoriteCount'].AsInteger); //フィードについたイイネ数
        WriteLn(feed['commentCount'].AsInteger);  //フィードについたコメント数
    end;
end;

しかし・・・

ここまででページの情報を取得できるようになりました。
あとはある一定の間隔でAPIから情報を取得し、前回の取得結果との差分が発生していれば、何かしらの通知を出すことで今回の目標は達成できます。

しかし、ここで問題が発生。
フィードの一覧を取得する際、レスポンスが同じにも関わらず、Utf8→Shift JISの変換、もしくはJSONのパースに失敗するという事例が発生。
このバグの原因が分からず現在停止注意・・・。