15年開発してるMac用画像ビューアを3年ぶりにバージョンアップした時にやったこと

こんにちは、絵描き兼開発者の「ゆき」です。
今日は私が学生時代から10年以上、地味に開発・サポートを続けているMac向けの画像ビューアを3年ぶりくらいにバージョンアップしたお話です。
<img width="1004" alt="500img.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/70bfa8af-70c7-58ad-b798-2bd7179bd811.jpeg">

# この記事のモチベーションと対象読者

詳細は後ろに書きますが、Macアプリの個人開発界隈はそろそろ死にます。[^death]
今年リリース予定の次期MacOS(10.15)で32bitアプリのサポートが完全に終了するためです。古くからの個人開発アプリは新しいバージョンが出ない限り新しいOSでは動きません。

[^death]: もちろん私見ですし異論も多々あるかと思いますが、Webやクロスプラットフォームな実行環境の選択肢が色々出てきている時代です。Macのネイティブアプリを個人がわざわざ作る時代ではないのかな...と。そして、過去の遺産も次のMacOSで清算されるので、その意味で「死」と表現しました。

もちろんきちんと開発継続しているアプリはもう大体対応済みかとは思いますが、個人開発のフリーウエア・シェアウエアに関しては開発者本人すら忘れていたりすることもある[^forget]ので、そんな個人開発者の方向けにこの記事が届けばいいなー...くらいの気持ちで書き残しておこうと思います。
[^forget]: 私です

# 今回の対象アプリ

画像ビューア「絵箱」
http://nekobooks.com/main/ebako/

「絵箱」はMac向けの画像ビューア＆ファイラー的なアプリです。フォルダ内の画像ファイルをサムネイル一覧でプレビューして、簡単な整理・編集ができる機能を提供しています。
今でこそFinder上でもそこそこ画像のプレビューができますが、それでもシンプルなUIと直感的な操作性で今でも幾らかのユーザーさんに使っていただいています（完全にフリーなのでMacの方は試してみてください）。

このアプリの前提環境を記載しておきます：

* 開発環境・言語はXojo(REAL basic) [^xojo]
* プラグインの作成にObjective-C/Xcodeを使用 [^plugin]
* App Storeでは配布しない野良アプリ（証明書はあり）[^store] 
* 形態は通常のアプリケーション（カーネル拡張等の特殊な要素はなし）

[^xojo]: 昔むかしにCrossBasic/REALbasicという名前でMacアプリの個人開発を流行らせた言語/開発環境。日本市場からは完全に消えたけど、アメリカではまだちゃんとバージョンアップしてる。クロスプラットフォームアプリが作れるVBA、みたいな立ち位置で小規模ビジネス用途にシフトして生き残っている感じ。これから開発を始める人には正直オススメしないけど、10年変わらずにサポートしてくれるのは素晴らしいことだと思う。

[^plugin]: Xojoには不足する機能をプラグインとして組み込める機能がある。Macの場合はObjective-CやSwiftで記述可能

[^store]: 正確にはStoreに置きたいけど置けない。Storeアプリはシステム内のファイルに直接アクセスできない（デスクトップやPictures等特定フォルダのみ可）ので、フォルダ階層をルートからツリーで表示するようなアプリは基本的に作れない。個人的にはこの辺りの制限もMacのAppStoreがイマイチ面白くならなかった理由だと思っている

ちょっと特殊な環境ではあると思いますが、逆に王道のStoreアプリの場合は常に最新の環境に合わせていかないとストアから落とされてしまうので、今この記事が役に立つ方がいるとすれば、ある種似たような環境なのかも、と思います。

# 3年ぶりのバージョンアップでやったことと躓きポイント

ここからはざっくり、今回のバージョンアップで行なった修正のポイントをメモしたいと思います。一部REALbasic(Xojo)のコードが出てきますが、なんとなく感触で読めると思います。

## 64bit対応

まず今回一番の山が64bit対応です。
64bit化自体は基本的にはコンパイラがよしなにやってくれるので、簡単なアプリであればほぼ修正なしで動くのですが、絵箱の場合は残念ながら大量の修正が必要でした。

