TechRAMEN 2025 Conferenceに参加しました

7/26(土)に北海道旭川市で開催された TechRAMEN 2025 Conference に参加してきました。

techramenconf.net

去年の参加レポートはこちら: TechRAMEN 2024 Conferenceに参加しました - 偏った言語信者の垂れ流し

本祭

今年は当日の朝4時に家を出て、バイクで羽田空港に行き、飛行機で旭川空港へ、バスで会場近くまで移動して、最後は少し歩いて会場に到達って感じでした。

基調講演を聞いたあとは興味のあったセッションを聞いてみたり、あとはメロンを食べたりして休憩しつつ、自分のワークショップや懇親会に備えていました。

全部のセッションに参加するのは私は疲れてしまうので、休憩場所が用意されているのはとても良かったと思います。地域のおやつもいっぱい。

特に、富良野のメロンはとても美味しかったです!

懇親会

会場から懇親会会場へバスで移動。スムーズに移動できて楽ちんでした。

懇親会では久しぶりの人たちや、はじめましての人たち、沢山会話できてよかったです。2次会のクラフトビールも美味しかった。

SQLワークショップ

今回、私はSQLワークショップをやりました。参加された方々ありがとうございました。

資料はGitHub Pagesで公開しています。

tokibito.github.io

リポジトリにDocker Composeのファイルもあるので、自分で環境を作って試すことができます。

github.com

ゆるい #techramen25conf 非公式後日祭 - コントリビュートまつり+

2日目は午前中だけですが、去年同様、コントリビュートする会に参加しました。

asahikawa.connpass.com

ドキュメント翻訳を少しやってPullRequestを出しました(その後マージしてもらえました)

github.com

帰り

日曜日の昼すぎに旭川空港について、ラーメンを食べてから飛行機で羽田空港へ。

羽田空港はバイクの駐車場が1日500円なので、使い勝手よくてありがたいです。

羽田空港からバイクで都内を走って帰宅しました。

来年は富良野で開催だそうで、また行きたいと思います。

Claude CodeにRustを書かせてみた雑感

先月の記事 で Claude Code を使い始めたと書いていました。 ClaudeをMaxプランで契約して使って色々書かせてみたり、仕事でも使っているのですが、プログラミング、アプリ開発に対する考え方を変えていく必要があるなと思っているところです。

さて、仕事ではPythonやTypeScriptを書かせることが多いのですが、普段あまり自分がやらないRustを試しに書かせてみることにしました。

私はRustについては、Hello worldを試したことがあるぐらいで、ほとんど使ったことがないです。 ある程度プログラミング言語としての特徴(高速軽量に動作、メモリ安全、クロスプラットフォーム対応)は知っていて、この便利な特性を活用することができるといいなとは思っていたので、いい機会だと思ってやってみました。

ファイル分割・結合を行うCLIツールを作る

私は何度かDelphi、FreePascalでファイル分割ツールを書いたことがあり、要件・仕様を把握しているソフトウェアの題材としてちょうどいいかなと思い、これをClaude Codeを使ってRustで書かせてみることにしました。

成果物はGitHubに置いています。 github.com GitHubのReleasesページには、ビルド済みのバイナリが置かれていて、サイズ指定でファイルを分割できる簡易的なCLIツールをダウンロードできます。

作業の手順

Claude Codeで作業をした手順を簡単にまとめておきます。SNS上を見ていると色々なやり方が議論されていて、まだまだいい方法を模索している状況でもありますが、ひとまず記録として。

  1. 完成品の仕様、使い方を示したREADMEを作成する
  2. CLAUDE.mdを整備する(私はレビューしやすいように日本語にしています)
  3. 実装プランを検討させて、コード設計をレビューし、CLAUDE.mdに反映
  4. コードを書かせる
  5. 動作確認
  6. テストコード、CIの整備をさせる
  7. READMEに使い方などを反映

こんな感じの手順でやってみました。CIの導入を後からやるのだと、Lintやコードフォーマッタを通すのにコードの結構書き換えが発生し、動作確認が二度手間になってしまったりがあったので、環境整備は早い段階でやったほうがいいかもしれないなと思いました。

