「.NET MAUI の iOS ビルドにネイティブ静的ライブラリを含めたら、AudioToolbox まわりの未定義シンボルで止まる」――この現象は、プロジェクト設定とリンカの最終挙動を正しく理解できていれば、短時間で確実に解決できます。本稿では、_AudioServicesCreateSystemSoundID をはじめとする AudioToolbox の未定義シンボル問題の根本原因と再発を防ぐ設定を、実プロジェクト向けの具体例・検証コマンドとともに体系的にまとめます。FFmpeg 連携時に一緒に現れがちな CoreMedia/CoreVideo・libbz2 依存の対処も一気通貫で解説します。
症状
Undefined symbols for architecture arm64:
"_AudioServicesCreateSystemSoundID", referenced from ...
"_AudioServicesPlaySystemSound", referenced from ...
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
<NativeReference> の <Frameworks>VideoToolbox,AudioToolbox</Frameworks> を宣言しているにも関わらず、最終リンクで AudioToolbox の解決に失敗します。シミュレータ(iossimulator-x64)では通るのに実機(ios-arm64)で落ちる、Release だけ落ちる、といった揺れ方をすることもあります。
原因の本質
- 静的ライブラリは自己完結ではない:
libmylib.aが C の関数(AudioToolbox の C API など)に依存しても、その依存フレームワークを自動では引っ張ってきません。最終リンク段階で-framework AudioToolboxを明示する必要があります。 - MAUI(mtouch)の最終リンクは「2段構え」:MSBuild の
<Frameworks>は中間生成物(Xcode プロジェクト相当)に伝わりますが、最後に mtouch が行うリンクに必ずしも伝播しないことがあります。ここで<MtouchExtraArgs>による直接指定が保険になります。 - デッドストリップ/スマートリンク:
ldは参照が弱い(または間接参照の)シンボルを掃除します。<ForceLoad>と<SmartLink>falseはこの最適化を抑止し、静的ライブラリ内の必要オブジェクトを確実に取り込ませるためのカギです。
結論(最短ルートの設定)
次の2点で解決します。
- mtouch に
-framework AudioToolboxを直渡し <NativeReference>でも重ねて宣言+最適化抑止
1) MtouchExtraArgs に AudioToolbox を強制指定
<PropertyGroup>
<MtouchExtraArgs>
--cxx -gcc_flags "-framework AudioToolbox"
</MtouchExtraArgs>
</PropertyGroup>
--cxx:静的ライブラリ側が C++ を含む場合の保険(libc++連携)。-gcc_flags:最終リンクへそのまま渡されます(名前は歴史的経緯)。
ビルド構成を限定したい場合は条件付きにできます:
<PropertyGroup Condition="'$(TargetFramework)'=='net8.0-ios' and '$(RuntimeIdentifier)'=='ios-arm64'">
<MtouchExtraArgs>--cxx -gcc_flags "-framework AudioToolbox"</MtouchExtraArgs>
</PropertyGroup>
2) NativeReference を堅牢化
<ItemGroup>
<NativeReference Include="Platforms\iOS\lib\libmylib.a">
<Kind>Static</Kind>
<Frameworks>VideoToolbox,AudioToolbox</Frameworks>
<ForceLoad>true</ForceLoad>
<SmartLink>false</SmartLink>
</NativeReference>
</ItemGroup>
- ForceLoad:
libmylib.a全体の取り込みを保証。 - SmartLink=false:デッドストリップの影響で必要コードが落ちるのを防止。
- 同じフレームワークを
<Frameworks>と<MtouchExtraArgs>に重複指定しても害はありません。伝播漏れ対策の二重化と考えてください。
FFmpeg 連携で「芋づる」発生する未定義シンボル
FFmpeg の静的ライブラリ群(libavcodec.a、libavformat.a など)を追加すると、AudioToolbox 以外にも依存が噴き出します。代表例と対処を表にまとめます。
| 未定義シンボル | 原因 | 対応策 |
|---|---|---|
BZ2_bzDecompress* など | FFmpeg を bzip2 (libbz2) 有効でビルドしている | ① FFmpeg を --disable-bzlib で再ビルドし依存を除去② もしくは iOS 向けにビルドした libbz2.a を追加しリンク |
_CMSampleBufferCreate ほか | CoreMedia.framework が不足 | <Frameworks> または -framework CoreMedia を追加 |
_CVPixelBuffer* ほか | CoreVideo.framework が不足 | <Frameworks> または -framework CoreVideo を追加 |
最終的に動作した設定例
<PropertyGroup>
<MtouchExtraArgs>
--cxx -gcc_flags "-framework AudioToolbox -framework CoreMedia -framework CoreVideo"
</MtouchExtraArgs>
</PropertyGroup>
<ItemGroup>
<!-- 自前ライブラリ -->
<NativeReference Include="Platforms\iOS\lib\libmylib.a">
<Kind>Static</Kind>
<Frameworks>AudioToolbox,CoreMedia,CoreVideo,VideoToolbox</Frameworks>
<ForceLoad>true</ForceLoad>
<SmartLink>false</SmartLink>
</NativeReference>
<!-- FFmpeg ライブラリ群 -->
<NativeReference Include="Platforms\iOS\lib\libavcodec.a">
<Kind>Static</Kind>
<Frameworks>CoreMedia,CoreVideo,VideoToolbox</Frameworks>
<ForceLoad>true</ForceLoad>
<SmartLink>false</SmartLink>
</NativeReference>
<NativeReference Include="Platforms\iOS\lib\libavformat.a">
<Kind>Static</Kind>
<ForceLoad>true</ForceLoad>
<SmartLink>false</SmartLink>
</NativeReference>
<NativeReference Include="Platforms\iOS\lib\libavutil.a">
<Kind>Static</Kind>
<ForceLoad>true</ForceLoad>
<SmartLink>false</SmartLink>
</NativeReference>
<NativeReference Include="Platforms\iOS\lib\libswresample.a">
<Kind>Static</Kind>
<ForceLoad>true</ForceLoad>
<SmartLink>false</SmartLink>
</NativeReference>
<NativeReference Include="Platforms\iOS\lib\libswscale.a">
<Kind>Static</Kind>
<ForceLoad>true</ForceLoad>
<SmartLink>false</SmartLink>
</NativeReference>
<!-- bzip2 を使う場合のみ -->
<NativeReference Include="Platforms\iOS\lib\libbz2.a">
<Kind>Static</Kind>
<ForceLoad>true</ForceLoad>
<SmartLink>false</SmartLink>
</NativeReference>
</ItemGroup> </code></pre>
<h2>なぜ「<Frameworks>だけ」ではダメだったのか</h2>
<p><code><Frameworks></code> は MSBuild パイプラインの途中で生成されるバンドル情報に寄与しますが、最終的な <strong>mtouch のリンク行</strong>はプロジェクトの他設定や依存関係の解析結果次第で最適化・調整されます。ここで参照が弱い(または条件付き)なシンボルは取りこぼされる可能性があるため、<em>必ず</em> <code><MtouchExtraArgs></code> で <code>-framework</code> を直渡しするのが安全策です。二重化は冗長ではなく、<strong>最終段リンクへの伝播保証</strong>と理解してください。</p>
<h2>再発防止のためのチェックリスト</h2>
<ol>
<li><strong>ネイティブ依存の棚卸し</strong>:<br><code>nm -gU libXXX.a | grep _</code> で「<em>未解決(U)シンボル</em>」を洗い出す。FFmpeg など複数の .a を束ねる場合はすべてに対して実施。</li>
<li><strong><code><Frameworks></code> と <code>MtouchExtraArgs</code> を双方設定</strong>:<br>同じフレームワークを二重に書いても問題なし。むしろ安心。</li>
<li><strong>外部圧縮/暗号ライブラリの有無</strong>:<br><code>zlib</code>/<code>bzlib</code>/<code>iconv</code>/<code>zstd</code> 等、FFmpeg のビルドオプションで依存が変わる。不要なら無効化、有用なら iOS 用に静的ビルドして追加。</li>
<li><strong><code>ForceLoad=true</code> & <code>SmartLink=false</code> を基本に</strong>:<br>カテゴリやコールバックベースの参照は検出が弱く、最適化で落ちがち。まずはこの組み合わせでビルドを安定化。</li>
<li><strong>アーキテクチャの整合</strong>:<br><code>lipo -info libmylib.a</code> で <code>arm64</code> を含むことを必ず確認。シミュレータ用(<code>x86_64</code>/<code>arm64e</code>)しか入っていない「薄い」ライブラリは実機ビルドに失敗します。</li>
<li><strong>C++ 混在の明示</strong>:<br>少しでも C++ コードが含まれるなら <code>--cxx</code> を付与。必要に応じて <code><IsCxx>true</IsCxx></code> を <code><NativeReference></code> に追加すると確実です。</li>
<li><strong>クリーンビルドで検証</strong>:<br>キャッシュに引っ張られないよう <code>bin/</code>・<code>obj/</code> を削除してから再ビルド。</li>
</ol>
<h2>トラブルシューティング(実践コマンド集)</h2>
<ul>
<li><strong>未定義シンボルを探す</strong><br>
<pre><code>nm -u path/to/libmylib.a | sort | uniq
nm -gU path/to/libavcodec.a | grep -E "AudioServices|CMSampleBuffer|CVPixelBuffer|BZ2"
アーキテクチャを確認
lipo -info path/to/libmylib.a
静的ライブラリの中身(オブジェクトファイル)を確認
ar -t path/to/libmylib.a | head
最終リンク行を詳しく見る
dotnet build -f net8.0-ios -c Release -v:n
# あるいは -v:detailed で、mtouch が実行する ld の引数を観察
FFmpeg 側のビルド方針(依存を最小化する)
iOS への静的リンクは「依存が少ないほど勝ち」です。以下は実運用で安定している構成の考え方です。
- 不要機能は無効化:
--disable-bzlib、--disable-iconv、--disable-autodetectなどで依存を削る。 - ハードウェアアクセラレーション:VideoToolbox を使うなら
--enable-videotoolbox。その場合はCoreMedia/CoreVideoの追加も忘れずに。 - ビルド成果物の粒度:使わないライブラリ(
swscaleなど)はそもそもリンクしない。1つでも余計な .a があると依存が発火しやすい。
「ビルドは通るが実機で動かない」を潰す最終点検
- Entitlements/Info.plist の権限:AudioToolbox のシステム音は一般に追加権限不要ですが、マイク入出力や背景音再生を同時に扱う場合は
NSMicrophoneUsageDescriptionのようなキーが要るシナリオもあります。 - RuntimeIdentifier を明示:CI/CD での取り違え防止に
<RuntimeIdentifier>ios-arm64</RuntimeIdentifier>を設定しておくと確実。 - Release/Debug で同じフラグか:Release でのみ最適化が強く、
SmartLinkの影響が変わることがあるため、両構成でMtouchExtraArgsを揃える。
よくある誤解と解毒
| 誤解 | 正しい理解 |
|---|---|
| 「<Frameworks> に書けば十分」 | 最終リンクまでは保証されないことがある。-framework を MtouchExtraArgs で直渡しする。 |
| 「ForceLoad は重くなるから使わない」 | ビルドサイズ・リンク時間の増加は限定的。未定義で止まる方が痛い。問題切り分け中は常時有効に。 |
| 「AudioToolbox は iOS 標準だから自動でリンクされる」 | 自動では来ない。依存は依存として明示するのが鉄則。 |
| 「シミュレータで動くなら実機も動く」 | アーキテクチャや最適化が異なる。実機(arm64)で必ず検証する。 |
設計メモ:AudioToolbox を呼ぶ側のベストプラクティス
- API は C 層で薄く包む:MAUI からは P/Invoke で最小限の関数に橋渡し。Swift/Objective‑C のカテゴリ拡張に頼らない構造にするとリンクが安定します。
- 戻り値とエラーの伝搬:AudioServices 系は OS 側で失敗しても戻り値でしかわかりません。成功/失敗を
OSStatusで受け取り、.NET 例外にマッピングしておくと保守性が高まります。 - サウンドIDのライフサイクル:
AudioServicesCreateSystemSoundID→ 再生 →AudioServicesDisposeSystemSoundIDを必ず対で。リークすると実機で症状が出やすい。
終端まとめ(現場の手順書)
- まず 未定義シンボルの一覧を
nmで出す。 - 足りない Apple フレームワークを
<Frameworks>とMtouchExtraArgsの両方に列挙(AudioToolbox/CoreMedia/CoreVideo)。 - 静的ライブラリは
ForceLoad=true・SmartLink=falseで登録。 - FFmpeg 由来の bzip2 依存は、
--disable-bzlibで消すか、libbz2.aをリンク。 - アーキテクチャ(
arm64)と C++ 有無(--cxx)を確認。 - クリーンビルド → 実機で動作確認。
テンプレ:問題解決に使い回せる .csproj 断片
<PropertyGroup Condition="'$(TargetFramework)'=='net8.0-ios'">
<RuntimeIdentifier>ios-arm64</RuntimeIdentifier>
<MtouchExtraArgs>
--cxx -gcc_flags "-framework AudioToolbox -framework CoreMedia -framework CoreVideo"
</MtouchExtraArgs>
</PropertyGroup>
Static
true
AudioToolbox,CoreMedia,CoreVideo,VideoToolbox
true
false
Static
true
false
Static
true
false
チェックリスト(配布前の最終確認用・印刷推奨)
| 項目 | 観点 | 確認方法 |
|---|---|---|
| フレームワーク指定 | AudioToolbox/ CoreMedia / CoreVideo を二重化 | .csproj の <Frameworks> と <MtouchExtraArgs> |
| スマートリンク抑止 | 必要コードのデッドストリップ防止 | ForceLoad=true、SmartLink=false |
| FFmpeg 依存 | bzlib など外部依存の最小化 | FFmpeg の configure オプション確認 |
| アーキテクチャ | arm64 を含む | lipo -info |
| C++ 有無 | --cxx 指定の整合 | MtouchExtraArgs または <IsCxx> |
| 未定義の有無 | 未知の「U」シンボルが残っていない | nm -u の結果ゼロ |
| 実機試験 | 通知音・再生・解放の動作 | サウンド ID のライフサイクル確認 |
まとめ
AudioToolbox の未定義シンボルは、最終リンクへ -framework を直渡しし、ForceLoad と SmartLink=false で最適化を抑えることで解消します。FFmpeg を同梱するなら、CoreMedia/CoreVideo と外部圧縮ライブラリ(とくに libbz2)の取り扱いが定石です。MAUI の iOS ネイティブ連携は「依存を明示する」「最終リンクを制御する」の二本柱。ここさえ押さえれば、AudioToolbox、FFmpeg + VideoToolbox を含む構成でも安定してビルド・動作させられます。
付録:簡易デバッグ・スクリプト(任意)
プロジェクトの Platforms/iOS/lib 直下を検査するワンライナー例です。CI の前段で差し込み、早期に依存漏れを検出できます。
#!/usr/bin/env bash
set -euo pipefail
LIBDIR="Platforms/iOS/lib"
echo "[arm64 check]"
for a in "$LIBDIR"/*.a; do
echo -n "$(basename "$a"): "
lipo -info "$a" | sed 's/.*are: //'
done
echo "[undefined symbols of interest]"
for a in "$LIBDIR"/*.a; do
nm -gU "$a" | grep -E "U _AudioServices|U _CMSampleBuffer|U _CVPixelBuffer|U *BZ2*" || true
done
付録:P/Invoke 最小例(AudioToolbox)
ネイティブ側の API を薄く包み、.NET では責務を厳格化して扱います。
// C レイヤ(libmylib.a 側)
#include <AudioToolbox/AudioToolbox.h>
OSStatus PlaySystemSoundFromURL(CFURLRef url) {
SystemSoundID sid = 0;
OSStatus st = AudioServicesCreateSystemSoundID(url, &sid);
if (st != noErr) return st;
AudioServicesPlaySystemSound(sid);
AudioServicesDisposeSystemSoundID(sid);
return noErr;
}
// C# 側(.NET MAUI)
using System;
using System.Runtime.InteropServices;
static class NativeAudio
{
[DllImport("__Internal")]
private static extern int PlaySystemSoundFromURL(IntPtr cfUrl);
public static void PlayFromFile(string path)
{
if (string.IsNullOrEmpty(path)) throw new ArgumentNullException(nameof(path));
using var url = CF.CFUrlCreateFromFile(path); // 自作の CFURL ラッパー想定
var status = PlaySystemSoundFromURL(url.Handle);
if (status != 0) throw new InvalidOperationException($"AudioToolbox error: {status}");
}
}
この形なら、リンクのために必要なシンボルが はっきり 参照され、ForceLoad との相性も良くなります。
付録:ビルドログを読む(何が渡っているか?)
-v:detailed でビルドすると、mtouch が最終的に実行したリンカ行が表示されます。そこに -framework AudioToolbox、-framework CoreMedia、-framework CoreVideo が並んでいるかを目視で確かめましょう。もし欠けているなら、MtouchExtraArgs の引用符(ダブルクォート)やスペース区切りを見直してください(よくあるのは全角スペース混入や改行位置のズレ)。
クロージング
ネイティブ連携での「未定義シンボル」は、設定と観察の手順を一度整備してしまえば、以後は 機械的に再現・解決できます。本稿のテンプレとチェックリストをチームの標準に組み込み、AudioToolbox/FFmpeg/VideoToolbox のような複合依存でも、迷わず安定した .NET MAUI iOS アプリを届けていきましょう。

コメント