### OS API呼び出し部分の修正

Xojoは基本的にOS固有の機能をそれほど提供してくれないので、不足する部分はMacOSのAPIを呼び出して補完します。
絵箱はファイルシステムを操作したりFinder等のOS標準の挙動に合わせたりと、何かとAPI呼び出しが多いアプリなのでまずこの部分の書き換えが必要になりました。

例えば、CGRect。
￼![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/78e4dee7-fcc0-cf30-3a0c-d132a47e1e67.png)
CoreGraphics系の処理を呼ぶためにXojo側にも構造体の定義をしているのですが、64bit環境では当然ながら各要素は8バイトのdoubleにしないと動きません。
残念なことにこのミスマッチはコンパイルはおろか呼び出し時にもチェックされないため、実行してみて初めてクラッシュしたりおかしな値（おそらくxとyの8バイト分をdouble型で読んでしまう）を取り出してしまって後続の処理でエラーになったりします。

これはわかってしまえば対処は簡単で、定義を全部書き換えるだけです。

もうちょっと厄介なのが直接ポインタを操作している場所。
例えば下の例はNSArrayをXojoの配列に変換する関数なのですが、どこが問題かお分かりになるでしょうか？

```vb

Function NSArrayToArray (ref as Ptr) as Ptr()
  Soft Declare Function count Lib "Cocoa" Selector "count" (nsArrayRef as Ptr) as Integer
  Soft Declare Sub getObjects Lib "Cocoa" Selector "getObjects:range:" (nsArrayRef as Ptr,objsRef as Ptr,range as CocoaLibMod.NSRange)

  Dim Count as Integer = count(ref)
  Dim ArrContsRef as MemoryBlock = New MemoryBlock(Count * 4)
  getObjects ref,ArrContsRef,NSMakeRange(0, Count)

  Dim ContPtrs(Count - 1) as Ptr
  For I as Integer = 0 to Count - 1
    ContPtrs(I) = ArrContsRef.Ptr(I * 4)
  Next

  Return ContPtrs
End Function
```

ちゃんとCからネイティブを勉強されてる人であれば（REALbasicは知らなくても）わかると思います。`* 4`ですね。仕事でやったらレビューで怒られる:scream:ヤツだと思うのですが、きっとこれ書いた時には64bitのことなんて何も考えていなかったんだと思います。。

これも修正は簡単で、`4`を定数化して64bit環境では`8`になるように変更するだけです。

```vb
Dim SIZE as Integer = 4
#If Target64Bit Then
  SIZE = 8
#Endif
```

問題はこの地雷が埋め込まれている場所をどうやって探すか・・・
結局4とか8とか怪しい数字で4万行のコードを全検索する羽目になりました。。マジックナンバーはダメ！絶対！:no_good_tone1:

### バイナリ読み書きの修正

OSのAPI呼び出し以外で64bitが問題になったのがバイナリの読み書き部です。
絵箱では性能を気にしてバイナリファイルを多用していたのと、データのシリアライズやディープコピーに自作したバイナリ操作クラスを使っていたためにあらゆる箇所でアプリがクラッシュしました。

たとえば下記のメモリーブロックに文字列とその長さ情報を書き込む処理。

```vb
Dim M as MemoryBlock = New MemoryBlock(6 + LenB(ValStr))
M.Long(0) = LenB(ValStr) + 6
M.Short(4) = 8
M.StringValue(6,LenB(ValStr)) = ValStr
```

もともと`Long`は4バイト符号付整数、`Short`は2バイト符号付整数だったのですが、
Xojoコンパイラが64bitに対応した時にこれらは`Long == Integer`, `Short == Int16`の別名と定義されています。さらに`Integer`は32bit環境では`Int32`、64bit環境では`Int64`の別名とみなされます。

つまり、上記のコードでは`Short`が常に2バイト固定であることが保証されているのに対し、`Long`は環境依存で4バイトだったり8バイトだったりするわけです。

修正は基本的に環境依存の型を使用せず、バイト数を明示して読み書きするように修正すればOKです。

