デバッグ情報移行: Intrinsic からレコードへ

LLVM からデバッグ情報 Intrinsic を削除する予定です。これらは低速で扱いにくく、最適化パスがそれらを予期していない場合、混乱を招く可能性があります。次のような命令シーケンスを持つ代わりに、

    %add = add i32 %foo, %bar
    call void @llvm.dbg.value(metadata %add, ...
    %sub = sub i32 %add, %tosub
    call void @llvm.dbg.value(metadata %sub, ...
    call void @a_normal_function()

デバッグ情報レコードを表す dbg.value Intrinsic を使用すると、代わりに次のように出力されます。

    %add = add i32 %foo, %bar
      #dbg_value(%add, ...
    %sub = sub i32 %add, %tosub
      #dbg_value(%sub, ...
    call void @a_normal_function()

デバッグレコードは命令ではなく、命令リストには表示されず、意図的に掘り下げない限り、最適化パスには表示されません。

素晴らしい、何をすればいいですか!

ほとんど何もする必要はありません。LLVM 全体をこれらの新しいレコード(”DbgRecords”)を処理し、過去の LLVM の動作と同様に動作するように既に計測済みです。これは現在デフォルトで有効になっているため、DbgRecords はメモリ、IR、およびビットコードでデフォルトで使用されます。

API の変更

注意すべき重要な変更が 2 つあります。まず、BasicBlock::iterator クラスにデバッグ関連のデータを 1 ビット追加しています(これは、範囲がブロックの先頭にデバッグ情報を含めることを意図しているかどうかを判断できるようにするためです)。つまり、LLVM IR 命令を挿入するパスを記述する場合、裸の Instruction * ではなく、BasicBlock::iterator で位置を識別する必要があります。ほとんどの場合、これは何かを挿入する場所を特定した後、命令位置で getIterator を呼び出す必要があることを意味します。ただし、ブロックの先頭に挿入する場合、最初の命令へのポインタを取得するのではなく、必ず getFirstInsertionPtgetFirstNonPHIIt、または begin を使用してそのイテレータを使用して挿入する必要があります。

2 つ目の問題は、命令シーケンスを手動である場所から別の場所に転送する場合、つまり splice を使用していた可能性のある場所で moveBefore を繰り返し使用する場合、代わりに moveBeforePreserving メソッドを使用する必要があるということです。moveBeforePreserving は、デバッグ情報レコードをそれが添付されている命令と共に転送します。これは現在自動的に行われています。命令シーケンスのすべての要素で moveBefore を使用すると、デバッグ Intrinsic はコードの通常の過程で移動されますが、命令以外のデバッグ情報ではこの動作が失われます。

デバッグレコードをサポートするように既存のコードを更新する方法の詳細な概要については、以下のガイドを参照してください。

テキスト形式の IR の変更

デバッグ Intrinsic からデバッグレコードに変更すると、LLVM によって生成された IR の解析に依存するツールは、新しい形式を処理する必要があります。ほとんどの場合、デバッグ Intrinsic 呼び出しの出力形式とデバッグレコードの違いは些細なものです。

  1. インデントに 2 つのスペースが追加されます。

  2. (tail|notail|musttail)? call void @llvm.dbg.<type> というテキストは、#dbg_<type> に置き換えられます。

  3. 先頭の metadata は、Intrinsic の各引数から削除されます。

  4. DILocation は、!dbg !<Num> 形式の命令添付から、デバッグレコードの最後の引数として渡される通常の引数、つまり !<Num> に変更されます。

これらのルールに従って、デバッグ Intrinsic と同等のデバッグレコードの例を次に示します。

; Debug Intrinsic:
  call void @llvm.dbg.value(metadata i32 %add, metadata !10, metadata !DIExpression()), !dbg !20
; Debug Record:
    #dbg_value(i32 %add, !10, !DIExpression(), !20)

テストの更新

LLVM の IR 出力をテストするメインの LLVM リポジトリのダウンストリームにあるテストは、レコードを使用するように変更された結果、破損する可能性があります。上記の更新ルールを考えると、個々のテストを Intrinsic の代わりにレコードを予期するように更新するのは些細なことです。ただし、多くのテストを更新するのは負担になる可能性があります。メインリポジトリの lit テストを更新するには、次の手順を使用しました。

  1. 失敗した lit テストのリストを、改行で区切られた(そして改行で終わる)単一のファイル failing-tests.txt に収集します。

  2. 次の行を使用して、失敗したテストを update_test_checks を使用するテストと使用しないテストに分割します。

    $ while IFS= read -r f; do grep -q "Assertions have been autogenerated by" "$f" && echo "$f" >> update-checks-tests.txt || echo "$f" >> manual-tests.txt; done < failing-tests.txt
    
  3. update_test_checks を使用するテストについては、適切な update_test_checks スクリプトを実行します。メインの LLVM リポジトリでは、これは次のように実現されました。

    $ xargs ./llvm/utils/update_test_checks.py --opt-binary ./build/bin/opt < update-checks-tests.txt
    $ xargs ./llvm/utils/update_cc_test_checks.py --llvm-bin ./build/bin/ < update-checks-tests.txt
    
  4. 残りのテストは手動で更新できますが、テストの数が多い場合は、次のスクリプトが役立つ場合があります。まず、ファイルからチェックラインプレフィックスを抽出するために使用されるスクリプトです。

    $ cat ./get-checks.sh
    #!/bin/bash
    
    # Always add CHECK, since it's more effort than it's worth to filter files where
    # every RUN line uses other check prefixes.
    # Then detect every instance of "check-prefix(es)=..." and add the
    # comma-separated arguments as extra checks.
    for filename in "$@"
    do
        echo "$filename,CHECK"
        allchecks=$(grep -Eo 'check-prefix(es)?[ =][A-Z0-9_,-]+' $filename | sed -E 's/.+[= ]([A-Z0-9_,-]+).*/\1/g; s/,/\n/g')
        for check in $allchecks; do
            echo "$filename,$check"
        done
    done
    

    次に、一連の単純な置換パターンを使用して、各失敗テストのチェックラインを実際に更新する作業を実行する 2 番目のスクリプトです。

    $ cat ./substitute-checks.sh
    #!/bin/bash
    
    file="$1"
    check="$2"
    
    # Any test that explicitly tests debug intrinsic output is not suitable to
    # update by this script.
    if grep -q "write-experimental-debuginfo=false" "$file"; then
        exit 0
    fi
    
    sed -i -E -e "
    /(#|;|\/\/).*$check[A-Z0-9_\-]*:/!b
    /DIGlobalVariableExpression/b
    /!llvm.dbg./bpostcall
    s/((((((no|must)?tail )?call.*)?void )?@)?llvm.)?dbg\.([a-z]+)/#dbg_\7/
    :postcall
    /declare #dbg_/d
    s/metadata //g
    s/metadata\{/{/g
    s/DIExpression\(([^)]*)\)\)(,( !dbg)?)?/DIExpression(\1),/
    /#dbg_/!b
    s/((\))?(,) )?!dbg (![0-9]+)/\3\4\2/
    s/((\))?(, ))?!dbg/\3/
    " "$file"
    

    これらのスクリプトは両方とも、manual-tests.txt のリストで次のように使用できます。

    $ cat manual-tests.txt | xargs ./get-checks.sh | sort | uniq | awk -F ',' '{ system("./substitute-checks.sh " $1 " " $2) }'
    

    これらのスクリプトは、clang/test および llvm/test のチェックの大部分を正常に処理しました。

  5. 結果のテストが合格することを確認し、失敗したテストを検出します。

    $ xargs ./build/bin/llvm-lit -q < failing-tests.txt
    ********************
    Failed Tests (5):
    LLVM :: DebugInfo/Generic/dbg-value-lower-linenos.ll
    LLVM :: Transforms/HotColdSplit/transfer-debug-info.ll
    LLVM :: Transforms/ObjCARC/basic.ll
    LLVM :: Transforms/ObjCARC/ensure-that-exception-unwind-path-is-visited.ll
    LLVM :: Transforms/SafeStack/X86/debug-loc2.ll
    
    
    Total Discovered Tests: 295
    Failed: 5 (1.69%)
    
  6. 一部のテストは失敗した可能性があります。更新スクリプトは単純で、行全体でコンテキストを保持しないため、処理されないケースがあります。残りのケースは手動で更新する必要があります(または、さらにスクリプトで処理する必要があります)。

C-API の変更

追加された新しい関数の一部は一時的なものであり、将来廃止される予定です。これらは、移行期間中にダウンストリームプロジェクトの適応を支援することを目的としています。

Deleted functions
-----------------
LLVMDIBuilderInsertDeclareBefore   # Insert a debug record (new debug info format) instead of a debug intrinsic (old debug info format).
LLVMDIBuilderInsertDeclareAtEnd    # Same as above.
LLVMDIBuilderInsertDbgValueBefore  # Same as above.
LLVMDIBuilderInsertDbgValueAtEnd   # Same as above.

New functions (to be deprecated)
--------------------------------
LLVMIsNewDbgInfoFormat     # Returns true if the module is in the new non-instruction mode.
LLVMSetIsNewDbgInfoFormat  # Convert to the requested debug info format.

New functions (no plans to deprecate)
-------------------------------------
LLVMDIBuilderInsertDeclareRecordBefore   # Insert a debug record (new debug info format).
LLVMDIBuilderInsertDeclareRecordAtEnd    # Same as above. See info below.
LLVMDIBuilderInsertDbgValueRecordBefore  # Same as above. See info below.
LLVMDIBuilderInsertDbgValueRecordAtEnd   # Same as above. See info below.

LLVMPositionBuilderBeforeDbgRecords          # See info below.
LLVMPositionBuilderBeforeInstrAndDbgRecords  # See info below.

LLVMDIBuilderInsertDeclareRecordBeforeLLVMDIBuilderInsertDeclareRecordAtEndLLVMDIBuilderInsertDbgValueRecordBefore、および LLVMDIBuilderInsertDbgValueRecordAtEnd は、削除された LLVMDIBuilderInsertDeclareBefore スタイルの関数を置き換えています。

LLVMPositionBuilderBeforeDbgRecords および LLVMPositionBuilderBeforeInstrAndDbgRecords は、LLVMPositionBuilder および LLVMPositionBuilderBefore と同じように動作しますが、挿入位置はターゲット命令の前にあるデバッグレコードの前に設定されます。これは、選択した命令の前にあるデバッグ Intrinsic がスキップされることを意味するのではなく、デバッグレコードのみがスキップされることを意味します(デバッグレコードとは異なり、それ自体は命令ではありません)。

どの関数を呼び出すかわからない場合は、次のルールに従ってください。ブロックの先頭に挿入しようとしている場合、または他の理由でデバッグ Intrinsic を意図的にスキップして挿入ポイントを決定しようとしている場合は、新しい関数を呼び出します。

LLVMPositionBuilder および LLVMPositionBuilderBefore は変更されていません。これらは、指定された命令の前、ただし添付されているデバッグレコードの後に挿入します。

新しい「デバッグレコード」モデル

以下は、デバッグ Intrinsic を置き換える新しい表現の概要です。古いコードを更新するための有益なガイドについては、こちらを参照してください。

デバッグ Intrinsic を具体的に何と置き換えましたか?

デバッグ情報を格納するために DbgRecord と呼ばれる専用の C++ クラスを使用しています。デバッグ Intrinsic の各インスタンスと LLVM IR プログラムの各 DbgRecord オブジェクトの間には 1 対 1 の関係があります。これらの DbgRecord は、[ソースレベルデバッグ](project:SourceLevelDebugging.rst#Debug Records) ドキュメントで説明されているように、IR では命令以外のデバッグレコードとして表されます。このクラスには、デバッグ Intrinsic に格納されている情報とまったく同じ情報を格納する一連のサブクラスがあります。それぞれがほぼ同じメソッドセットを持ち、同じように動作します。

https://llvm.dokyumento.jp/docs/doxygen/classllvm_1_1DbgRecord.html https://llvm.dokyumento.jp/docs/doxygen/classllvm_1_1DbgVariableRecord.html https://llvm.dokyumento.jp/docs/doxygen/classllvm_1_1DbgLabelRecord.html

これにより、たとえばジェネリック(自動パラメータ)ラムダでは、DbgVariableRecorddbg.value/dbg.declare/dbg.assign Intrinsic であるかのように扱うことができ、DbgLabelRecorddbg.label についても同様です。

これらの DbgRecords は命令ストリームにどのように適合しますか?

以下のように。

                 +---------------+          +---------------+
---------------->|  Instruction  +--------->|  Instruction  |
                 +-------+-------+          +---------------+
                         |
                         |
                         |
                         |
                         v
                  +-------------+
          <-------+  DbgMarker  |<-------
         /        +-------------+        \
        /                                 \
       /                                   \
      v                                     ^
 +-------------+    +-------------+   +-------------+
 |  DbgRecord  +--->|  DbgRecord  +-->|  DbgRecord  |
 +-------------+    +-------------+   +-------------+

各命令は、DbgRecord オブジェクトのリストを含む DbgMarker(オプションになります)へのポインタを持っています。命令リストにはデバッグレコードはまったく表示されません。DbgRecord は、所有している DbgMarker への親ポインタを持ち、各 DbgMarker は、所有している命令へのポインタを持っています。

DbgRecord から `Value`/`Metadata` 階層の他の部分へのリンクは図示されていません: `DbgRecord` のサブクラスは、使用する DIMetadata への追跡ポインタを持ち、`DbgVariableRecord` は `DebugValueUser` 基底クラスに格納されている `Value` への参照を持ちます。これは、`TrackingMetadata` 機能を介して、`Value` を参照する `ValueAsMetadata` オブジェクトを参照します。

さまざまな種類のデバッグ組み込み関数(value、declare、assign、label)はすべて `DbgRecord` サブクラスに格納され、「RecordKind」フィールドで `DbgLabelRecord` と `DbgVariableRecord` を区別し、`DbgVariableRecord` クラスの `LocationType` フィールドで、それが表すことができるさまざまなデバッグ変数組み込み関数をさらに明確にします。

既存のコードを更新する方法

何らかの方法でデバッグ組み込み関数と相互作用する既存のコードはすべて、同じ方法でデバッグレコードと相互作用するように更新する必要があります。コードを更新する際に留意すべきいくつかの簡単なルール

  • 命令を反復処理してもデバッグレコードは表示されません。命令の直前に表示されるデバッグレコードを見つけるには、`Instruction::getDbgRecordRange()` を反復処理する必要があります。

  • デバッグレコードはデバッグ組み込み関数と同じインターフェースを持つため、デバッグ組み込み関数を操作するコードは、デバッグレコードにも簡単に適用できます。この例外は、デバッグレコードに論理的に適用されない `Instruction` または `CallInst` メソッドと、`isa`/`cast`/`dyn_cast` メソッドであり、`DbgRecord` クラス自体のメソッドに置き換えられます。

  • デバッグレコードは、デバッグ組み込み関数も含まれるモジュールには表示できません。この2つは相互に排他的です。デバッグレコードは将来の形式であるため、新しいコードではレコードの正しい処理を優先する必要があります。

  • 組み込み関数のサポートがなくなるまで、デバッグ組み込み関数のみを処理し、更新するのが難しいコードの有効な修正方法は、`Module::setIsNewDbgInfoFormat` を使用してモジュールを組み込み関数形式に変換し、後で元に戻すことです。

    • これは、`ScopedDbgInfoFormatSetter` クラスを使用して、モジュールまたは個々の関数の字句スコープ内でも実行できます。

    void handleModule(Module &M) {
      {
        ScopedDbgInfoFormatSetter FormatSetter(M, false);
        handleModuleWithDebugIntrinsics(M);
      }
      // Module returns to previous debug info format after exiting the above block.
    }
    

以下は、現在デバッグ組み込み関数をサポートしている既存のコードをデバッグレコードをサポートするように更新する方法の大まかなガイドです。

デバッグレコードの作成

新しい形式が有効になっている場合、デバッグレコードは `DIBuilder` クラスによって自動的に作成されます。命令と同様に、`DbgRecord::clone` を呼び出して、既存のレコードの未接続のコピーを作成することもできます。

デバッグレコードのスキップ、`Value` のデバッグ使用の無視、命令の安定したカウントなど

これはすべて、考えることなく透過的に行われます!

for (Instruction &I : BB) {
  // Old: Skips debug intrinsics
  if (isa<DbgInfoIntrinsic>(&I))
    continue;
  // New: No extra code needed, debug records are skipped by default.
  ...
}

デバッグレコードの検索

`findDbgUsers` などのユーティリティには、`Value` を参照する `DbgVariableRecord` レコードのセットを返すオプションの引数が追加されました。これらは組み込み関数と同じように扱うことができます。

// Old:
  SmallVector<DbgVariableIntrinsic *> DbgUsers;
  findDbgUsers(DbgUsers, V);
  for (auto *DVI : DbgUsers) {
    if (DVI->getParent() != BB)
      DVI->replaceVariableLocationOp(V, New);
  }
// New:
  SmallVector<DbgVariableIntrinsic *> DbgUsers;
  SmallVector<DbgVariableRecord *> DVRUsers;
  findDbgUsers(DbgUsers, V, &DVRUsers);
  for (auto *DVI : DbgUsers)
    if (DVI->getParent() != BB)
      DVI->replaceVariableLocationOp(V, New);
  for (auto *DVR : DVRUsers)
    if (DVR->getParent() != BB)
      DVR->replaceVariableLocationOp(V, New);

位置におけるデバッグレコードの検査

`Instruction::getDbgRecordRange()` を呼び出して、命令にアタッチされている `DbgRecord` オブジェクトの範囲を取得します。

for (Instruction &I : BB) {
  // Old: Uses a data member of a debug intrinsic, and then skips to the next
  // instruction.
  if (DbgInfoIntrinsic *DII = dyn_cast<DbgInfoIntrinsic>(&I)) {
    recordDebugLocation(DII->getDebugLoc());
    continue;
  }
  // New: Iterates over the debug records that appear before `I`, and treats
  // them identically to the intrinsic block above.
  // NB: This should always appear at the top of the for-loop, so that we
  // process the debug records preceding `I` before `I` itself.
  for (DbgRecord &DR = I.getDbgRecordRange()) {
    recordDebugLocation(DR.getDebugLoc());
  }
  processInstruction(I);
}

これは、より一般的に使用される DbgVariableRecords を具体的に反復処理するために、関数 `filterDbgVars` を介して渡すこともできます。

for (Instruction &I : BB) {
  // Old: If `I` is a DbgVariableIntrinsic we record the variable, and apply
  // extra logic if it is an `llvm.dbg.declare`.
  if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I)) {
    recordVariable(DVI->getVariable());
    if (DbgDeclareInst *DDI = dyn_cast<DbgDeclareInst>(DVI))
      recordDeclareAddress(DDI->getAddress());
    continue;
  }
  // New: `filterDbgVars` is used to iterate over only DbgVariableRecords.
  for (DbgVariableRecord &DVR = filterDbgVars(I.getDbgRecordRange())) {
    recordVariable(DVR.getVariable());
    // Debug variable records are not cast to subclasses; simply call the
    // appropriate `isDbgX()` check, and use the methods as normal.
    if (DVR.isDbgDeclare())
      recordDeclareAddress(DVR.getAddress());
  }
  // ...
}