Rustのツールチェーン整備

RustはCargoを使ってビルド、パッケージ管理ができるので、プロジェクト開始からの環境整備が簡単にできたのもよかったです。 またクロスコンパイルに対応しているため、GitHub Actionsによるビルドで、簡単に各OS(Windows x64、macOSXLinux)向けのリリース用のバイナリを作れるのもよかったです。

また、ClaudeCode任せではあったのですが、Lint、Audit-Checkなども当たり前のように導入してくれたところも良かったです。

出力結果に対する評価

今回は出力されたコードの内容を私が読んで大丈夫そうか評価していたのですが、コードの書き方で不明なところがあれば、ClaudeCodeに質問すれば答えてくれるので、これもやりやすかったです。

まとめ

  • 言語未経験でも実用的なツールが作れる: Hello world程度の知識しかなくても、Claude Codeの支援により実際にリリースできる品質のCLIツールを開発できた
  • Rustのエコシステムとの相性が良い: Cargo、クロスコンパイル、GitHub Actionsなど、Rustを使って開発する際に利用するツールチェーンをClaude Codeが適切に環境構築してくれる
  • コードレビューと学習が両立できる: 生成されたコードを読んで評価し、分からない部分はその場でClaude Codeに質問できるため、実装しながら言語を学べる
  • 作業手順の確立が重要: README作成→CLAUDE.md整備→設計検討→実装→テスト・CI整備という流れでやってみましたが、試行錯誤で時間を使う部分もあったので、スムーズに開発を進めるためには作業手順は重要になりそうです。

全体的にはすごくいい体験だったので、他にも色々作ってみたいと思いました。

2025年はそろそろ半分、近況とか

仕事と子育て、家事が結構忙しく、あまり新しいことに手を出せてない日々が続いております。

何かしらはアウトプットしておきたいなと思い、近況やこれからの予定などを少し書いてみます。

Claude Codeを使いはじめた

時間が取れなくて本当にようやく使い始めたところなのですが、 Claude Code を使い始めました。

GitHub Copilotは普段から使っているのですが、Vibe Codingが流行りみたいで何かしらやっておかないとなと思い、Claude Codeは評判が良さそうだったので、社内のメンバーに使ってもらいつつ、私も試していくことにしました。

雑多で後回しになっているコードを書くタスクなどをこなせたらいいなと思っています。

django-jaの活動

django-jaの活動は引き続きやっています。

5月にオンラインミートアップをやりました。

https://django.connpass.com/event/354005/:embed:code

次は 7/21(月) にDjango 20周年のお祝いパーティ(飲み会?)をオフラインでやりたいね、と話しているところです。

Discordのdjango-ja にて話しているので興味ある方はぜひご参加ください。

カンファレンス参加予定

今のところ参加予定のカンファレンスです。

仕事

とても忙しいです。小さいのも含めると常に4~6プロジェクトを見ている状況。

コードを書くことは最近は少ないですが、生成AIエージェントにコードを書いてもらえるならやれることがもう少し増えるかも。

子育てと家事

子守にどうしても時間を取られてしまうので、余暇の時間がほとんどなくなっています。

仕事はリモートワークなので家で作業することが多いのですが、仕事のオンライン会議が最近増えてきて、静かな環境を用意するのが中々難しいこともあり、家の近所に作業部屋を借りることにしました。徐々に環境を整えていきたいと思います。

畑は作業時間を取れないので、今年の7月末の契約終了に合わせて一旦終了しようと思います。 子育てで余裕が出てきたら、また再開を検討したいと思います。

近況は以上です。がんばろう。

PHPでDjangoの暗号署名機能と互換性のあるライブラリを作りました

先月の記事で、DjangoのセッションデータをPHPから読み書きするのを試していました。

tokibito.hatenablog.com

この際にPHPで実装していた django.core.signing.Signer と互換性のある Signer クラスを調整してライブラリ化、再利用できるようにしました。

Packagistへ登録したパッケージ