```vb:修正
Dim TotalLen as Integer = HeaderLen + LenB(ValStr)
M = New MemoryUtil.StreamMemoryBlock(TotalLen)
M.WriteUInt32Value TotalLen
M.WriteUInt16Value Variant.TypeString
M.WriteStringValue ValStr
```

### Objective-Cプラグインの修正と再ビルド

絵箱では基本的に外部のプラグインは利用していないのですが、唯一ファイルのサムネイルを取得するためにプラグインを書いています。[^gcd]

[^gcd]: サムネイルの読み込み自体は通常のAPIコールで対応できるのですが、Xojoでは言語仕様上マルチコア・マルチスレッドで処理を実行する方法がないため、性能を重視する場面ではプラグインを組む必要が出てきます。

マイナー言語のさらにマイナーな領域になるので細かくは書きませんが、基本的にはxcode上でターゲットアーキテクチャを64bitに変更すればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/a2b80ffd-cfbe-6014-0387-fb997b2280db.png)

数少ない情報源としては[GREIF Software - 64bit対応について](http://greif.la.coocan.jp/rb_64bit.html)がとても参考になりました。
以前Xojoに同梱されていたサンプルでは、なぜかデフォルトで`QuickTime.lib`がリンクされているのでこの辺り不要なリンクも外さないとエラーになると思います。

## 廃止APIの置き換え
これも吐き気してくるヤツですね。数年ぶりにビルド通そうとするといろんなAPIが廃止されていて悲しみにくれることになります。特に初期のCarbon時代のAPIは単純な移行パスがないものも多いです。

例えばファイルのラベル一覧の定義を取得する処理。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/7d0a72ce-ad48-6f05-0da9-a50175d5b26b.png)
（↑この一覧を作るのに必要）

```vb:ラベルの一覧を取得する
Public Function GetLabelColorAtIndex (Index as Integer) as Color
  #If TargetCarbon or TargetCocoa Then
    Soft Declare Function GetLabel Lib "Carbon" (LabelNumber as Integer,LabelColor as Ptr,LabelString as Ptr) as Integer
  #Else
    Soft Declare Function GetLabel Lib "InterfaceLib" (LabelNumber as Integer,LabelColor as Ptr,LabelString as Ptr) as Integer
  #EndIf

  Dim LabelColor, LabelString as MemoryBlock
  Dim Re as Integer

  LabelColor = New MemoryBlock(6)
  LabelString = New MemoryBlock(256)
  Re = GetLabel(index,LabelColor, LabelString)
  Return RGB(LabelColor.UShort(0)/257,LabelColor.UShort(2)/257,LabelColor.UShort(4)/257)
End Function
```

`InterfaceLib`とか出てくる時点でどれだけ古いんだこのコード...[^label]