個々のデバッグレコードの処理

ほとんどの場合、デバッグ組み込み関数を操作するコードは、デバッグ組み込み関数とデバッグレコードの両方に適用できるテンプレート関数または自動ラムダ(まだ存在しない場合)に抽出できます。ただし、`isa`/`cast`/`dyn_cast` は `DbgVariableRecord` タイプには適用されないという主な例外に注意してください。

// Old: Function that operates on debug variable intrinsics in a BasicBlock, and
// collects llvm.dbg.declares.
void processDbgInfoInBlock(BasicBlock &BB,
                           SmallVectorImpl<DbgDeclareInst*> &DeclareIntrinsics) {
  for (Instruction &I : BB) {
    if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I)) {
      processVariableValue(DebugVariable(DVI), DVI->getValue());
      if (DbgDeclareInst *DDI = dyn_cast<DbgDeclareInst>(DVI))
        Declares.push_back(DDI);
      else if (!isa<Constant>(DVI->getValue()))
        DVI->setKillLocation();
    }
  }
}

// New: Template function is used to deduplicate handling of intrinsics and
// records.
// An overloaded function is also used to handle isa/cast/dyn_cast operations
// for intrinsics and records, since those functions cannot be directly applied
// to DbgRecords.
DbgDeclareInst *DynCastToDeclare(DbgVariableIntrinsic *DVI) {
  return dyn_cast<DbgDeclareInst>(DVI);
}
DbgVariableRecord *DynCastToDeclare(DbgVariableRecord *DVR) {
  return DVR->isDbgDeclare() ? DVR : nullptr;
}

