GPG: ファイルの暗号化/復号

$ echo "this is message" > message.txt

$ cat message.txt
this is message

相手の公開鍵で暗号化

メッセージの共有相手から事前に公開鍵をもらっておく必要がある。
相手の鍵の中に、暗号用用途 ([E]) の鍵が含まれている必要がある。

pub   ed25519/BF98EE21F2F9F048 2024-04-04 [C]
      A360CEF20643088F3958CDECBF98EE21F2F9F048
uid           [unknown] foo@example.com
sub   ed25519/02B2D9F670FD18C9 2024-04-04 [S]
sub   cv25519/371EEB1712AAD061 2024-04-04 [E] # <- NEED [E] KEY

相手の鍵を指定して、 gpg --encrypt --recipient *KEYID* *FILE* コマンドを実行する。(KEYID は相手のメールアドレスでもよい。)
–armor オプションを併せて指定すると、テキスト形式で出力される。(ファイルサイズはバイナリで出力した時よりも大きくなる)

$ gpg --encrypt --recipient 371EEB1712AAD061 message.txt
$ ls
message.txt   message.txt.gpg
$ cat message.txt.gpg
���`�10�fTv!��~����ւh���uIү�m   &?�W�q��:Z��8G1�OB��p�_,S�k3�M}�NW>7�.KQm\�B|���j꽽{��}
                                                                                       b)��8-z�␦��ֲL}�9�OQ�_^�VH�
$ gpg --armor --encrypt --recipient 371EEB1712AAD061 message.txt
$ ls
message.txt   message.txt.asc
$ cat message.txt.asc
-----BEGIN PGP MESSAGE-----

hF4DNx7rFxKq0GESAQdAODJxrtgR5PtcY8aLP4iGYgL+mQLRZ+zGW59KYH5hCXEw
3J149tyDSW2/MdYW8DvZpLILlcYryp/xuFtbTzW1Wyk71FyhX6IjDGREQkTzG0TW
0k8BEIUOAtjt+c2EccpmzuQ/r75AXZzUlhigPtOSzjdGoL2dLTsPJ2P313YC0ptR
j7ZVL+KKCNQ9BQZ5KOYH0WXjUEJuqHng9oUKsR2uuuAv
=bFxQ
-----END PGP MESSAGE-----

自分の秘密鍵で復号

自分の公開鍵で暗号化されたファイルを受け取ったら、自分の秘密鍵で復号する。処理結果をファイルに残すとオプションと併用して、 gpg --output *OUTFILE* --decrypt *INFILE* コマンドを実行する。

$ ls
message.txt.asc
$ gpg --output message.txt --decrypt message.txt.asc
gpg: encrypted with 255-bit ECDH key, ID 371EEB1712AAD061, created 2024-04-08
      "foo@example.com"
$ ls
message.txt   message.txt.asc
$ cat message.txt
this is message

GPG: Git コミットサイン環境整備

署名鍵の作成

Git コミット署名用の鍵を作成する。

鍵を作成するにあたって、すでに親となる鍵が作成されているものとする。(まだ作成してない場合は、鍵の作成とエクスポート/インポートの 「鍵署名用の鍵を作る」を見ながら主鍵を先に作っておく)
既に署名用の副鍵を作っている場合や、主鍵にデータ署名機能 ([SC]) を持たせている場合は、この手順を飛ばすことができる。

# コンピュータにインストールされている秘密鍵を確認
$ gpg -K --keyid-format LONG
/home/user/.gnupg/pubring.kbx
-------------------------------
sec   ed25519/BF98EE21F2F9F048 2024-04-04 [C]
      A360CEF20643088F3958CDECBF98EE21F2F9F048
uid                 [ultimate] foo@example.com

gpg --edit-key *KEYID* コマンドを用い、副鍵を追加する。(最後に保存するのを忘れずに)

$ gpg --expert --edit-key BF98EE21F2F9F048
Secret key is available.

sec  ed25519/BF98EE21F2F9F048
     created: 2024-04-04  expires: never       usage: C
     trust: ultimate      validity: ultimate
[ultimate] (1). foo@example.com

gpg> addkey
Please select what kind of key you want:
   (3) DSA (sign only)
   (4) RSA (sign only)
   (5) Elgamal (encrypt only)
   (6) RSA (encrypt only)
   (7) DSA (set your own capabilities)
   (8) RSA (set your own capabilities)
  (10) ECC (sign only)                  # <- Select
  (11) ECC (set your own capabilities)
  (12) ECC (encrypt only)
  (13) Existing key
  (14) Existing key from card
Your selection? 10
Please select which elliptic curve you want:
   (1) Curve 25519                      # <- Select
   (3) NIST P-256
   (4) NIST P-384
   (5) NIST P-521
   (6) Brainpool P-256
   (7) Brainpool P-384
   (8) Brainpool P-512
   (9) secp256k1
Your selection? 1
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y
Really create? (y/N) y
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.

sec  ed25519/BF98EE21F2F9F048
     created: 2024-04-04  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  ed25519/02B2D9F670FD18C9
     created: 2024-04-04  expires: never       usage: S
[ultimate] (1). foo@example.com

gpg> save

Git のコミット時に署名する設定を行う

$ git config --global user.signingkey BF98EE21F2F9F048
$ git config --global commit.gpgsign true

下記の設定が追加されていれば OK。

$ cat ~/.gitconfig
[user]
        signingkey = 02B2D9F670FD18C9
[commit]
        gpgsign = true

テストを兼ねて、適当にリポジトリを作成し1つコミットを作成する。
コミット作成時にパスフレーズを尋ねられるようになるため、鍵作成時に設定したパスフレーズを入力する。問題なくコミットできれば完了。

             ┌───────────────────────────────────────────────────────────────┐
              Please enter the passphrase to unlock the OpenPGP secret key: 
              "foo@example.com"                                             
              255-bit EDDSA key, ID 02B2D9F670FD18C9,                       
              created 2024-04-04 (main key ID BF98EE21F2F9F048).            │
                                                                            
                                                                            
              Passphrase: _________________________________________________ 
                                                                            
                      <OK>                                   <Cancel>       
             └───────────────────────────────────────────────────────────────┘

公開鍵のエクスポート

コミットを共有する相手が署名を検証できるようにするには、署名した秘密鍵に対応する公開鍵を相手に渡しておく必要がある。
公開鍵をエクスポートする を参考にして公開鍵を出力し、鍵を相手に渡したり Github などにアップロードしたりできる。

トラブルシュート

gpg failed to sign the data が発生する

https://gist.github.com/paolocarrasco/18ca8fe6e63490ae1be23e84a7039374 に調査方法が記載されていたので、それを試す。

まず、GIT_TRACE=1 を設定して Git が何をしようとしているかを確認する。

$ GIT_TRACE=1 git commit -m "initial commit" --allow-empty
08:00:22.114351 git.c:455               trace: built-in: git commit -m 'initial commit' --allow-empty
08:00:22.116297 run-command.c:668       trace: run_command: gpg --status-fd=2 -bsau 02B2D9F670FD18C9
error: gpg failed to sign the data
fatal: failed to write commit object

自分の環境の場合、パスフレーズを求めるダイアログが表示されず失敗していたので、 export GPG_TTY=$(tty) を設定してから再度 git commit をしたら正常にコミットできた。

$ export GPG_TTY=$(tty)
$ git commit -m "initial commit" --allow-empty
[master 43b460f] initial commit

export GPG_TTY=$(tty) を .bashrc に書いておくのがセオリーらしい。

他にコミットが失敗することがある状況としては、指定された Key ID が見つからない、指定された鍵に [S] フラグが付いていない、などがある。

GPG: 鍵ファイルの情報を調べる

gpg コマンドで鍵をインポートせずに鍵の情報を調べる方法。

gpg に標準入力経由で鍵ファイルを渡し、 gpg --import --import-option show-only コマンドで鍵を登録せずに内容を覗き見ることができる。

$ cat any.key | gpg --import --import-option show-only
pub   rsa2048 2024-04-04 [SC]
      04463FC0263842076536957C7D542294072DFE79
uid                      John smith <john@example.com>
sub   rsa2048 2024-04-04 [E]

GPG: 鍵の作成とエクスポート/インポート

鍵署名用の鍵を作る

gpg --full-generate-key --expert コマンドを用いて鍵署名用の主鍵を作成する。ここで作る鍵は副鍵を作るための用途のみであり、単体で利用することはできない。

鍵のアルゴリズムは任意だが、鍵署名専用の鍵を作るためには set your own capabilities のオプションが付いた種類を選んで allowed actions: Certify にする。(Certify 以外を外す)

$ gpg --full-generate-key --expert
gpg (GnuPG) 2.2.27; Copyright (C) 2021 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
   (7) DSA (set your own capabilities)
   (8) RSA (set your own capabilities)
   (9) ECC and ECC
  (10) ECC (sign only)
  (11) ECC (set your own capabilities)   # <- Select this in example
  (13) Existing key
  (14) Existing key from card
Your selection? 11

Possible actions for a ECDSA/EdDSA key: Sign Certify Authenticate
Current allowed actions: Sign Certify

   (S) Toggle the sign capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? S                        # <- Remove sign

Possible actions for a ECDSA/EdDSA key: Sign Certify Authenticate
Current allowed actions: Certify

   (S) Toggle the sign capability
   (A) Toggle the authenticate capability
   (Q) Finished

Your selection? Q
Please select which elliptic curve you want:
   (1) Curve 25519                       # <-
   (3) NIST P-256
   (4) NIST P-384
   (5) NIST P-521
   (6) Brainpool P-256
   (7) Brainpool P-384
   (8) Brainpool P-512
   (9) secp256k1
Your selection? 1
Please specify how long the key should be valid.
         0 = key does not expire         # <-
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name:
Email address: foo@example.net
Comment:
You selected this USER-ID:
    "foo@example.net"
    
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: key 1111111111111111 marked as ultimately trusted
gpg: revocation certificate stored as '/home/user/.gnupg/openpgp-revocs.d/FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.rev'
public and secret key created and signed.

pub   ed25519 2024-04-01 [C]
      FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
uid                      foo@example.com

$ gpg --keyid-format LONG -K
/home/user/.gnupg/pubring.kbx
-------------------------------
sec   ed25519/1111111111111111 2024-04-01 [C]
      FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
uid                 [ultimate] foo@example.com

$ gpg --keyid-format LONG -k
/home/user/.gnupg/pubring.kbx
-------------------------------
pub   ed25519/1111111111111111 2024-04-01 [C]
      FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
uid                 [ultimate] foo@example.com

副鍵を追加する

上記で作成した鍵は単体で利用することはできないものなので、副鍵を追加する。副鍵は用途ごとに複数追加できる。

鍵の使用法 (usage) について

鍵には使用法を設定する。主鍵作成時には Certify を選択したが、それ以外に Sign, Authenticate, Encrypt の使用法が存在する。鍵に設定されている使用法は gpg -k コマンドなどを使って表示される [C], [S], [A], [E] などの表記で確認することができる。

実際に鍵を追加する

副鍵を追加するには、 gpg --expert --edit-key *KEY-ID* コマンドで操作する鍵を選択してから addkey を指示する。副鍵を作成した後には save の指示をすること (save しないと反映されない)。

データ署名用の副鍵を追加する例:

$ gpg --expert --edit-key 1111111111111111 
gpg (GnuPG) 2.2.27; Copyright (C) 2021 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Secret key is available.

sec  ed25519/1111111111111111 
     created: 2024-04-01  expires: never       usage: C
     trust: ultimate      validity: ultimate
[ultimate] (1). foo@example.com

gpg> addkey
Please select what kind of key you want:
   (3) DSA (sign only)
   (4) RSA (sign only)
   (5) Elgamal (encrypt only)
   (6) RSA (encrypt only)
   (7) DSA (set your own capabilities)
   (8) RSA (set your own capabilities)
  (10) ECC (sign only)                   # <- Select this in example
  (11) ECC (set your own capabilities)
  (12) ECC (encrypt only)
  (13) Existing key
  (14) Existing key from card
Your selection? 10
Please select which elliptic curve you want:
   (1) Curve 25519
   (3) NIST P-256
   (4) NIST P-384
   (5) NIST P-521
   (6) Brainpool P-256
   (7) Brainpool P-384
   (8) Brainpool P-512
   (9) secp256k1
Your selection? 1
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y
Really create? (y/N) y
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.

sec  ed25519/1111111111111111 
     created: 2024-04-01  expires: never       usage: C
     trust: ultimate      validity: ultimate
ssb  ed25519/2222222222222222
     created: 2024-04-01  expires: never       usage: S
[ultimate] (1). foo@example.com

gpg> save

主鍵をエクスポートし、端末上から削除する

主鍵は別の鍵を発行する能力を持つので、十分に安全で漏洩が起こりえない場所に保管するべきである。コンピュータの中に保管するのは避け、 (USBメモリなどの) リムーバブルメディアに保存し、物理的なトラブル (盗難、破損など) が起きない場所に保管することを推奨する。

端末に保存されたキーは ~/.gnupg/private-keys-v1.d/ に保存されている。 gpg --with-keygrip -k コマンドを使うことで、どの鍵がどのファイルに保存されているか知ることができる。

$ ls ~/.gnupg/private-keys-v1.d/
4658BD2EB68C52A3A5FAA5A306251E2A6869FB3F.key  E590B6845A28CD09D055F1AAA06D478EF08732FF.key
7FFFCE2F16118B000C9D268402A7CB2FFA6DF53D.key

$ gpg --with-keygrip -k
/home/user/.gnupg/pubring.kbx
-------------------------------
pub   ed25519 2024-04-01 [C]
      FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
      Keygrip = E590B6845A28CD09D055F1AAA06D478EF08732FF
uid           [ultimate] foo@example.com
sub   ed25519 2024-04-01 [S]
      Keygrip = 4658BD2EB68C52A3A5FAA5A306251E2A6869FB3F
sub   ed25519 2024-04-01 [S]
      Keygrip = 7FFFCE2F16118B000C9D268402A7CB2FFA6DF53D

ファイルを直接削除しても目的は達成されるが、コマンドを使って副鍵のエクスポートを併せて行っておくと、後で使いまわせる (別端末で副鍵をインポートするときに使う)

以下の手順を踏む。なお、--armor --output *FILENAME* オプションを指定するとやや取り扱いやすくなる。

  1. gpg --export-secret-keys *KEY_ID* コマンドで、対象の鍵をすべて (主副) エクスポートする
  2. gpg --export-secret-subkeys *KEY_ID* コマンドで、対象の鍵の副鍵をエクスポートする
  3. gpg --delete-secret-key *KEY_ID* コマンドで、対象の秘密鍵 (すべて) を削除する
  4. gpg --import *KEYFILE* コマンドで、副鍵をインポートする
  5. gpg --edit-key *KEY_ID* コマンドで、インポートした副鍵を信頼する
  6. 出力した鍵ファイルのうち主鍵が含まれているものは安全な場所に移動させ、端末から消去する
$ gpg --armor --output all.keys --export-secret-keys      #(1)
$ gpg --armor --output sub.keys --export-secret-subkeys   #(2)
$ gpg --delete-secret-key 1111111111111111                #(3)
sec  ed25519/1111111111111111 2024-04-01 foo@example.com

Delete this key from the keyring? (y/N) y
This is a secret key! - really delete? (y/N) y

$ gpg --import sub.keys                                   #(4)
gpg: key 1111111111111111: public key "foo@example.com" imported
gpg: To migrate 'secring.gpg', with each smartcard, run: gpg --card-status
gpg: key 1111111111111111: secret key imported
gpg: Total number processed: 1
gpg:               imported: 1
gpg:       secret keys read: 1
gpg:   secret keys imported: 1

$ gpg --keyid-format LONG -K
/home/user/.gnupg/pubring.kbx
-------------------------------
sec#  ed25519/1111111111111111 2024-04-01 [C]
      FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
uid                 [ unknown] foo@example.com # <-- status: unknown
ssb   ed25519/2222222222222222 2024-04-01 [S]
ssb   ed25519/3333333333333333 2024-04-01 [S]

$ gpg --edit-key 1111111111111111                         #(5)
gpg> trust
Please decide how far you trust this user to correctly verify other users keys
(by looking at passports, checking fingerprints from different sources, etc.)

  1 = I don't know or won't say
  2 = I do NOT trust
  3 = I trust marginally
  4 = I trust fully
  5 = I trust ultimately
  m = back to the main menu

Your decision? 5                               # <- set ultimate
Do you really want to set this key to ultimate trust? (y/N) y
pub  ed25519/1111111111111111
     created: 2024-04-01  expires: never       usage: C
     trust: ultimate      validity: unknown
ssb  ed25519/2222222222222222 
     created: 2024-04-01  expires: never       usage: S
ssb  ed25519/3333333333333333 
     created: 2024-04-01  expires: never       usage: S
[ unknown] (1). foo@example.com

$ gpg --keyid-format LONG -K
/home/user/.gnupg/pubring.kbx
-------------------------------
sec#  ed25519/11111111111111112024-04-01 [C]
      FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
uid                 [ultimate] foo@example.com # <- status: ultimate
ssb   ed25519/2222222222222222 2024-04-01 [S]
ssb   ed25519/3333333333333333 2024-04-01 [S]

別端末に副鍵の秘密鍵をインポートする

複数の端末で同じ鍵を使いたい場合、鍵が入っている端末から副鍵をエクスポートし、別の端末でインポートする必要がある。

前のセクション「主鍵をエクスポートし、端末上から削除する」の手順 2 を実施して副鍵を取り出したあと、別の端末で手順 4 を実施する。

公開鍵をエクスポートする

自分の署名を相手に検証してもらう / 自分宛のメッセージやデータを相手に暗号化してもらう、などの目的を達成するには、やり取りする相手の手元に自分の公開鍵が存在する必要がある。

$ gpg --armor --output foo.key --export 1111111111111111

公開鍵をインポートする

相手から鍵を受け取ったら、それを自分の環境にインポートすることで利用できるようになる。

$ gpg --import bar.key

git-bash mingw: ターミナルのカスタマイズ

Windows 環境において Linux コマンドを用いて作業したいときなどには基本的に WSL2 を使うことが多いのだが、WSL2 は仮想マシンなのでネットワーク周りが不便なこともあり、それ単体だけではどうしても都合が悪いこともあったりするので、開発作業時などでは Git-bash に付属している mingw を併用して凌いだりしている。

個人的には、 Git-bash に付属する mingw を Windows Terminal (wt.exe) 内で起動して、見た目を Ubuntu に寄せるような設定を適用して利用している。

~/.bashrc

言語設定や不便な点の解消をするための設定の寄せ集めの記述がメイン。

##### language settings
export LC_ALL=ja_JP.utf8
export LANG=ja_JP.utf8
export LANGUAGE=ja_JP.utf8
export LC_CTYPE="ja_JP.utf8"
export LC_NUMERIC="ja_JP.utf8"
export LC_TIME="ja_JP.utf8"
export LC_COLLATE="ja_JP.utf8"
export LC_MONETARY="ja_JP.utf8"
export LC_MESSAGES="ja_JP.utf8"

##### ls color
eval "$(dircolors /etc/DIR_COLORS -b)"
alias ls='ls --color=auto'

##### unblock Control + S
stty stop undef

~/.config/git/git-prompt.sh

プロンプトのカスタマイズ。
Ubuntu と完全に同一にしてしまうと、自分がどのシェルソフトウェアを使っているかわからなくなるので、Windows っぽい目印 (❖) を含めている (仮想マシンじゃなくて互換レイヤを使っているよってことで)。

##### PROMPT SETTINGS
PS1='\[\033]0;$PWD\007\]' # window title
PS1="$PS1"'\[\033[01;32m\]\u@❖ \h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '

~/.inputrc

プロンプトに何も入力してない状態で Backspace を押したときなどでベルが鳴る操作をすると、ターミナルウィンドウが明滅してしまう。その抑制設定。

# suppress flashing window for wt.exe
set bell-style audible

Filemaker: プラグインのコンパイル

Claris Filemaker に組み込んで利用するためのプラグインの作成について。
プラグイン環境構築からコンパイルまで。

開発環境および SDK について

SDK の入手

Claris 社からサンプルプロジェクトが配布されている。
https://www.claris.com/ja/resources/downloads/ の画面下部スポイラー「プラグインのサポート」の中に、SDK のダウンロードリンクが張ってある。SDK に変更が加わるごとにダウンロードできるファイルも更新されるようなので、最新版が出てないか時々ダウンロードするとよい。

コンパイラについて

SDK に含まれているサンプルプロジェクトには、 Visual Studio 向けのプロジェクトファイル (.sln) と Code::Blocks 向けのプロジェクトファイル (.cbp) が同梱されている。

開発には C++ を利用するため、上記の IDE および C++ 向けのコンパイラを用意しておく必要がある。
私の手元の開発環境には Visual Studio がインストールされているので、以降の説明は Visual Studio 向け。

SDK 構成について

Headers/FMWrapper

Filemaker の内部で利用されている型や API や定数値をまとめたヘッダファイル群。ここでは細かく解説しない (機会があれば別のところで) が、特に大事なファイルは FMXExtern.h である。(プラグインのエントリポイント関数のシグネチャについて記述されている)

Libraries

各環境 (OS) 向けのライブラリが存在する。中身までは詳しく見ていないが、名前からして Headers/FMWrapper のヘッダに対応した本体のバイナリであると思われる。

MiniExample

プラグインのサンプルプロジェクト。

コンパイルについて

MSBuild から

FMMiniPlugin.sln が存在するディレクトリ内で、 MSBuild.exe コマンドを発行する。手元で試したところ、 x64/Debug/FMMiniPlugin.fmx64 ファイルが出力された。これがプラグインファイルである。

MSBuild コマンドに対してオプションを指定することによって、 Debug/Release や x86/x64 向けなどのビルド構成を指示できる。

> MSBuild.exe
MSBuild のバージョン 17.9.5+33de0b227 (.NET Framework)
2024/03/18 5:41:56 にビルドを開始しました。

ノード 1 上のプロジェクト "C:\xxxxx\PlugInSDK\MiniExample\FMMiniPlugIn.sln" ( 既定のターゲット)。"
  ソリューション構成 "Debug|x64" をビルドしています。
PrepareForBuild:
  構造化出力が有効になっています。コンパイラ診断の書式設定にはエラー階層が反映されます。詳細については、https://aka.ms/cpp/structured-output を参照してください。
InitializeBuildStatus:
  "AlwaysCreate" が指定されたため "x64\Debug\FMMiniPlugIn.tlog\unsuccessfulbuild" を作成しています。
  "x64\Debug\FMMiniPlugIn.tlog\unsuccessfulbuild" のタッチ タスクを実行しています。
ClCompile:
  すべての出力が最新のものです。
ResourceCompile:
  すべての出力が最新のものです。
Link:
  すべての出力が最新のものです。
  FMMiniPlugIn.vcxproj -> C:\xxxxx\PlugInSDK\MiniExample\x64\Debug\FMMiniPlug
  In.fmx64
FinalizeBuildStatus:
  ファイル "x64\Debug\FMMiniPlugIn.tlog\unsuccessfulbuild" を削除しています。
  "x64\Debug\FMMiniPlugIn.tlog\FMMiniPlugIn.lastbuildstate" のタッチ タスクを実行しています。
プロジェクト "C:\xxxxx\PlugInSDK\MiniExample\FMMiniPlugIn\FMMiniPlugIn.vcxproj" (既定
のターゲット) のビルドが完了しました。

プロジェクト "C:\xxxxx\PlugInSDK\MiniExample\FMMiniPlugIn.sln" (既定のターゲット) のビルドが完了しま
した。

ビルドに成功しました。
    0 個の警告
    0 エラー

経過時間 00:00:00.26

Visual Studio (IDE) から

FMMiniPlugin.sln ファイルを Visual Studio から開く。Ctrl + Shift + B キー押下でビルドを実行すると、 x64/Debug/FMMiniPlugin.fmx64 ファイルが出力される。

まとめ

本記事では、提供元からサンプルプロジェクトをダウンロードしてきて、手元でコンパイルを実行するまでの手順を並べた。

実際は、ビルド時に読み込まれるメタデータ (プラグイン発行元など) を編集したり、独自の機能を提供するために C++ コードを記述したりする必要がある。実際のカスタマイズについては、別途解説記事を作成する予定。

C# から Lua スクリプトを実行する

NLua パッケージを利用することによって、 C# のプログラム中から Lua スクリプトを呼び出すことができる。

NLua パッケージの追加

nuget に NLua パッケージが存在するので、プロジェクトに追加する。
https://www.nuget.org/packages/NLua

$ dotnet add package NLua

最小のサンプル

// Program.cs
using NLua;

using var lua = new Lua();
lua.DoString("print('Hello,world')");   

動的にスクリプトを生成したり、テンプレートエンジンの生成結果を利用したりするなら、 DoString メソッドで手軽に動作させることができる。
外部ファイルから読み込んで実行したいときは、自前でファイル読み込みをせずに、次の方法を使うとよい。

ファイルから読み込んで実行

-- scrpit.lua
print('Hello,world')
// Program.cs
using NLua;

using Lua lua = new Lua();
lua.DoFile("script.lua");

外部の Lua ファイルを読み込む場合、C# プログラムのコンパイル後であっても Lua スクリプトを変更するだけでプログラムをカスタマイズすることができる。

C# 側から Lua の関数を呼び出す

-- script.lua
function add_and_sub(a, b)
    print("add_and_sub called")
    return a + b, a - b
end
// Program.cs
using NLua;

using Lua lua = new();
lua.DoFile("script.lua");

LuaFunction? add_and_sub = lua["add_and_sub"] as LuaFunction;
if (foo != null)
{
    object[] result = add_and_sub.Call(1, 2);
    Console.WriteLine("foo result add: " + result[0]);
    Console.WriteLine("foo result sub: " + result[1]);
}

Lua オブジェクトはインデクサを使って大域オブジェクトにアクセスできるので、ラッパー型である LuaFunction の Call メソッドを使って呼び出すことができる。

引数および戻り値はともに object[] である。
(Call に渡す実引数について、足りない値は nil で埋められ、余分な値は無視される。)
(Call の戻り値は配列である。Lua の関数は複数の値を返せるので、戻り値を利用するときは何番目を使うか指定する。戻り値が 1 つしかない場合でも同様。)

.NET オブジェクトの共有

.NET のオブジェクトインスタンスを Lua スクリプトと共有することができる模様。

public class SampleClass
{
    public int x;
    public int y;
}

class App
{
    static void Main(string[] args)
    {
        using Lua lua = new();
        
        SampleClass sample = new();
        sample.x = 10;
        sample.y = 20;
        lua["sample"] = sample;
        
        Console.WriteLine("before: x = " + sample.x + " y = " + sample.y);
        
        lua.DoString("""
            function overwrite()
                sample.x = 99
                sample.y = 99
            end
        """);

        (lua["overwrite"] as LuaFunction)!.Call();
        
        Console.WriteLine("after : x = " + sample.x + " y = " + sample.y);
    }
}
before: x = 10 y = 20
after : x = 99 y = 99

なお、共有したオブジェクトを Lua のテーブルで書き換えてしまうと、C# 側の元の参照からはそれが見えないので注意が必要。

using Lua lua = new();

SampleClass sample = new();
sample.x = 10;
sample.y = 20;
lua["sample"] = sample;

var before = lua.GetObjectFromPath("sample");
Console.WriteLine("type : " + before.GetType().Namespace + "." + before.GetType().Name);
Console.WriteLine("C#  before: x = " + sample.x + " y = " + sample.y);

lua.DoString("""
    function overwrite()
        print("Lua before: x = " .. sample.x .. " y = " .. sample.y)
        sample = {
            x = 99,
            y = 99
        }
        print("Lua after : x = " .. sample.x .. " y = " .. sample.y)
    end
""");
(lua["overwrite"] as LuaFunction)!.Call();

Console.WriteLine("C#  after : x = " + sample.x + " y = " + sample.y);

var after = lua.GetObjectFromPath("sample");
Console.WriteLine("type : " + after.GetType().Namespace + "." + after.GetType().Name);
type : LuaApp.SampleClass
C#  before: x = 10 y = 20
Lua before: x = 10 y = 20
Lua after : x = 99 y = 99
C#  after : x = 10 y = 20
type : NLua.LuaTable

Lua 側から C# の関数を呼び出す

インスタンスメソッド

Lua らしい記法を利用して C# 側のメソッドを透過的に呼び出すことができる。

public class SampleClass
{
    public int x;
    public int y;
    
    public void add(int x, int y)
    {
        this.x += x;
        this.y += y;
    }
}

class App
{
    static void Main(string[] args)
    {
        using Lua lua = new();
        
        SampleClass sample = new();
        sample.x = 10;
        sample.y = 20;
        
        lua["sample"] = sample;
        lua.DoString("""
            function call_add()
                sample:add(5, 5)
            end
        """);
        
        (lua["call_add"] as LuaFunction)!.Call();
        System.Console.WriteLine("x: " + sample.x + " y: " + sample.y);
    }
}
x: 15 y: 25

クラスメソッド

static メソッドをインスタンス経由で呼び出すことはできないので、 CLR (共通言語ランタイム) のパッケージとして対象のクラスをロードしてから呼び出す。
Lua 内の import(assembly_name, namespace_name) は、 LoadCLRPackage() メソッドを呼び出すことによって利用可能になる関数。

public class SampleClass
{
    public static void Greet(string name, int age) {
        string greet = "Hello " + name + ", " + age + " years old";
        Console.WriteLine(greet);
    }
}

class App
{
    static void Main(string[] args)
    {
        using Lua lua = new();
        lua.LoadCLRPackage();
        
        lua.DoString("""
            import('nlua_example', 'LuaApp')
            function greet()
                SampleClass.Greet('Lua', 20)
            end
        """);
        
        (lua["greet"] as LuaFunction)!.Call();
    }
}

関数

C# 側の処理を関数として Lua 側に公開したいなら、関数自体をそのまま共有することによって実現できる。

static void Main(string[] args)
{
    using Lua lua = new();
    
    lua["greet"] = (Func<string, int, string>)Greet;
    
    lua.DoString("""
        greet('Lua', 10);
    """);   
}

static string Greet(string name, int age)
{
    string greet = "Hello " + name + ", " + age + " years old";
    Console.WriteLine(greet);
    return greet;
}

Jupyter Notebook を使ってさっくりグラフ描画を試してみる

防備を兼ねて。

ちょっとした野暮用があって、直交座標系のグラフ上に線をプロットして絵作りする必要が出てきてしまったので、 Jupyter Noteboot を使って試してみることにした。

実行環境について

Jupyter Notebook を試すことのできる環境の選択肢は様々あるので、ユースケースに合ったものを使うとよい。以下は今回試したもの。

  • Google Colab
    Google のアカウントがあってオンラインで作業できるならこれ。 https://colab.research.google.com/ にアクセスするだけですぐに始められるし、環境構築する必要もない。
    特に難しいこともないのでここでは割愛。
  • Web App (Local)
    pip を使って自分の手元に Notebook 環境を作り、 Web ブラウザでアクセスして利用する方法。コンピューターに python が入っていればとりあえず使える。
  • VSCode (Local)
    pip を使って自分の手元に Notebook 環境を作り、 VSCode の拡張機能を使って利用する方法。VSCode の機能が使える。

Web App

適当なディレクトリで以下のコマンドを実行して、 Jupyter Notebook 環境をインストールする。

$ python3 -m venv venv
$ ./venv/bin/activate
(venv)$ pip3 install jupyter matplotlib
# (中略)

起動する。

(venv)$ jupyter notebook

コンソールにアクセス用の URL が表示されるので、 Web ブラウザを使ってアクセスする。

VSCode

VSCode の Jupyter 拡張機能をインストールする。
https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter

上記 Web App セクションのインストールコマンド (pip3 install jupyter matplotlib まで) を実行する。

venv を作成したディレクトリを VSCode のワークスペースとして開き (code .)、 任意のノートブックファイル (.ipynb) を作成する。
VSCode の Explorer ペインから ipynb ファイルを選択すると、通常のエディタとは異なる、ノートブック編集用エディタが開く。

F1 キーを押下し、 Notebook: Select Notebook Kernel コマンドを呼び出す。利用するカーネルとして、 ./venv/bin/python を選択する。

所感

ざっと試したところ、VSCode であれば Python 拡張機能の支援 (Type suggestionなど) を受けながら作業ができるため、非常に快適だった。

import matplotlib.pyplot as plt
import math

circle_points = [(math.cos(math.radians(x)),math.sin(math.radians(x))) for x in range(0, 360, 12)]
for p in circle_points:
    plt.plot(p[0], p[1], 'xb')

star_points = [(math.cos(math.radians(x)),math.sin(math.radians(x))) for x in range(0 + 90, 720, 144)]
star_after_points = [(math.cos(math.radians(x)),math.sin(math.radians(x))) for x in range(144 + 90, 720 + 144, 144)]
for i in range(5):
    plt.plot([star_points[i][0], star_after_points[i][0]], [star_points[i][1], star_after_points[i][1]], 'r-')


plt.axis('equal')
plt.draw()

DCS World: Mod の構成

DCS の Mod (GameGUI) の構成について。備忘録。

ディレクトリ構成

基本となる構成:

saved games/
 |- dcs/
     |- Mods/
     |   |- tech/
     |       |- {MOD_NAME}/
     |           |- Options/
     |           |   |- (files)
     |           |- Theme/
     |           |   |- (files)
     |           |- entry.lua
     |- Scripts/
         |- Hooks
             |- {MOD_NAME}.lua

各ファイルについて

entry.lua

entry.lua は Mod のサマリファイルである。
デフォルトでは Mods/Aircraft/ もしくは Mods/tech/ の下がスキャン対象 (再帰的) になるらしい。Mod 用のディレクトリを作ってその中に配置するのが妥当かと思われる。
(DCS-SRS は Mods/Services の下に Mod 本体を配置しているので、例外がありそう)

declere_plugin(mod_id, information)および plugin_done()を記述することによって、Mod のメタデータを宣言することができる。これらの情報は DCS 起動時に読み込まれ、メインメニューにアイコンを表示したり settings 画面の項目を追加したりするのに使われる。 (詳細については後述)

Scripts/Hooks/***.lua

ゲーム実行中に発生した Hook/Callback の対象となる処理を記述するファイルである。

ここに置くファイルにフック処理を直接書いてもよいが、いくつかの Mod を見るとどうやら entry.lua の近くに配置した別ファイルをロードすることが多いようである。 (特にルールはない)
参考として、Tacview の場合は Mods/tech/tacview/bin/tacview.dll を呼び出していて、DCS-SRS の場合は Mods/Services/DCS-SRS/Script/DCS-SRSGameGUI.lua を呼び出している。

Theme ディレクトリ

Mod のアイコン画像ファイルを配置するディレクトリ。entry.lua 内でディレクトリ名を指定する。(ディレクトリ名は Theme でなくてもよい)

Options ディレクトリ

Mod の設定画面 (GUI) および設定項目を定義したファイルを配置するディレクトリ。 entry.lua 内でディレクトリ名を指定する。(ディレクトリ名は Options でなくてもよい)

利用できる API とコールバック

DCS のインストールディレクトリ下 (DCSWorld/API/DCS_ControlAPI.html) に、利用可能な API やコールバックを列挙したマニュアルが配置されている。

マニュアルの付近にヘッダファイル (.h) やテンプレートプロジェクト (.sln) などが存在するので、それらを利用することもできる。 (当方未確認)

DCS World: Scripting の概要

(注: 個人用備忘録)
バージョン: DCS World 2.9.1

Scripting の種類

hoggitworld の情報を流し見する限りだと、大きく分けて 2種類の分類があり、 Mod にはさらに 3つに分けられる模様。

  • Mission
  • Mod
    • Export
    • Server control
    • Liveries

Scripting の種類の概要

Mission

Lua スクリプトをミッションファイル (.miz) に組み込む形で動作する。

ゲーム内オブジェクトのコントロールができるのは Mission Scripting のみである。僚機や AI 機や地上ユニットなどの動作を制御したい場合は、 Mission Scripting の形で実装する必要がある。
無線コマンドなどを追加できるのも Mission Scripting のみ。

Mod – Export

Export.lua ファイルから直接呼び出される形で動作する。ベースは Lua だが、DLL を呼び出す形で別のプログラムを実行することができる。基本的には ~\Saved Games\dcs\Scripts\Export.lua に直接処理を記述するか、そこから呼び出されるような形で処理を記述する。

Mission Scripting とは少々異なる方法でゲーム内の情報を取得できるほか、 (おそらく十分なドキュメント化されていないが) プレイヤーが操作している機体の状態を取得/変更することもできる。独自の計器やコントローラーをハードウェアとして作成した場合などに、Export を通じて DCS と連動させることができる。

LoGetWorldObjects などの Object 系 API に関しては、自機の状態ではなく全てのゲーム内オブジェクトの状態 (位置/速度など) にアクセスできる。マルチプレイヤーの場合、 (おそらく不正行為を防ぐため) サーバー側で一部の API へのアクセスを制限するように設定することができる。

Mod – GameGUI

ゲームサーバーもしくはクライアントにインストールする形で動作する。マルチプレイヤーのサーバーとしても利用できる処理群。作るのは少々手間だが、 Export の上位互換。 ~\Saved Games\dcs\Mods\tech の下に Mod 用のフォルダを作り、その中に処理群を作成する。

メニュー画面内にアイコンを出したり、設定画面に Mod 用の項目を追加したりできる。ゲーム内の情報にアクセスする方法は、 Export と同じものが使える。

Liveries

Livery は機体 Mod のことだが、特に調べてないので詳細不明。 ~\Saved Games\dcs\Mods\Aircraft の下に機体用のフォルダを作り、必要なファイルを設置する。