nullpobug/django-signing という名前のパッケージで、Packagistに公開しています。 Composerでインストールして利用できます。

packagist.org

リポジトリGitHubにあります:

GitHub - tokibito/php-django-signing: Django compatible signing library for PHP

PHPStanを導入するPullRequestをtadsanからもらったりして、いい感じに仕上がりました。

Packagistへの登録をしたのは初めてですが、GitHubと連携してしまえば特に難しいことはないと感じました。便利ですね。

サンプルコード

前回の記事のコードをコピーして、作成したパッケージのほうで動くように調整してみました。

https://github.com/tokibito/sample_nullpobug/tree/main/django/django-shared-session2

セッションデータのデコード・エンコードのところを差し替えています。

<?php
// 中略
// セッションデータをデコード
$session = Api::loads($session_data, APP_SECRET_KEY, APP_SESSION_SALT);

// セッションデータをエンコード
$session_data = Api::dumps($session, APP_SECRET_KEY, APP_SESSION_SALT, true, true);

https://github.com/tokibito/sample_nullpobug/blob/main/django/django-shared-session2/phpapp/webroot/php/index.php#L52

このデモのコードがもっと短くなるように練習がてら色々作ってみるのも良いかも知れない。

参考

DjangoフレームワークのセッションデータをPHPから読み書きする

実際に必要になったわけではないので、遊びみたいなものなのですが、Djangoのセッションの仕組みを理解しておくと、こういうこともできるよ、という例のために、DjangoのセッションデータをPHPから読み書きするのをやってみました。

Djangoのセッションの概要

最初にDjangoのセッション機能についておさらいです。

Djangoのセッションについてのドキュメントは以下の通り:

セッションの使いかた | Django documentation | Django

Djangoのセッション機能は、複数の異なるHTTPリクエストにおいて、同一のブラウザ(閲覧者)からのリクエストの場合に一連の「セッション」として取り扱って、データを保持する機能を提供します。

HTTPとセッションについてはここでは説明しませんが、よくあるウェブアプリケーションフレームワークのセッション機能と同様です。

request.session が辞書ライクなオブジェクトになっており、これを読み書きできます。同一ブラウザからの異なるリクエストでこのセッションデータは保持されます。

class SpamPageView(View):
    def get(self, request):
        my_counter = request.session.get("my_counter", 0)  # セッションに保存した値の取得
        my_counter += 1
        request.session["my_counter"] = my_counter  # セッションに値を保存
        ...

セッションはユーザー認証などでも利用されています。Djangoの認証機能を使った場合は、ユーザーがログインするとこのセッションにユーザーIDなどが保持されます。

Djangoのセッションはバックエンドの実装を切り替えることで、保存先や各種挙動を変更できます。

デフォルトの動作

カスタマイズしないデフォルト設定の場合のDjangoのセッションは以下のような挙動です。

  • request.session に変更があった場合に、Djangoのセッションミドルウェアでデータが保存され、レスポンスに Set-Cookie ヘッダーでセッションIDが追加される
    • セッションIDはCookieに保存される。Cookieのキーは sessionid
  • セッションデータはデータベース上の django_session テーブルに保存される
    • カラムは session_key session_data expire_date の3つ
  • セッションデータ(request.session の辞書ライクなオブジェクト)は django.core.signing により、シリアライズ+暗号署名が付与される
    • Pythonの辞書をJSONエンコードJSONをzlibで圧縮、base64により文字列化、タイムスタンプをbase62で文字列にして付与、HMAC-sha256にてハッシュ値(base64)を生成、これを : で連結
    • つまり、改ざん検知用のハッシュ値付きで保存されている、ただし暗号化されているわけではない

データベースに保存されたセッションデータの確認

Djangoのプロジェクトを作って、管理者ユーザーを作成し、Django管理画面にログインすると、セッションデータがデータベースに追加されます。

この状態で、Djangoシェルから、セッションデータを確認してみると、以下のような文字列になっています。