template<typename DbgVarTy, DbgDeclTy>
void processDbgVariable(DbgVarTy *DbgVar,
                       SmallVectorImpl<DbgDeclTy*> &Declares) {
    processVariableValue(DebugVariable(DbgVar), DbgVar->getValue());
    if (DbgDeclTy *DbgDeclare = DynCastToDeclare(DbgVar))
      Declares.push_back(DbgDeclare);
    else if (!isa<Constant>(DbgVar->getValue()))
      DbgVar->setKillLocation();
};

void processDbgInfoInBlock(BasicBlock &BB,
                           SmallVectorImpl<DbgDeclareInst*> &DeclareIntrinsics,
                           SmallVectorImpl<DbgVariableRecord*> &DeclareRecords) {
  for (Instruction &I : BB) {
    if (DbgVariableIntrinsic *DVI = dyn_cast<DbgVariableIntrinsic>(&I))
      processDbgVariable(DVI, DeclareIntrinsics);
    for (DbgVariableRecord *DVR : filterDbgVars(I.getDbgRecordRange()))
      processDbgVariable(DVR, DeclareRecords);
  }
}

デバッグレコードの移動と削除

`DbgRecord::removeFromParent` を使用して `DbgRecord` をマーカーから切り離し、`BasicBlock::insertDbgRecordBefore` または `BasicBlock::insertDbgRecordAfter` を使用して `DbgRecord` を他の場所に再挿入できます。 `DbgRecord` のリストの任意の場所に `DbgRecord` を挿入することはできません(`llvm.dbg.value` でこれを行っている場合、正しくない可能性があります)。

