F# Interactiveでunmanagedコードをデバッグ

F# Interactiveでデバッグをする際は、Interactiveウィンドウ上で右クリックメニュー"Start Debugging"を利用するか、fsxファイル上で右クリックメニュー"Debug In Interactive"を利用すると思います。

しかしこの方法ではデバッガがマネージドコードのみを対象としてしまうため、ネイティブのソースコード内のブレークポイントにはヒットしません。

通常、.Netのアプリをデバッグする際、プロジェクトの"アンマネージコードデバッグを有効にする"を有効にすればネイティブコードもデバッグが可能になります。
f:id:any-programming:20180919173911p:plain

しかしF# Interactiveのexeは上記オプションが有効になっていないため、デバッガがマネージドモードで起動されます。(と思います。)

下記に示すデバッガのネイティブモードでの起動は、特段F# Interactiveに限ったものではなく、汎用的に利用できる方法です。

ネイティブモードでデバッガ起動

"デバッグ"→"プロセスにアタッチ"でダイアログを表示します。

対象プロセス(今回はfsi.exe)を選択し、アタッチ先を"ネイティブコード"に変更して、アタッチします。
f:id:any-programming:20180919174623p:plain

後はF# Interactive上でコードを実行すればネイティブコード内のブレークポイントにヒットするようになります。

そのかわりマネージドコードのブレークポイントにはヒットしなくなります。(アタッチ先で"マネージド/ネイティブ"が選択肢にないみたいです。)





メインスレッド以外でWPFを利用する

WPFのWindowをmain関数内で表示する場合は、下記のような書き方になる。

[<STAThread>]
[<EntryPoint>]
let main argv = 
    Application().Run(Window()) |> ignore


別スレッドでWindowを表示したい場合は下記のようになる。

let startApp() =
    Application().Run(Window()) |> ignore
let thread = Thread(startApp)
thread.SetApartmentState(ApartmentState.STA)
thread.Start()


ただしApplicationインスタンスはAppDomainに対し一つしか生成できないため、上記のスレッドを二つ以上走らせるとエラーとなる。

ちなみにApplicationインスタンスを生成しない場合(WPFのコントロールクラスの利用のみなど)スレッドを複数生成できるが、WPFのコントロールを生成(DispatcherObjectを継承したクラスの生成)した際にDispatcherが生成されるため、自前で開放する必要がある。

let noApplication() =
    let button = Button()
    Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.SystemIdle)
    Dispatcher.Run()    
let thread = Thread(noApplication)
thread.SetApartmentState(ApartmentState.STA)
thread.Start()






_CrtSetAllocHookでメモリアロケーション検知

VC++にはメモリアロケーションをフックするために、_CrtSetAllocHookという関数が用意されています。

今回はこれを利用してみたいと思います。

ちなみに_DEBUGマクロが定義されていないと動作しません。

Hook関数

アローケーション時にprintfする関数を定義してみます。

blockTypeが_CRT_BLOCKの時はCランタイムライブラリによって呼び出されたものなので無視します。(そうしないとprintfでクラッシュします。)

allocTypeが_HOOK_ALLOCの時にprintfします。

返り値をTRUEにすることで、既定のアロケーションを呼び出すようにします。(FALSEだとアロケーションが行われません。)

int Hook(
    int allocType,
    void *userData,
    size_t size,
    int blockType,
    long requestNumber,
    const unsigned char *filename,
    int lineNumber
) {
    if (blockType == _CRT_BLOCK) {
        return TRUE;
    }
    if (allocType == _HOOK_ALLOC) {
        printf("alloc\n");
    }
    return TRUE;
}


動作確認

下記の様に記述すると、newの行でallocと出力されます。

_CrtSetAllocHook(Hook);
int* p = new int(10); // alloc

ちなみに、std::functionにラムダを代入する際はキャプチャの大きさによってアロケーションが行われたり行われなかったりするのですが、それを確認できたりします。

_CrtSetAllocHook(Hook);

int x1;
std::function<void()> f1 = [x1] {};

int x2[10];
std::function<void()> f2 = [x2] {}; // alloc

Hookを解除する場合はNULLを設定します。

_CrtSetAllocHook(NULL);






PHP --- DateTimeを利用してカレンダー作成

PHPには標準でカレンダーを作成する機能はないので、実装の一例を紹介します。

二次元配列生成