>>> session = Session.objects.first()
>>> session.session_data
'.eJxVjEEOwiAQRe_C2pAOLQO4dO8ZyJQZpGpKUtqV8e7apAvd_vfef6lI21ri1mSJE6uzAnX63UZKD5l3wHeab1WnOq_LNOpd0Qdt-lpZnpfD_Tso1Mq3loxsAY1kAIdB2HqhhM5yQEY2wbnOZw8dhcweiIy3QwYm7vswIKr3B-nLN8Y:1uCK0q:WMjyqXdLN94dX2CVYdckucQvJari-41kMairMphjvmI'

先頭1文字目のドット(.)は圧縮フラグを表します。ドットがついていればデータはzlibで圧縮されています。

区切り文字はコロン(:)です。 .[データ]:[タイムスタンプ]:[署名] というフォーマットになります。

Djangoのアプリ内でそのまま読み書きするのであれば、APIの実装があるため、 session.get_decoded() のようにすれば、デコードされた辞書オブジェクトを取得できます。

>>> session.get_decoded()
{'_auth_user_id': '1', '_auth_user_backend': 'django.contrib.auth.backends.ModelBackend', '_auth_user_hash': 'ef6d5162ef11769ed58eac675d96d6d297708f810a9fd81aa2854f1dad339466'}

この処理は django.core.signing モジュールの機能を内部的に使っていますが、一旦改ざん検知は置いといて、Pythonの標準モジュールの機能だけでデコードをしてみます。

>>> import base64
>>> import json
>>> import zlib
>>> json.loads(zlib.decompress(base64.urlsafe_b64decode(session.session_data[1:].split(':', 1)[0] + "=")))
{'_auth_user_id': '1', '_auth_user_backend': 'django.contrib.auth.backends.ModelBackend', '_auth_user_hash': 'ef6d5162ef11769ed58eac675d96d6d297708f810a9fd81aa2854f1dad339466'}

簡単ですね。

base64については、URLセーフに対応する関数を使ってデコードしています。これはエンコード時に django.core.signing がURLセーフに対応するために urlsafe_b64encode を使っているためです。パディングのところはこの例だと雑に = を付与しています。

上記のようにデコードできるので、 Djangoのセッションデータは暗号化はされていない という点に気を付けておく必要があります。

署名による改ざん検知はできますが、暗号化はされていないため、セッションに保存する内容や保存先については気をつける必要があります。

PHP側で読み書きするコード

前置きが長くなりましたが、PHPで先程のセッションデータをデコードするのはさほど難しくはないので、読み取りだけであれば、実装は簡単です。

しかし、PHP側でセッションデータの更新をする場合、Django側は改ざん検知の仕組みがあるため、PHP側でも同じアルゴリズムシグネチャを生成して付与する必要があり、これは結構複雑です。

生成AIに手伝ってもらいながら、 django.core.signing と同等の機能を持つクラスを作ってみました。

signing.php:

<?php

function b62_encode(int $num): string
{
  if (!is_int($num) || $num < 0) {
    throw new InvalidArgumentException("Only non-negative integers allowed");
  }

  $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
  $base = 62;

  if ($num === 0) {
    return '0';
  }

  $result = '';
  while ($num > 0) {
    $result = $chars[$num % $base] . $result;
    $num = intdiv($num, $base);
  }

  return $result;
}

function b62_decode(string $str): int
{
  $chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
  $base = 62;

  $num = 0;
  $len = strlen($str);
  for ($i = 0; $i < $len; $i++) {
    $pos = strpos($chars, $str[$i]);
    if ($pos === false) {
      throw new InvalidArgumentException("Invalid character in input: " . $str[$i]);
    }
    $num = $num * $base + $pos;
  }

  return $num;
}