[^label]: 多分もともとは[REALbasicとFutureBASICで使おうToolbox最新活用テクニック (MAC POWER BOOKS)](https://www.amazon.co.jp/dp/4756140262)に載ってたコードだと思う

ここで呼び出している[GetLabel](http://mirror.informatimago.com/next/developer.apple.com/documentation/Carbon/Reference/IconServices/icon_services/function_group_11.html#//apple_ref/doc/uid/TP30000239/F01827)関数はもはやドキュメントからも削除されていて、何に置き換えたら良いのかもわからず。。結局同等と思われる[機能を持つAPI](https://developer.apple.com/documentation/appkit/nsworkspace/1533953-filelabels?language=objc)を探して実装し直しています。CarbonLib時代のAPIは大体消えてしまったのでこの辺りは単純な書き換えができないのが辛いところ。。

```vb:NSWorkSpace.class
Public Function fileLabelColors() as Color()
  Declare Function getFileLabelColors Lib "AppKit.framework" Selector "fileLabelColors" (ws as Ptr) as Ptr
  
  Dim ArrPtr as Ptr = getFileLabelColors(Me.Ref)
  Dim ColPtrs() as Ptr = NSArrayToArray(ArrPtr) // NSArrayをXojoの配列に変換
  Dim LabelColors(-1) as Color
  For each ColPtr as Ptr in ColPtrs
    Dim Col as Color = NSColorToColor(ColPtr) // NSColorをXojoのColorに変換
    LabelColors.Append Col
  Next
  
  Return LabelColors
End Function
```

## Retinaディスプレイに対応

ここまでで大体、アプリとしては動く状態になりました。ここからは「最近のアプリっぽく」するために必要な機能を追加していきます。

まずはRetina対応から。
￼![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/a8106e5b-4c72-6eaf-ccf5-f2e1fd39aa7f.png)

上が対応後。下が対応前です。
メインマシン(iMac)がいまだにRetinaではないので気がつかなかったのですが、Retina環境で並べてみると歴然です。
Retina対応自体は[Info.plistを書き換えるだけ](https://blog.totakke.net/posts/2012-12-18-mac-os-x-app-retina-support/)の簡単なお仕事で、文字も画像も（画像データのピクセル数さえ足りていれば）勝手に×2で描画してくれます。
問題なのは描画性能向上のためにオフスクリーンバッファ（メモリ上で一旦画像を生成してから画面に描画する方法）を使っている場合。
×2になるのはあくまでRetinaディスプレイ上に描画するコンテキストを使う場合だけなので、オフスクリーンで描いてしまうとRetinaになりません。
対処法としてはオフスクリーン上では（出力先がRetinaであれば）2倍で描画するのが基本になります。同様に、アイコン等サイズを指定してOSから引っ張ってくるものも2倍にする必要があります。
ただ、昔は性能向上やチラツキ回避に多用されたオフスクリーンバッファですが、今では不要になっているケースも多いはずです。絵箱でもこの機会に細かい描画は直接行うように見直しました。

## MacOSX MojaveのDark Modeに対応

もう一つの新機能対応がDark Modeです。これ対応するだけで今っぽくなりますね。
これも基本的には何もしなくてもOSのコントロール部分はOS側がよしなにやってくれるのですが...
￼![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/c6b30983-b89b-6177-021f-adb656010371.png)

こんな感じですね。。自力で描画している領域が多いアプリの場合、Dark Modeを検知して描画も変えてあげないといけません。
絵箱は幸い以前からテーマの切り替え機能を作り込んでいるので、今回はDark Modeを検知した時点で専用のテーマに自動切り替えすることで対処しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245740/845f2af4-b0ec-e79b-6774-c3d5f5753455.png)

絵箱はたまたまテーマ切り替え機能を作り込んでいたからよかったのですが、そのあたりの機能がない状態だとこの対応はかなり面倒かもしれません。

## 証明書の更新とアプリの署名

作ったアプリを配布するにはAppleに発行してもらった自分の証明書を使ってアプリを署名する必要があります。署名がないアプリはダブルクリックで起動できないので、配布するなら実質的に署名は必須です。作業は簡単だけど有償の開発者登録（年間1万弱）が必要です。[^devmember]
[^devmember]: 有償メンバシップが切れても、一度取得した証明書の有効期限内（4年くらい）であれば取得済みの証明書で署名できる。また、一度署名したアプリは証明書の有効期限が切れても問題なく起動できるはず。

証明書の取得方法自体は（iOSアプリでも同様に必要なので）巷にたくさん情報があると思います。[iOS, Certificate 証明書を作ってみる](https://i-app-tec.com/ios/apply-application.html)あたりが分かりやすかったです。
基本的にはiOSと同じ手順ですが、証明書の発行はMacアプリ用をリクエストしてください。
https://developer.apple.com/account/mac/certificate/create

Xcodeを使った開発の場合はXcode側に証明書を登録してビルドするのかと思いますが、Xojoの場合には自分でコードサインの作業が必要です。下のようなスクリプトを作っておいて、ビルド時に呼ばれるようにしておくと良いかと思います。

```doCodeSign.sh
#!/bin/sh
xattr -cr $1
codesign -f --deep -s  "Developer ID Application: XXXX（証明書の名前）" $1
```

# まとめ

- 古いMacアプリは64bit化しないと次のMacOSでは動かない
- 古いアプリもRetinaやDark Modeに対応させると今風に（なるかも）
- 昔作ったフリーウエアをHDDの奥に寝かせている方はこの機会にバージョンアップしてみるのもいいかも

あと、よかったら絵箱使ってみてくださいませ