年月を入力すると、日曜始まりのカレンダー二次元配列が生成されます。(日付が存在しない場所は0が入っています。)

DateTimeのformatに'w'を渡すと曜日(日:0~土:6)が取得でき、't'を渡すとその月の日数が取得できます。

<?php
function getCalendar(int $year, int $month) : array {
    $firstDate = DateTime::createFromFormat('Y-m-d', "{$year}-{$month}-1");
    assert($firstDate);
    $firstDayOfWeek = (int)$firstDate->format('w');
    $lastDay = (int)$firstDate->format('t');
    $weeks = (int)ceil(($firstDayOfWeek + $lastDay) / 7);

    $calender = [];
    for ($i = 0; $i < $weeks * 7; $i++) {
        $day = $i + 1 - $firstDayOfWeek;
        if ($day <= 0 || $lastDay < $day) {
            $day = 0;
        }
        $calender[intdiv($i, 7)][$i % 7] = $day;
    }
    return $calender;
}


Table生成

<table>
    <thead>
        <tr>
            <th></th>
            <th></th>
            <th></th>
            <th></th>
            <th></th>
            <th></th>
            <th></th>
        </tr>
    </thead>
    <tbody>
<?php foreach (getCalendar(2018, 6) as $week) { ?>
        <tr>
<?php foreach ($week as $day) { ?>
            <td>
                <?php if (0 < $day) echo $day ?>
            </td>
<?php } ?>
        </tr>
<?php } ?>
    </tbody>
</table>

f:id:any-programming:20180628182608p:plain





PHP --- POSTで送られてきた画像をリサイズして保存

GDを利用するため、有効にしておいてください。

php.ini

extension=php_gd2.dll


<?php
$originalImage = imagecreatefromjpeg($_FILES['image']['tmp_name']);
assert($originalImage);

$originalWidth  = imagesx($originalImage);
$originalHeight = imagesy($originalImage);

$resizedWidth  = 100;
$resizedHeight = (int)($originalHeight * $resizedWidth / $originalWidth);

$resizedImage = imagecreatetruecolor($resizedWidth, $resizedHeight);
assert($resizedImage);

$result = imagecopyresampled(
    $resizedImage,
    $originalImage,
    0,
    0,
    0,
    0,
    $resizedWidth,
    $resizedHeight,
    $originalWidth,
    $originalHeight
);
assert($result);

$dstPath = '...';
$result = imagejpeg($resizedImage, $dstPath);
assert($result);

imagedestroy($originalImage);
imagedestroy($resizedImage);






designModeを利用して、ブラウザ上でリッチテキストエディタ

ブラウザ上で高機能なテキストエディタを実現したい時、ブラウザに元々備わっているdesignModeを利用することができます。

iframeを用意

ページにテキストエディタとなるiframeを配置します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>デザインモード</title>
</head>
<body>
<iframe id="design-area" width="500" height="500"></iframe>
</body>
</html>


designModeをON

下記のようにdesignModeをONにするだけでテキストが入力できるようになります。

var designDoc = document.getElementById("design-area").contentDocument;
designDoc.designMode = "On";

f:id:any-programming:20180627131735p:plain

execCommand

designModeをONにしただけではテキスト入力できるだけです。(貼り付けでHTMLを貼り付けることは可能です。)

例えば太字にしたい、色を付けたい、などを実現するにはexecCommandを利用します。

下記は太字化の機能を加えたものとなります。

<button onclick="bold()">bold</button>
function bold() {
    designDoc.execCommand("bold");
}

f:id:any-programming:20180627133259p:plain

貼り付け時にテキストのみ貼り付ける

デフォルトでは貼り付けで、コピーした色々なHTMLを貼り付けることができます。

テキストだけを貼り付けたい時は、下記のようにonpaste関数を置き換えます。

改行文字は削除していますが、brに置き換えてもいいと思います。

bodyDoc.body.onpaste = function(ev) {
    ev.preventDefault();
    var text = ev.clipboardData.getData("text/plain");
    text = text.replace(/\r?\n|\r/g, "");
    bodyDoc.execCommand("insertHTML", false, text);
};


画像の挿入

一度画像をアップロードしてから、execCommandでURLを貼り付ける手順となります。

FormDataにデータを詰めて、XMLHttpRequestでサーバーにリクエストを飛ばします。