function b64_encode($data): string
{
  if (!is_string($data)) {
    throw new InvalidArgumentException("Only strings allowed");
  }

  return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

function b64_decode($input)
{
  $remainder = strlen($input) % 4;
  if ($remainder) {
    $padlen = 4 - $remainder;
    $input .= str_repeat('=', $padlen);
  }
  return base64_decode(strtr($input, '-_', '+/'));
}

function salted_hmac($key_salt, $value, $secret_key, $algorithm = 'sha1')
{
  if (!is_string($value)) {
    $value = strval($value);
  }
  $key = hash($algorithm, $key_salt . $secret_key, true);
  $hmac = hash_hmac($algorithm, $value, $key, true);
  return $hmac;
}

class Signer
{
  protected string $sep;
  protected string $salt;
  protected string $secret;
  protected string $algorithm;

  public function __construct(string $secret, string $salt = 'django.core.signing.Signer', string $sep = ':', string $algorithm = 'sha256')
  {
    if (preg_match('/[' . preg_quote($sep, '/') . ']/', $salt)) {
      throw new InvalidArgumentException("Salt cannot contain the separator character");
    }
    $this->secret = $secret;
    $this->salt = $salt;
    $this->sep = $sep;
    $this->algorithm = $algorithm;
  }

  protected function get_signature(string $value): string
  {
    return b64_encode(salted_hmac($this->salt . 'signer', $value, $this->secret, $this->algorithm));
  }

  public function sign(string $value): string
  {
    return $value . $this->sep . $this->get_signature($value);
  }

  public function unsign(string $signed_value): string
  {
    $sep_pos = strrpos($signed_value, $this->sep);
    if ($sep_pos === false) {
      throw new RuntimeException("Bad signature");
    }

    $value = substr($signed_value, 0, $sep_pos);
    $sig = substr($signed_value, $sep_pos + strlen($this->sep));

    $expected_sig = $this->get_signature($value);

    if (!hash_equals($expected_sig, $sig)) {
      throw new RuntimeException("Signature does not match");
    }

    return $value;
  }
}

class TimestampSigner extends Signer
{
  protected string $timestamp_salt = 'django.core.signing.TimestampSigner';

  public function make_timestamp(): string
  {
    return b62_encode(time());
  }

  public function sign(string $value): string
  {
    $timestamp = $this->make_timestamp();
    $value_with_ts = $value . $this->sep . $timestamp;
    return parent::sign($value_with_ts);
  }

  public function unsign(string $signed_value, int|null $max_age = null): string
  {
    $result = parent::unsign($signed_value);
    $parts = explode($this->sep, $result);
    if (count($parts) !== 2) {
      throw new RuntimeException("Bad signature format");
    }

    [$value, $ts_b62] = $parts;

    if ($max_age !== null) {
      $timestamp = b62_decode($ts_b62);
      $age = time() - $timestamp;
      if ($age > $max_age) {
        throw new RuntimeException("Signature has expired");
      }
    }

    return $value;
  }

  public function timestamp(string $signed_value): int
  {
    $parts = explode($this->sep, $signed_value);
    if (count($parts) !== 3) {
      throw new RuntimeException("Bad signature format");
    }
    return b62_decode($parts[1]);
  }
}

function django_signer_dumps($value, string $secret, string $salt, bool $compress = false, bool $add_timestamp = false): string
{
  $json = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);

  if ($compress) {
    $data = zlib_encode($json, ZLIB_ENCODING_DEFLATE);
  } else {
    $data = $json;
  }

  $b64 = b64_encode($data);
  // add a dot to the beginning of the string if compress is true
  if ($compress) {
    $b64 = '.' . $b64;
  }

  if ($add_timestamp) {
    $signer = new TimestampSigner($secret, $salt);
  } else {
    $signer = new Signer($secret, $salt);
  }

  return $signer->sign($b64);
}

function django_signer_loads(string $signed_value, string $secret, string $salt, int|null $max_age = null)
{
  // Use appropriate signer
  if (substr_count($signed_value, ':') === 2) {
    $signer = new TimestampSigner($secret, $salt);
    $b64 = $signer->unsign($signed_value, $max_age);
  } else {
    $signer = new Signer($secret, $salt);
    $b64 = $signer->unsign($signed_value);
  }

  // first character is a dot, indicating compression
  $is_compressed = false;
  if (strlen($b64) > 0 && $b64[0] === '.') {
    $is_compressed = true;
    $b64 = substr($b64, 1);
  }

  $raw = b64_decode($b64);
  if ($is_compressed) {
    $json = zlib_decode($raw);
  } else {
    $json = $raw;
  }

  if ($json === false) {
    throw new RuntimeException("Base64 decoding failed");
  }

  $data = json_decode($json, true);

  if (json_last_error() !== JSON_ERROR_NONE) {
    throw new RuntimeException("JSON decoding failed: " . json_last_error_msg());
  }

  return $data;
}

