みつきんのメモ

組み込みエンジニアです。Interface誌で「My オリジナルLinuxの作り方」連載中

Zen言語の構造体入門

はじめに

Zen入門中の自分用の備忘録。

詳細はここにある。

使用するZenコンパイラのバージョンは0.8.20191124+552247019(2020/03/12時点のパブリックベータ版)。

基本

src/foo.zenとして次のような構造体を定義する。

pub const Foo = struct {
    a : u32 = 0,
    b : u32 = 0,
    c : u32 = 0,
};

次のようにsrc/foo.zenの中にテストを書いて動作を確認する。

const std = @import("std");
const ok = std.testing.ok;
test "struct" {
    const foo = Foo{.a = 10, .b = 20, .c = 30};
    ok(foo.a == 10);
    ok(foo.b == 20);
    ok(foo.c == 30);
}

{.a = 10}のような感じでメンバ名を指定して初期化できる。

変数にアクセスする場合はfoo.aの様に構造体のインスタンス.区切りでメンバを指定する。

実行結果

$ zen test ./src/foo.zen
All tests passed.

メソッド

Zenでは構造体の中に関数のメンバを定義することができる。

先程のFooの例であればfoo.func()ように呼び出せる。

ダメな例

慣れていない場合は次のようにしてしまいそうだが、これはエラーになる。

pub const Foo = struct {
    a : u32 = 0,
    b : u32 = 0,
    c : u32 = 0,
    fn func() u32 {
        return a + b + c;
    }
};

次のようなテストで確認する。

test "method" {
    const foo = Foo{.a = 10, .b = 20, .c = 30};
    ok(foo.func() == 60);
}

実行結果

t$ zen test ./src/foo.zen 
src/foo.zen:21:28: エラー[E09055]: 「0」個の引数が求められていますが、与えられた引数は「1」個です。
    const result = foo.func();
                           ~