今回はサーバー側にupload-imageのpostが準備してあり、成功なら画像のURLが返ってくる状況を想定しています。

inputのonchangeでvalueをクリアして、おなじファイルを選んでもイベントが実行されるようにしています。

<input type="file" accept="image/jpeg" onchange="uploadImage(this.files); this.value = ''">
function uploadImage(files) {
    if (files.length !== 1) {
        return;
    }
    var request = new XMLHttpRequest();
    request.open("POST", "/upload-image");
    var data = new FormData();
    data.append("image", files[0]);

    request.onreadystatechange = function () {
        if (request.readyState === 4) {
            if (request.status === 200) {
                var imageUrl = request.responseText;
                designDoc.execCommand("insertImage", false, imageUrl);
            } else {
                alert("アップロードに失敗しました。");
            }
        }
    };

    request.send(data);
}


デフォルトの段落要素を変更

エディタ上で改行を行うと、Chromeならdivで一文が囲まれます。

一方IEだとpで囲まれます。

これを統一するのには下記を実行します。

designDoc.execCommand("DefaultParagraphSeparator", false, "div");

しかしChromeだと最初の一文がdivで囲まれなかったり、完全統一とはいかない様子です。

内容の取得

実際のHTMLテキストは下記でアクセスできます。

designDoc.body.innerHTML






PHP --- フレームワークを利用しないでToDoアプリ作成(4)

前回までで、基本的なToDoアプリの実装を行いました。
PHP --- フレームワークを利用しないでToDoアプリ作成(3) - 何でもプログラミング

今回はCSRF対策とログイン機能を盛り込んでみたいと思います。

両方ともセッションを利用しますので、$_SESSIONにアクセスする前までに下記を呼んでおいてください。

session_start();


CSRF対策

iframe内で他ページを開くなどして、勝手に送出された悪意あるPOSTをはじくために、各フォームにtokenを埋め込んでおきます。

フォーム表示ごとにtokenを作り直してもいいのですが、今回はセッションIDと同じ寿命にして実装しました。


tokenを管理するために、下記のような関数群を定義しました。

tokenの生成のアルゴリズムや格納キーは適宜変更しても問題ありません。

<?php
function setNewCsrfToken(string $seed) : void {
    $token = sha1($seed . session_id() . microtime());
    $_SESSION['csrfToken'] = $token;
}
function isCsrfTokenSet() : bool {
    return isset($_SESSION['csrfToken']);
}
function getCsrfToken() : string {
    if (isset($_SESSION['csrfToken']) === false) {
        throw new Exception403();
    }
    return $_SESSION['csrfToken'];
}
function assertCsrfToken(string $token) : void {
    if (isset($_SESSION['csrfToken']) === false || $_SESSION['csrfToken'] !== $token) {
        throw new Exception403();
    }
}

アプリケーションの初めに、tokenが準備されていなかったら生成するコードを追加します。

setNewCsrfTokenに渡すseedは適宜変更してください。

<?php
if (isCsrfTokenSet() === false) {
    setNewCsrfToken('todo');
}

今まで作成したすべてのフォームにtokenを埋め込みます。

<input type="hidden" name="token" value="<?php safeEcho(getCsrfToken()) ?>">

POSTを処理する関数の各々でtokenをチェックしてもいいのですが、今回はrespond関数を書き換えて一括でチェックします。

<?php
function respond(string $pathInfo, array $routesGET, array $routesPOST, callable $onError) : void {
    try {
        if ($_SERVER['REQUEST_METHOD'] === 'GET') {
            $response = getResponse($pathInfo, $routesGET);
        } else if ($_SERVER['REQUEST_METHOD'] === 'POST') {
            assert403(isset($_POST['token']));
            assertCsrfToken($_POST['token']);
            $response = getResponse($pathInfo, $routesPOST);
        }
        if (isset($response)) {
            $response->send();
        } else {
            throw new Exception404();
        }
    } catch (Exception $e) {
        $onError($e)->send();
    }
}


ログイン機能

ToDoの編集を、ログインした人のみできるよう実装してみたいと思います。

ログイン状態を管理するよう、下記の関数群を定義します。

<?php
function setAuthenticated(bool $authenticated) : void {
    $_SESSION['authenticated'] = $authenticated;
}
function isAuthenticated() : bool {
    return $_SESSION['authenticated'] ?? false;
}
function assertAuthenticated() : void {
    if (isAuthenticated() === false) {
        throw new Exception403();
    }
}