こんな感じです。PHPの組み込みの関数では足りないURLセーフなbase64エンコード、デコードの機能も実装してあります。

signing.phpの利用

作成したモジュールに含まれる django_signer_loads はセッションデータをデコードする関数で、Djangodjango.core.signing.loads とおおむね同等です。

エンコードdjango_signer_dumps を使います。

<?php
require_once('/signing.php');

// 中略

// セッションデータをデコード
$session = django_signer_loads($session_data, APP_SECRET_KEY, APP_SESSION_SALT);

// 中略

// セッションデータをエンコード
$session_data = django_signer_dumps($session, APP_SECRET_KEY, APP_SESSION_SALT, true, true);

APP_SECRET_KEY の部分は、Django側の settings.pySECRET_KEY と同じ文字列を指定します。

APP_SESSION_SALT の部分は、 "django.contrib.sessions.SessionStore" を常に指定します。

サンプルコードの動作

サンプルコード全体は以下に置いています。

https://github.com/tokibito/sample_nullpobug/tree/main/django/django-shared-session

Djangoはデフォルト設定だとsqlite3のデータベースを使い、セッションもそこに保存されるので、PHPからもPDOでsqlite3を読み書きする形にしました。

  • nginxでリバースプロキシしていて、同一ドメイン名のサブディレクトリでDjangoPHPがそれぞれ動作するような形
  • /php/ 以下はPHPのアプリが動作します。
  • それ以外のパスではDjangoのアプリが動作します。

docker-composeで動かせます。あらかじめDjango側の manage.py createsuperuseradmin という名前のユーザーを作っておいて、ブラウザで http://localhost/ にアクセスします。

1. ログイン画面

Djangoの認証フレームワークでログイン画面を作っています。UIは django-bootstrap5 を使っているので、bootstrapの見た目です。

2. Django側の画面

ログインすると、Django側の画面を表示します。ログイン中のユーザー名と、セッションデータを表示しています。

3. PHP側の画面

PHP側の画面へ」のリンク先はPHPのアプリのページになります。Djangoが保存したセッションデータを同じsqlite3のデータベースファイルから取得し、デコードして表示しています。

また、セッションデータの更新の例として、PHP側のページにアクセスする毎に、 counter というキーでセッション内に数値をカウントアップしています。

4. Django側の画面(PHP側で更新したセッションデータの確認)

  • パス: /django/

PHP側のページにアクセスしたあと、Django側のページを再度表示すると、PHP側で counter キーで保存した値をDjango側でも確認できます。

まとめ

  • Djangoのセッションデータは署名付きでエンコードされて保存される
  • PHPから読み取るのは簡単だが、更新をする場合は署名の生成も必要となる
  • Django側の仕様で保存されたセッションデータをPHPで読み取り、更新することができた

こういうコードを書いてみて、Djangoのセッションの仕組みや、signerについて理解が進みました。

このような実装を実際に使うことは基本的にないと思いますが、仕組みとしては面白いですね。

PowerToysを使おう

開発用のPCにWindowsを使っているのであれば、PowerToysを入れておくことを改めておすすめしたい。

learn.microsoft.com

Windows標準機能だけでは足りない部分や、便利な機能を足すツール。Microsoftが公式にリリースしています。

私がよく使っている機能だけ紹介しておきます。

Hosts ファイル エディター

hostsファイル のエディター。コメントアウトのつけ外しもできるし、らくちん。

カラーピッカー

画面上の色コードを取得するツール。

Always On Top

任意のウィンドウをショートカットキーで最前面表示にできる。

ここに書いたもの以外にも色々機能があるので、Windowsユーザーの人で使ったことなければ、ぜひ試してみてほしい。