src/foo.zen:5:5: メモ[E00001]: ここで宣言されています。
    fn func() u32 {
    ~
src/foo.zen:6:16: エラー[E07002]: 未宣言の識別子「a」が使用されています。
        return a + b + c;

エラーの原因は2つある。

エラー原因1

まずは1つ目。

src/foo.zen:21:28: エラー[E09055]: 「0」個の引数が求められていますが、与えられた引数は「1」個です。
    const result = foo.func();

これはメソッドがどのインスタンスに対して呼び出されたかを特定するために暗黙に第一引数に自分自身が渡される。 これがないと次のような場合に困る。

const a = Foo{}; 
const b = Foo{};
a.func(); //
b.func(); // どちらもおなじFoo型のfunc

funcメソッドの場合、fn func(self: Foo) u32のようなシグニチャが求められる。 つまり、次のようなことが暗黙的に行われている。

const a = Foo{}; 
const b = Foo{};
Foo.func(a); // 第一引数に自分を渡す。
Foo.func(b); // これでどちらのインスタンスか分かる。

a.func(); //
b.func(); // これらは上のメソッド呼び出しと同じ。

エラー原因2

次に2つ目。

src/foo.zen:6:16: エラー[E07002]: 未宣言の識別子「a」が使用されています。
        return a + b + c;

結局は1つ目と同じ原因で、どのインスタンスのメンバか判断できないためエラーになる。

こちらも、暗黙に渡される第一引数を使用して判別する。

pub const Foo = struct {
    a : u32 = 0,
    b : u32 = 0,
    c : u32 = 0,
    fn func(self: Foo) u32 {
        return self.a + self.b + self.c;
    }
};

次のようにテストする。

test "method" {
    const foo = Foo{.a = 10, .b = 20, .c = 30};
    ok(Foo.func(foo) == 60);
    ok(foo.func() == 60);
}

実行結果

$ zen test ./src/foo.zen 
All tests passed.

ミューテーション

いわゆるsetterの様に自分のメンバを書き換えるメソッドを定義してみる。

ダメな例

安直にすると次のように書いてしまいそうだが、これもエラーになる。

pub const Foo = struct {
    a : u32 = 0,
    fn setA(self: Foo, a: u32) void {
        self.a = a;
    }
};

テストを書いてみる。

test "mutation" {
    var foo = Foo{};
    foo.setA(10);
    ok(foo.a == 10);
}

実行結果

$ zen test ./src/foo.zen 
src/foo.zen:9:18: エラー[E02030]: 定数変数に代入できません。
        self.a = a;
                 ~

変数foovarとして定義されているので定数ではないはずだがこのエラーが発生する。

定数変数という表現が微妙だがイミュータブル(immutable)な変数ということ。

この原因はメソッドの第一引数であるselfがイミュータブルな変数として扱われるためである。

selfに限らず関数の引数として構造体を渡す場合、基本的にイミュータブルな変数として扱われる。

つまり、次のケースも同様にエラーとなる。

fn globalFunc(foo: Foo) void {
    foo.a = 10;
}

test "global mutation" {
    var foo = Foo{};
    globalFunc(foo);
}

実行結果

$ zen test ./src/foo.zen 
src/foo.zen:35:13: エラー[E02030]: 定数変数に代入できません。
    foo.a = 10;
            ~

このような場合、fn setA(self: *Foo, a: u32) voidの様に引数を構造体のポインタにする。

pub const Foo = struct {
    a : u32 = 0,
    fn setA(self: *Foo, a: u32) void {
        self.a = a;
    }
};

test "mutation" {
    var foo = Foo{};
    foo.setA(10);
    ok(foo.a == 10);
}

実行結果。

$ zen test ./src/foo.zen 
All tests passed.

クラス変数的なもの

その型に横断的に存在し、複数のインスタンスに共通な変数を定義することもできる。

pub const Foo = struct {
    a : u32 = 0,
    var bar: u32 = 100;
    const foobar = 50;
 };

test "class member" {
    ok(Foo.bar == 100);
    Foo.bar = 200;
    ok(Foo.bar == 200);
    ok(Foo.foobar == 50);
}

このような場合、インスタンス名ではアクセスできないことに注意が必要。

test "access class member via instance" {
    const foo = Foo{};
    ok(foo.bar == 100);
}

実行結果

$ zen test ./src/foo.zen 
src/foo.zen:48:11: エラー[E07004]: 「Foo」にはメンバー「bar」がありません。
    ok(foo.bar == 100);
          ~

クラス変数的なものはメンバではないということらしい。

クラス関数的なもの

関数も同様に定義できる。もちろんインスタンスが必要なメンバにはアクセスできない。

pub const Foo = struct {
    a : u32 = 0,
    const foobar = 50;

    fn getFoobar() usize {
        return foobar;
    }
};

test "class function" {
    ok(Foo.getFoobar() == 50);
}

こちらもインスタンス名ではアクセスできない。

test "class function via instance" {
    const foo = Foo{};
    ok(foo.getFoobar() == 50);
}

実行結果

$ zen test ./src/foo.zen 
src/foo.zen:67:21: エラー[E09055]: 「0」個の引数が求められていますが、与えられた引数は「1」個です。
    ok(foo.getFoobar() == 50);
                    ~
src/foo.zen:14:5: メモ[E00001]: ここで宣言されています。
    fn getFoobar() usize {
    ~

@This()

メソッドを定義する際に自分自身の型名が必要になるが、これを@This()という組み込み関数で抽象化することができる。

@This()は自分自身の型名を取得することができる。

pub const Foo = struct {
    a : u32 = 0,
    fn hoge(self: @This()) u32 {
        return self.a;
    }
};

test "hoge" {
    const foo = Foo{.a = 60};
    ok(foo.hoge() == 60);
}

テストの実行結果

$ zen test ./src/foo.zen 
All tests passed.

メソッドの定義で毎回@This()を呼び出すのはC言語のマクロの多用の様に可読性が低くなるので、次のようにして予め自身の型名を取得しておくと可読性が上がる。

pub const Foo = struct {
    a : u32 = 0,
    const Self = @This();
    fn fuga(self: Self) u32 {
        return self.a;
    }
};

test "fuga" {
    const foo = Foo{.a = 60};
    ok(foo.fuga() == 60);
}

テストの実行結果

$ zen test ./src/foo.zen 
All tests passed.

まとめ

構造体ではメソッドや型に横断的な変数、定数、関数が定義できる。

意外と写経したものを深く考えずに使ってしまいがちなので、いろいろ試してみた。

Zenではテストを書いて一つずつためしながら実装できるのがうれしい。

Giant BoardをDebian GNU/Linux 9 (stretch)で動かす

Giant Boardが届いた。

このボードにはCortext-A5コアのATSAMA5D27-D1G-CUが搭載されている。

MICROCHIPによると、こういうものらしい。

The SAMA5 series are high-performance, ultra-low power ARM Cortex-A5 core based MPU devices. They support multiple memories, including DDR3, LPDDR3, and QSPI Flash.

ATSAMA5D27MPU System in Packages (SiPs)というもので、パッケージの中にDDRも入っている。 なので、他にメモリを配置する必要がないためボードがかなり小さい。

Cortex-AなのでLinuxも動く。というか標準サポートのOSがDebianとなっている。

How to build custom Giant Board device images.を参考にOSをビルドしてみた。

作業環境はUbuntu 18.04

依存パッケージのインストール

device tree compilerをインストールする。18.04のaptでインストールできるバージョンは古いので下記から取ってくる。

$ cd /tmp
$ wget http://mirrors.edge.kernel.org/ubuntu/pool/main/d/device-tree-compiler/libfdt1_1.5.1-1_amd64.deb
$ wget http://mirrors.edge.kernel.org/ubuntu/pool/main/d/device-tree-compiler/device-tree-compiler_1.5.1-1_amd64.deb
$ sudo gdebi libfdt1_1.5.1-1_amd64.deb
$ sudo gdebi device-tree-compiler_1.5.1-1_amd64.deb

ツールのセットアップ

ビルド用のスクリプトを使用する。

$ git clone https://github.com/Groboards/giantboard-tools.git
$ cd giantboard-tools
$ chmod +x build_menu.sh
$ ./build_menu.sh

メニューを起動する。

Build Options:
1: Setup Build Environment.(Run on first setup.)
2: Build at91bootstrap
3: Build u-boot
4: Build kernel/clean
5: Rebuild kernel
6: Build debian rootfs
7: Chroot into rootfs
8: Build device overlays
9: Make bootable device image
Enter selection [1-7] > 

初回実行時は1: Setup Build Environment.(Run on first setup.)を選択する。

それ以降は必要に応じて実行することになるが、まっさらな状態では次の順序で実行することになる。

| 2: Build at91bootstrap | 3: Build u-boot | 4: Build kernel/clean | 6: Build debian rootfs | 8: Build device overlays | 9: Make bootable device image

Build device overlaysで下記の様なエラーが出力されるが、特に問題はなさそう?

<stdin>:20.22-29.6: Warning (spi_bus_reg): /fragment@1/__overlay__/ethernet@1: SPI bus unit address format error, expected "0"
done building..

Make bootable device imagegiantboard.imgが生成される。

これをddコマンドなどでSDに書き込んで起動する。

$ sudo dd if=giantboard.img of=/dev/sdX bs=100M --dry-run

/dev/sdXは環境によって適宜読み替える必要がある。

起動

ピンヘッダに出ているTX,RX,GNDをUSB-シリアル変換ケーブルでPCに接続し、minicomでログインする。

アカウントは次の通り。

item value
user root
passwd root

次のようになればOK。

Debian GNU/Linux 9 giantboard ttyS0

Login incorrect
giantboard login: root
Password:
Linux giantboard 5.0.0+ #1 Mon Feb 17 19:03:20 JST 2020 armv7l
   _____ _             _     ____                      _
  / ____(_)           | |   |  _ \                    | |
 | |  __ _  __ _ _ __ | |_  | |_) | ___   __ _ _ __ __| |
 | | |_ | |/ _` | '_ \| __| |  _ < / _ \ / _` | '__/ _` |
 | |__| | | (_| | | | | |_  | |_) | (_) | (_| | | | (_| |
  \_____|_|\__,_|_| |_|\__| |____/ \___/ \__,_|_|  \__,_|


root@giantboard:~# uname -a
Linux giantboard 5.0.0+ #1 Mon Feb 17 19:03:20 JST 2020 armv7l GNU/Linux
root@giantboard:~#

まとめ

giantboardが動いた。カーネルのージョンはもともと4.14とされていたのだが、意外にも5.0.0だった。

電源については少し不安定な様で、ボードのUSBポートへVBUSで給電するよりも、USB-シリアル変換の5VをボードのVBATに入れるのが安定しているように見えた。

素人ながら回路図を確認した感じだと、VBATは入ってすぐにMIC5247-2.0に入るので、 VBATは5V入れても問題なさそう。(わからんけど)

今のところは元気に動いているが、ちょっと電力を使うようなものを動かすとリブート祭りになる。

Bashスクリプトで複数のテーブルを組み合わせて使用する

はじめに

タイトルいったい何のことか伝わらないと思うので。

いわゆる2次元配列っぽいことをしたいというか、要素数の異なる複数のテーブルを多重ループで回す感じ。

2次元配列が近いと思って調べたけど、自分のやりたいこととうまく合致しないかな。と。

やりたいこと

例えば、複数の環境と対象としてテストプログラムを実行したいが、環境毎に実行するプログラムが異なる。

環境と環境毎のテストプログラムのテーブルをそれぞれもってて、それを組み合わせてループさせたい。

(C++らしなく)C++で書くとこんな感じのやつ。

#include <cstdio>
#include <vector>

int main() {
    enum {
        Env1,
        Env2,
        Env3,
        Env_Max,
    };
    std::vector<const char*> env1_test {"testA", "testB", "testC"};
    std::vector<const char*> env2_test {"testD"};
    std::vector<const char*> env3_test {"testA", "testB", "testC", "testD"};
    std::vector<std::vector<const char*>> table {
            {env1_test}, {env2_test}, {env3_test},
    };
    for (auto i = 0; i < Env_Max; ++i) {
        for (auto elem : table[i]) {
            std::printf("Env[%d]:%s\n", i, elem);
        }
    }
    return 0;
}

実行結果

Env[0]:testA
Env[0]:testB
Env[0]:testC
Env[1]:testD
Env[2]:testA
Env[2]:testB
Env[2]:testC
Env[2]:testD

環境毎のテストプログラムの数が異なるのがポイント。

やったこと

bashスクリプトでは多重配列が無い(はず?)なので、このようなケースは意外と難しい。

ほとんど型の概念がなく、文字列処理としての側面が強いので、実行時に変数名を合成して複数の配列を組み合わせることにした。

#!/bin/bash

readonly -a Env1_test=(
    testA
    testB
    testC
)

readonly -a Env2_test=(
    testD
)

readonly -a Env3_test=(
    testA
    testB
    testC
    testD
)

readonly -a Environments=(
    Env1
    Env2
    Env3
)

execute() {
    local env=$1
    local test=$2
    echo ${env} ${test}
}

execute_all() {
    for env in ${Environments[@]} ; do
        local -n tests=${env}_test
        for t in ${tests[@]} ; do
            execute ${env} ${t}
        done
    done
}

execute_all

実行結果

$ ./test.sh
Env1 testA
Env1 testB
Env1 testC
Env2 testD
Env3 testA
Env3 testB
Env3 testC
Env3 testD

execute_allの「local -n tests=${env}_test」の行で変数名を合成している。 ここではEnvironments配列の内容であるEnvXと固定の文字列_testを合成してEnvX_testという変数名を作成して、 その中の要素にループでアクセスしている。-nがポイントで、変数を名前参照とすることで、 展開後の変数と文字列を組み合わせて、別に実体のある変数の参照をローカル変数のtestsに格納している。

これをしないと、実体が空のEnvX_testがtestsに格納されるので意図した動きにならない。

まとめ

名前参照を使うと意外と便利なことができる。 ただしやりすぎると破綻する。

PlatformIO Digisparkを動かす

はじめに

ShigezoneでDigisparkを購入したのPlatformIOで動かしてみる。

書き込みの時にコツがいるのでメモっておく。

プロジェクトの作成

作業用ディレクトリを次のように作成する。

$ mkdir -p ~/pio/digispark
$ cd ~/pio/digispark

このボードのCPUはATTINY85なのでボードをdigispark-tinyに設定する。

$ pio init -b digispark-tiny

プログラムの作成

platformio.iniはデフォルトのままで良い。

src/main.cppを次の内容で作成する。

#include <Arduino.h>

#define LED (1)

void setup() {
    pinMode(LED, OUTPUT);
}

void loop() {
    digitalWrite(LED, HIGH);
    delay(500);
    digitalWrite(LED, LOW);
    delay(500);
}

ビルドと書き込み

次のコマンドでビルド。

$ pio run

書き込みでエラー

書き込みで使用しているmicronucleusがエラーにになる。

$ pio run -t upload
(...省略 ...)
micronucleus: library/micronucleus_lib.c:66: micronucleus_connect: Assertion `res >= 4' failed.
Aborted (core dumped)
*** [upload] Error 134

有名なエラーのようでトラブルシューティングは見つかる。

ここによると、udevルールを作成すると良いとある。

/etc/udev/rules.d/49-micronucleus.rules

# UDEV Rules for Micronucleus boards including the Digispark.
# This file must be placed at:
#
# /etc/udev/rules.d/49-micronucleus.rules    (preferred location)
#   or
# /lib/udev/rules.d/49-micronucleus.rules    (req'd on some broken systems)
#
# After this file is copied, physically unplug and reconnect the board.
#
SUBSYSTEMS=="usb", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="0753", MODE:="0666"
KERNEL=="ttyACM*", ATTRS{idVendor}=="16d0", ATTRS{idProduct}=="0753", MODE:="0666", ENV{ID_MM_DEVICE_IGNORE}="1"
#
# If you share your linux system with other users, or just don't like the
# idea of write permission for everybody, you can replace MODE:="0666" with
# OWNER:="yourusername" to create the device owned by you, or with
# GROUP:="somegroupname" and mange access using standard unix groups.

次のコマンドで設定を反映する。

$ sudo udevadm control --reload-rules

それでも発生するエラー

バイスへのアクセス権限の問題だと言われていたが、それを解決したはずなのにまだこのエラーが出る。

micronucleus: library/micronucleus_lib.c:66: micronucleus_connect: Assertion `res >= 4' failed.

試行錯誤を繰り返し辿り着いた解決方法。

ボードとPCをUSBケーブルで接続しているが、書き込む前に一度接続を外すとうまく行くことがわかった。

手順としては次のようになる。

  1. USBケーブルを抜去
  2. 書き込みコマンドを実行
  3. USBケーブルを挿入
  4. 書き込み成功

まず、USBケーブルを抜いて接続されていない状態にする。

$ pio run -t upload
(...省略...)
AVAILABLE: micronucleus
CURRENT: upload_protocol = micronucleus
Uploading .pio/build/digispark-tiny/firmware.hex

この表示が出たところで、ケーブルを接続する。

> Please plug in the device ... 
> Press CTRL+C to terminate the program.
> Device is found!
connecting: 40% complete
> Device has firmware version 1.6
> Available space for user applications: 6012 bytes
> Suggested sleep time between sending pages: 8ms
> Whole page count: 94  page size: 64
> Erase function sleep duration: 752ms
parsing: 60% complete
> Erasing the memory ...
erasing: 80% complete
> Starting to upload ...
writing: 100% complete
>> Micronucleus done. Thank you!
=================================== [SUCCESS] Took 46.83 seconds ===================================

このようにメッセージが表示されれば書き込み成功。

まとめ

micronucleusでの書き込みには若干のコツが必要となり手間取った。 Google先生によって、エラーメッセージとudevルールまでは簡単にたどり着くが、USBでの接続を一度切るというところまではなかなか見つけられなかった。

無事、LEDがチカチカしたのでOK。

ASF傘下に入ったNuttXをビルド

はじめに

NuttXはASFのIncubatorプロジェクトへ採択され、リポジトリがBitBucketからGitHubに変更された。

今後はプルリクエストなどもGitHubで受け付ける。

ソースコードを取得

GitHubからソースコードを取得するには次のようにする。

$ git clone https://github.com/apache/incubator-nuttx.git nuttx
$ git clone https://github.com/apache/incubator-nuttx-apps.git apps

ビルド

今のところ、ビルド手順に変更はない。

STM32F4DiscoveryのUSB NuttX Shell環境向けにビルドする。

$ cd nuttx
$ tools/configure.sh stm32f4discovery:usbnsh
$ make -j $(nproc)

書き込み

試しに書き込んでみる。

$ st-flash write nuttx.bin 0x8000000

まとめ

リポジトリ名がそれぞれincubator-nuttxincubator-nuttx-appsに変更されているので注意が必要。

incubator期間が終わったらまた名前が変わるのだろうか。

PlatformIO mbedのビルド時間を短縮するスクリプトを作った

はじめに

2020/3/6 追記

PlatformIOでframeworkをmbedにすると必要のない機能のソースコードコンパイルするため、 ビルド時間がかなり長くなってしまう。

| Mbed for STM32 compiles toooooo long | mbed + PlatformIO = too long compilation

これは度々話題に挙げられていて.mbedignoreで不要なソースをフィルタできると答えが出るのだが、

Does .mbedignore still work?

.mbedignoreは適切な場所に配置しないと効力を発揮しないことと、 適切な配置場所の具体的な例が乏しいため、実際のところ使いづらい。

いろいろ試した結果、.mbedignoreでビルド時間の短縮に成功した。しかし、毎回手でこれを行うが面倒なので PlatformIOのextra_scriptsの機能を使用して、プロジェクト毎に簡単に使用できるようにしてみた。

.mbedignoreファイル

まず、.mbedignoreファイルについて説明する。

mbed osのソースツリーに配置することで、使用しない機能のソースファイルをビルドから除外することができる。

ただしソースツリーのルート直下に配置された.mbedignoreは無視されてしまう。

.mbedignoreの内容を正しく適用するにはその一段下のディレクトリにそれぞれ配置する必要がある。

.
├── TESTS
├── TEST_APPS
├── UNITTESTS
├── cmsis
├── components
├── docs
├── drivers
├── events
├── features
├── hal
├── platform
├── platformio
├── rtos
├── targets
└── tools

例えば、features/cellular/*をビルド対象から外したい場合は、featuresディレクトリに次の内容で.mbedignoreを作成する必要がある。

cellular/*

extra_scripts

PlatformIOではmbedフレームワークのソースツリーは${HOME}/.platformio/packages/framework-mbedに格納される。

framework-mbedディレクトリは、そのユーザーが使用するすべてのPlatformIOのプロジェクトから参照されるので、プロジェクト毎に毎回調整する必要が出てくる。

少なくとも使用するボードによってビルド対象から外したい機能やドライバは変わってくるはずなので、これを毎回設定するのは面倒くさい。

そこで、pio-mbedignoreというリポジトリに、ヘルパスクリプトを作成した。

使い方

ヘルパスクリプトの使い方の流れは次のようになる。

  1. リポジトリから適当な場所にダウンロードする
  2. mbedignore.pyをPlatformIOの自分のプロジェクトのルートに配置する
  3. mbedignoreディレクトリを作成し、その下にfeaturesdriversなどの名前でファイルを作成する
  4. 作成したファイルに.mbedignoreの記述ルールに従って、無視したいファイルのパスを指定する
  5. platformio.iniを修正し、extra_scriptに設定する

具体的には次のようになる。

$ git clone https://github.com/mickey-happygolucky/pio-mbedignore.git
$ cp pio-mbedignore/mbedignore.py /path/to/pio_project
$ cd /path/to/pio_project
$ mkdir mbedignore

先程の例のようにcellularを除外するにはmbedignore/featuresを次の内容で作成する。

cellular/*

ビルド毎にmbedignore.pyが実行されるようにするために、platformio.iniに次の内容を追加する。

2020/3/6 追記1

extra_scripts =
  pre:mbedignore.py
  post:mbedignore.py

extra_scripts =
  post:mbedignore.py

これで、pio run毎に、必要に応じて、framework-mbed以下に.mbedignoreが配置され、指定したファイルをビルド対象から外すことができるようになる。

extra_scriptsのpost:指定にも指定しているのはゴミが残らないようにするためだが、これについては後述する。

pre:post:の仕様を勘違いしていた。platformio.iniでのpre/post指定は、extra_scriptsの実行タイミングをPlatformIOのビルドシステムのメインスクリプトを実行する前にするか、後ろにするかという指定。 env.AddPreAction()/env.AddPostAction()を使用するため、ターゲットシステムの情報が確定されてから実行したいのでpost:のみの設定が正解。

2020/3/6 追記1 終了

仕組み

PlatformIOではplatformio.iniextra_scriptspythonスクリプトのファイルを指定すると、platformioコマンド実行時に既存の処理をカスタムしたり、任意の処理を追加したりすることができる。

今回作成したスクリプトはそれを利用して、pio run毎に必要に応じてframework-mbedディレクトリに.mbedignoreを配置するようにしている。 実際には、実ファイルを作成するわけではなく、シンボリックリンクを作成している。

mbedignore.py

extra_scriptsではImport(env)を使用することで、platformio実行時の環境変数にアクセスしたり、 任意のタイミングで実行されるアクションを登録できるようになっている。

Import("env")
import os
import glob

srcs = glob.glob(os.getcwd() + '/mbedignore/*')
print(srcs)
dst_basedir = env.Split(env['PROJECT_PACKAGES_DIR'])[0] + '/framework-mbed'

def clean_mbedignore(source, target, env):
    print('Clean .mbedignore')

    for src in srcs:
        dst_dir = dst_basedir + '/' + os.path.basename(src)
        dst = dst_dir +'/.mbedignore'

        if os.path.exists(dst) == True:
            os.remove(dst)
            print('symlink deleted : ' + dst)


def mbedignore():
    for src in srcs:
        dst_dir = dst_basedir + '/' + os.path.basename(src)
        dst = dst_dir +'/.mbedignore'

        if os.path.exists(dst) == True:
            os.remove(dst)
            print('symlink deleted : ' + dst)

        if os.path.exists(dst_dir) == True:
            os.symlink(src, dst)
            print('symlink created : src = ' + src + '->' + dst)


mbedignore()
env.AddPostAction("checkprogsize", clean_mbedignore)

PlatformIOが知っているディレクトリやファイルなどは環境変数のようにアクセスできるので、 必要な情報はそこから取り出すようにしている。

2020/3/6 追記2

env.AddPreAction("buildprog", clean_mbedignore)の行で、このスクリプトによって作成されたシンボリックリンクを削除する処理を、 bulidprogターゲットの後に実行されるように登録している。

なぜかplatformio.inipre:指定した場合はここで設定した処理が実行されないので、このスクリプトpost:でも登録している。

ただし、このスクリプトの実行時にすでに.mbedignoreが存在している場合は、削除してから作り直すので post:の指定を忘れても実行に影響は無い。

修正前はenv.AddPreAction("buildprog", clean_mbedignore)としていたが、ビルド済みだった場合実行されないので、 env.AddPostAction("checkprogsize", clean_mbedignore)とした。修正後はcheckprogsizeターゲットの後に、このスクリプトによって作成されたシンボリックリンクを削除している。

2020/3/6 追記2 終了

実行時間比較

リポジトリのテスト用のmbedignoreファイルを使用して、 bluepill環境でpio runの実行時間を比較した。

どちらも実行前にcleanしてある。

スクリプト未使用時

real  2m23.069s
user    8m41.679s
sys 1m38.485s

スクリプト使用時

real  0m23.076s
user    1m56.642s
sys 0m16.316s

体感でも違いがわかるくらいには差が出ている。

まとめ

PlatforIOでmbedを使用するとビルド時間が長い。

.mbedignoreを使えば不要なコンパイルを避けビルド時間を短縮できるが、 具体的な使い方がの情報が少ないことと、意外と不親切。

今回はmbedignore.pyを作成し、簡単に.mbedignoreの恩恵を受けられるようにした。

ctagsのoptlibをつかってZen言語のタグジャンプを(少しだけ)快適にする

はじめに

前回の続き。

gtagsのPygments-pluginを拡張してZen言語のシンボルタグを抽出してタグジャンプできるようにした。

しかし、定義タグが無く、すべてシンボルタグとなるため関数のジャンプなどがちょっと残念。

Pygments-pluginはctags -xのコマンドの出力結果を使用して定義タグを抽出することができる。

そこで、universal-ctagsのoptlibという正規表現ベースで簡易的なパーサを追加できる機能を使用して、関数の定義タグを抽出できるようにしてみる。

universal-ctagsを改造する

ソースコードを取得する

まず、githubからソースコードを取得する。

$ git clone https://github.com/universal-ctags/ctags.git
$ cd ctags

optlibのパーサ定義ファイルを作成する

ソースツリーのoptlibディレクトリ以下に、*.ctagsと、その対になる*.cが存在する。

拡張子が.ctagsとなっているパーサ定義ファイルをツールに書けると、Cのソースファイルが自動生成され、 それを使用してlibctags.aというライブラリを作成し、ctagsからリンクするようになっている。

なのでまず、Zen言語向けのパーサ定義ファイルを作成する。

optlib/zen.ctagsを次の内容で作成する。

--langdef=Zen
--map-Zen=+.zen

--kinddef-Zen=c,const,constant values
--kinddef-Zen=v,var,variables
--kinddef-Zen=f,fn,functions

--regex-Zen=/^(pub|[[:blank:]])*const[[:blank:]]+([A-Za-z0-9_]+)[[:blank:]]*=/\2/c/
--regex-Zen=/^(pub|[[:blank:]])*var[[:blank:]]+([A-Za-z0-9_]+)[[:blank:]]*=/\2/v/
--regex-Zen=/^(pub|[[:blank:]])*fn[[:blank:]]+([[:alnum:]]+).*$/\2/f/

--kindef-Zenの行で、タグの種別を定義し、--regexp-Zenの行でそれぞれの抽出条件を定義する。 正規表現は拡張表現となっている。

Zen言語のすべての構文を正規表現で記述するのはムリがあるので、よく使うであろう、constvarfnのみ定義している。

ビルド対象へ追加

makefiles/optlib2c_input.makを次のように変更する。

diff --git a/makefiles/optlib2c_input.mak b/makefiles/optlib2c_input.mak
index 0a0e3b11..59302eab 100644
--- a/makefiles/optlib2c_input.mak
+++ b/makefiles/optlib2c_input.mak
@@ -15,5 +15,6 @@ OPTLIB2C_INPUT = \
        optlib/puppetManifest.ctags             \
        optlib/scss.ctags                       \
        optlib/systemtap.ctags                  \
+       optlib/zen.ctags                        \
        \
        $(NULL)

これで、ctagsをビルドする毎に、zen.ctagsからzen.cが生成されるようになる。

パーサ定義テーブルに追加

このままだとlibctags.aとしてビルドされるが、ctagsからはリンクされない状態になる。

Cの配列として定義されているパーサー定義テーブルにZenのパーサを追加する必要がある。

main/parsers_p.hを次のように修正する。

diff --git a/main/parsers_p.h b/main/parsers_p.h
index c7799eda..efa060b7 100644
--- a/main/parsers_p.h
+++ b/main/parsers_p.h
@@ -145,6 +145,7 @@
        WindResParser, \
        YaccParser, \
        YumRepoParser, \
+       ZenParser, \
        ZephirParser
 
 #endif  /* CTAGS_MAIN_PARSERS_H */

ビルドする

$ make distclean
$ ./autogen.sh
$ ./configure
$ make -j $(nproc)
$ sudo make install

これで、ctagsがZen言語のソースファイルを認識できるようになる。

タグを抽出してみる

ctags -xを実行して、Zenのソースファイルからタグが抽出できるか試してみる。

前回のサンプルプログラムで試してみる。

$ cd ~/zen/hello
$ ctags -x src/main.zen
add              fn            3 ./src/main.zen   fn add(lhs: u32, rhs: u32) u32 {
main             fn            7 ./src/main.zen   pub fn main() anyerror!void {
std              const         1 ./src/main.zen   const std = @import("std");
value            const         8 ./src/main.zen   const value = add(10, 30);

このように表示されればOK。

addmainがきちんと関数として認識されている。

Pygments-pluginが有効化された状態でgtagsを実行すると、エディタ上で関数のタグジャンプができるようになる。

$ gtags -v 
[Tue Jan 28 23:05:33 JST 2020] Gtags started.
 Using configuration file '/home/mickey/.globalrc'.
 Using configuration label 'pygments'.
 Using plug-in parser.
[Tue Jan 28 23:05:33 JST 2020] Creating 'GTAGS' and 'GRTAGS'.
 [1] extracting tags of src/main.zen
 [2] extracting tags of build.zen
[Tue Jan 28 23:05:33 JST 2020] Done.

まとめ

ctagsに正規表現ベースのカスタムパーサを追加することできるoptlibを追加して、GNU Globalの定義タグを抽出できるようにした。

メンバやenumなどがすべてシンボルタグになってしまうので、Zen言語のサポートとしてはまだまだ未熟だが、 関数や定数のジャンプが行えるようになったので、コーディングやコードリーディングはなかなか捗るのではないだろうか。

もちろん、複雑な記述によるコーナーケースなどはうまく行かないところは出てくると思うが。

不完全とはいえ、既存のタグシステムを使用してZenのような新しい言語への対応ができたのは面白いと思う。