ログイン画面を下記のように定義します。

login.php

<?php ob_start(); ob_implicit_flush(0); ?>

<?php if ($showError) { ?>
<p>ログインできませんでした。</p>
<?php } ?>

<form action="/login" method="post">
    <input type="hidden" name="token" value="<?php safeEcho(getCsrfToken()) ?>">
    <ul>
        <li>
            <label>
                ユーザー
                <input type="text" name="user">
            </label>
        </li>
        <li>
            <label>
                パスワード
                <input type="password" name="password">
            </label>
        </li>
        <li>
            <button type="submit">送信</button>
        </li>
    </ul>
</form>

<?php
$title = 'ログイン';
$body  = ob_get_clean();
require 'layout.php';
?>

f:id:any-programming:20180627113837p:plain

layout.phpにログアウトボタンを追加します。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title><?php safeEcho($title) ?></title>
</head>
<body>
<header>
    <p>ToDo</p>
<?php if (isAuthenticated()) { ?>
    <form action="/logout" method="post">
        <input type="hidden" name="token" value="<?php safeEcho(getCsrfToken()) ?>">
        <button type="submit">ログアウト</button>
    </form>
<?php } ?>
</header>
<main>
    <?php echo $body ?>
</main>
</body>
</html>

todoList.phpを、権限によって表示するものを変えるようにします。

<?php ob_start(); ob_implicit_flush(0); ?>

<?php if (isAuthenticated()) { ?>
<a href="/add">追加</a>
<?php } ?>

<table>
<?php foreach ($todos as $todo) { ?>
    <tr>
        <td>
            <?php safeEcho($todo->date->format('Y/m/d')) ?>
        </td>
        <td>
            <?php safeEcho($todo->text) ?>
        </td>
<?php if (isAuthenticated()) { ?>
        <td>
            <form action="/delete" method="post">
                <input type="hidden" name="token" value="<?php safeEcho(getCsrfToken()) ?>">
                <input type="hidden" name="id" value="<?php safeEcho($todo->id) ?>">
                <button type="submit">削除</button>
            </form>
        </td>
<?php } ?>
    </tr>
<?php } ?>
</table>

<?php
$title = 'ToDo一覧';
$body  = ob_get_clean();
require 'layout.php';
?>

index.phpを下記のように書き換えています。

ユーザーとパスワードは現状固定で判定しています。

セキュリティ向上のため、ログイン時にセッションIDとtokenの再生成を行っています。

<?php
function todoList() : Response {
    // 変更なし
}

function todoAdd() : Response {
    assertAuthenticated();
    // 変更なし
}

function login() : Response {
    $showError = false;
    $content = render('login.php', compact('showError'));
    return Response::ok($content);
}

function post_login() : Response {
    $user     = assertPOST('user');
    $password = assertPOST('password');
    if ($user === 'user' && $password === 'pass') {
        setAuthenticated(true);
        session_regenerate_id(true);
        setNewCsrfToken('todo');
        return Response::redirect('/');
    } else {
        $showError = true;
        $content = render('login.php', compact('showError'));
        return Response::badRequest($content);
    }
}

function post_logout() : Response {
    setAuthenticated(false);
    return Response::redirect('/');
}

function post_todoAdd() : Response {
    assertAuthenticated();
    // 変更なし
}

function post_todoDelete() : Response {
    assertAuthenticated();
    // 変更なし
}

function onError(Exception $e) : Response {
    if ($e instanceof Exception400) {
        return Response::badRequest('400 Bad Request');
    } else if ($e instanceof Exception403) {
        return Response::forbidden('403 Forbidden');
    } else if ($e instanceof Exception404) {
        return Response::notFound('404 Not Found');
    } else {
        return Response::internalServerError('500 Internal Server Error');
    }
}

$routesGET = [
    '/'      => 'todoList',
    '/add'   => 'todoAdd',
    '/login' => 'login'
];
$routesPOST = [
    '/add'    => 'post_todoAdd',
    '/delete' => 'post_todoDelete',
    '/login'  => 'post_login',
    '/logout' => 'post_logout'
];

session_start();
if (isCsrfTokenSet() === false) {
    setNewCsrfToken('todo');
}
respond(getPathInfo(), $routesGET, $routesPOST, 'onError');