`eraseFromParent` を呼び出して `DbgRecord` を消去します。

// Old: Move a debug intrinsic to the start of the block, and delete all other intrinsics for the same variable in the block.
void moveDbgIntrinsicToStart(DbgVariableIntrinsic *DVI) {
  BasicBlock *ParentBB = DVI->getParent();
  DVI->removeFromParent();
  for (Instruction &I : ParentBB) {
    if (auto *BlockDVI = dyn_cast<DbgVariableIntrinsic>(&I))
      if (BlockDVI->getVariable() == DVI->getVariable())
        BlockDVI->eraseFromParent();
  }
  DVI->insertBefore(ParentBB->getFirstInsertionPt());
}

// New: Perform the same operation, but for a debug record.
void moveDbgRecordToStart(DbgVariableRecord *DVR) {
  BasicBlock *ParentBB = DVR->getParent();
  DVR->removeFromParent();
  for (Instruction &I : ParentBB) {
    for (auto &BlockDVR : filterDbgVars(I.getDbgRecordRange()))
      if (BlockDVR->getVariable() == DVR->getVariable())
        BlockDVR->eraseFromParent();
  }
  DVR->insertBefore(ParentBB->getFirstInsertionPt());
}

ダングリングデバッグレコードについて

次のようなブロックがある場合

    foo:
      %bar = add i32 %baz...
      dbg.value(metadata i32 %bar,...
      br label %xyzzy

最適化パスはターミネータを消去してからブロックに何かをしたい場合があります。デバッグ情報が命令に保持されている場合は簡単に実行できますが、`DbgRecord` では、ターミネータが消去されると、上記のブロックに変数情報をアタッチする後続の命令がありません。このような縮退ブロックの場合、`DbgRecord` は `LLVMContext` のマップに一時的に格納され、ターミネータがブロックに再挿入されるか、`end()` に他の命令が挿入されると再挿入されます。

これは、最適化パスがターミネータを消去してからブロック全体を消去することを決定するという、非常にまれなシナリオで技術的に問題につながる可能性があります。(お勧めしません)。

他に何かありますか?

上記のガイドでは、デバッグ組み込み関数に適用できるすべてのパターンを網羅しているわけではありません。 ガイドの冒頭で述べたように、一時的な対策として、ターゲットモジュールをデバッグレコードから組み込み関数に変換できます。デバッグ組み込み関数で実行できるほとんどの操作には、デバッグレコードとまったく同等のものがありますが、例外が発生した場合は、クラスのドキュメント(こちらにリンクされています)を読むと、いくつかの洞察が得られる場合があります。既存のコードベースに例がある場合があり、フォーラムでいつでも助けを求めることができます。