hiragram / waiwai-swift-demangler

わいわいswiftc 番外編ワークショップ - 福岡

Home Page:https://iosdiscord.connpass.com/event/103252/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

SwiftDemangler

swift demangle のサブセットを作ります。

環境

  • Xcode 10.1
  • Swift 4.2.1

BNF

今回はもっともシンプルな関数+αのDemangleのみを扱います。 完全なドキュメントはSwiftレポジトリのdocs/ABI/Mangling.rstを参照してください。

Mangling

Swift4.2のPrefixのみサポートします。 このDemanglerが扱えるすべての名前にはPrefixとして$Sがつきます。

mangled-name ::= '$S'

Prefixあとからglobalが始まります。 今回は$S(モジュール名)(関数エンティティ) のもっともシンプルな形のみ扱います。

global ::= entity
entity ::= context entity-spec
context ::= module
module ::= identifier

Entity

今回は一部の非ジェネリックな関数のみ扱います。

entity-spec ::= decl-name label-list function-signature  'F'
function-signature ::= params-type params-type throws? // return and params
label-list ::= empty-list            // represents complete absence of parameter labels
label-list ::= ('_' | identifier)*
throws ::= 'K' 
params-type ::= type
decl-name ::= identifier
identifier ::= NATURAL IDENTIFIER-STRING

Identifier

identifier ::= NATURAL IDENTIFIER-STRING
identifier ::= '0' IDENTIFIER-PART

IDENTIFIER-PART ::= NATURAL IDENTIFIER-STRING
IDENTIFIER-PART ::= [a-z]
IDENTIFIER-PART ::= [A-Z]

IDENTIFIER-STRING ::= IDENTIFIER-START-CHAR IDENTIFIER-CHAR*
IDENTIFIER-START-CHAR ::= [_a-zA-Z]
IDENTIFIER-CHAR ::= [_$a-zA-Z0-9]

Substitutionは時間があれば扱います。

identifier ::= substitution
NATURAL ::= [1-9] [0-9]*
NATURAL_ZERO ::= [0-9]+

Type

Void, Int, Bool, String, Float, 型のリストと、 時間があればいくつかのユーザ定義型のみ扱います。

type ::= any-generic-type
any-generic-type ::= standard-substitutions
standard-substitutions ::= 'S' KNOWN-TYPE-KIND
KNOWN-TYPE-KIND ::= 'i' // Int
KNOWN-TYPE-KIND ::= 'b' // Bool
KNOWN-TYPE-KIND ::= 'S' // String
KNOWN-TYPE-KIND ::= 'f' // Float

type ::= type-list 't' 
type-list ::= list-type '_' list-type*
type-list ::= empty-list
empty-list ::= 'y' // Void
list-type ::= type

課題1

$S13ExampleNumber6isEven6numberSbSi_tF --—> ExampleNumber.isEven(number: Swift.Int) -> Swift.Bool

まずはもっとも基本的な関数のDemangleに挑戦してみましょう。 この関数のMangleされた名前のDemangleをします。 Examples/ExampleNumber.swift にコードがあります。

func isEven(number: Int) -> Bool {
    return number % 2 == 0
}

SILを出力してどのようにManglingされているか確認してみます。

$ swiftc -emit-sil Examples/ExampleNumber.swift

該当箇所を見つけてDemanglingしてみます。

$ swift demangle '$S13ExampleNumber6isEven6numberSbSi_tF'
$S13ExampleNumber6isEven6numberSbSi_tF ---> ExampleNumber.isEven(number: Swift.Int) -> Swift.Bool

--expand オプションを使うとどのような構成になっているかわかりやすいです。

$ swift demangle --expand '$S13ExampleNumber6isEven6numberSbSi_tF'
Demangling for $S13ExampleNumber6isEven6numberSbSi_tF
kind=Global
  kind=Function
    kind=Module, text="ExampleNumber"
    kind=Identifier, text="isEven"
    kind=LabelList
      kind=Identifier, text="number"
    kind=Type
      kind=FunctionType
        kind=ArgumentTuple, index=1
          kind=Type
            kind=Tuple
              kind=TupleElement
                kind=Type
                  kind=Structure
                    kind=Module, text="Swift"
                    kind=Identifier, text="Int"
        kind=ReturnType
          kind=Type
            kind=Structure
              kind=Module, text="Swift"
              kind=Identifier, text="Bool"
$S13ExampleNumber6isEven6numberSbSi_tF ---> ExampleNumber.isEven(number: Swift.Int) -> Swift.Bool

これと同じ動きをするようにDemanglerを実装してみましょう!

上記のBNFをみながらParserがかける人はここから先は自由に進めてもらって大丈夫です。

もしわからなければ以下の手順でやってみるとよいです。

Step1 - Prefix / Entityの種類を判別する

まずウォーミングアップとしてPrefixとSuffixを読んでみましょう。

  • 与えられたStringのPrefixが$Sであることを確認してBoolを返す isSwiftSymbol 関数
  • 与えられたStringのSuffixがFであることを確認してBoolを返す isFunctionEntitySpec 関数
let name = "$S13ExampleNumber6isEven6numberSbSi_tF"
isSwiftSymbol(name: name) // true
isFunctionEntitySpec(name: name) // true

Step2 - 簡易Parserを作って数字とその文字分の文字列読み取る機能を作る

Mangleされた名前の中には「ここから何文字分がIdentifierか」を表す数字が含まれています。 たとえば13ExampleNumber であればExampleNumber の13文字分が1つのIdentifierであることを表しています。

こんな感じで簡単なParserを作ってみましょう。

class Parser {
  private let name: String
  private var index: String.Index

  var remains: String { return String(name[index...]) }

  init(name: String) {
    self.name = name
    self.index = name.startIndex
  }
}

まずは、先頭から数字を読み取るメソッドを作ってみましょう。 正確には014などの0から始まるケースを弾く必要がありますが、今回は特に気にしなくても大丈夫です。 (もちろんやってもOKです)

extension Parser {
  func parseInt() -> Int? { ... }
}
 var parser = Parser(name: "0")

 // 0
 XCTAssertEqual(parser.parseInt(), 0)
 XCTAssertEqual(parser.remains, "")

 // 1
 parser = Parser(name: "1")
 XCTAssertEqual(parser.parseInt(), 1)
 XCTAssertEqual(parser.remains, "")

 // 12
 parser = Parser(name: "12")
 XCTAssertEqual(parser.parseInt(), 12)
 XCTAssertEqual(parser.remains, "")

 // 12
 parser = Parser(name: "12A")
 XCTAssertEqual(parser.parseInt(), 12)
 XCTAssertEqual(parser.remains, "A")

 // 1
 parser = Parser(name: "1B2A")
 XCTAssertEqual(parser.parseInt(), 1)
 XCTAssertEqual(parser.remains, "B2A")
 XCTAssertEqual(parser.parseInt(), nil)

数字が読み取れたら、今度はその文字数分identifierを読み取ってみましょう。

extension Parser {
  func parseIdentifier(lenght: Int) -> String { ... }
}
let parser = Parser(name: "3ABC4DEFG")

XCTAssertEqual(parser.parseInt(), 3)
XCTAssertEqual(parser.remains, "ABC4DEFG")
XCTAssertEqual(parser.parseIdentifier(length: 3), "ABC")
XCTAssertEqual(parser.remains, "4DEFG")

XCTAssertEqual(parser.parseInt(), 4)
XCTAssertEqual(parser.remains, "DEFG")
XCTAssertEqual(parser.parseIdentifier(length: 4), "DEFG")

あとは数字を読んでその文字数分Identifierを読むメソッドがあると便利そうです。

extension Parser {
  func parseIdentifier() -> String? { ... }
}
let parser = Parser(name: "3ABC4DEFG")
XCTAssertEqual(parser.parseIdentifier(), "ABC")
XCTAssertEqual(parser.remains, "4DEFG")
XCTAssertEqual(parser.parseIdentifier(), "DEFG")

Step3 - モジュール名を読みとる

ここまでできればモジュール名を読むのは簡単です。

Prefixを飛ばすためにparserPrefixを作っておきます。

extension Parser {
    func parsePrefix() -> String { ... }
}

今回扱う範囲ではPrefixの後にモジュール名がくるので、先ほど作ったparserIdentifier()を使って読み取ってあげればおしまいです。

extension Parser {
    func parseModule() -> String { ... }
}

今回の例であればExampleNumber が読み取れれば成功です。

let parser = Parser(name: "$S13ExampleNumber6isEven6numberSbSi_tF")
let _ = parser.parsePrefix()
XCTAssertEqual(parser.parseModule(), "ExampleNumber")

Step4 - 関数名と引数ラベルを読みとる

モジュール名のあとには関数を表すentity-specが続きます。

entity-spec ::= decl-name label-list function-signature  'F'

まずは 関数名isEvenにあたる decl-nameを読み取ってみましょう。 モジュール名と同様に先ほど作ったparserIdentifier() がそのまま使えます。

extension Parser {
  func parseDeclName() -> String { ... }
}

そのあとには引数のラベル名が続きます。今回はnumberというラベルが1つ付いているので6number と続いているのがわかるかと思います。

$S13ExampleNumber6isEven6numberSbSi_tF

これも同様にparserIdentifier() を使うだけですが、引数ラベルは複数ある可能性があるのでIdentifierを読み取れるだけ全部読み取る必要があります。

extension Parser {
  func parseLabelList() -> [String] { ... }
}

Step5 - 先読み/スキップ機能を作る

さてここから先は少し複雑になってきます。準備のためにParserにいくつかの機能を足してあげましょう。 indexを進めることなしに現在の先頭の文字を先読みして処理を分岐させるために、以下のような関数を作ってあげましょう。

extension Parser {
  // indexはそのままに一文字先読みする
  func peek() -> String { ... }
}

また、単純に与えられた文字数分スキップするskipメソッドも作ってあげましょう。

extension Parser {
  // length分だけindexを進める
  func skip(length: Int) { ... }
}

Step6 - 型を読み取る

ラベルの後には関数のシグネチャ(≒型) が続きます。 シグネチャを読む前にまずは型のParserを作りましょう。

今回は扱う型が限られているので、型を表すこんな感じのenumを作ってあげると良さそうです。

enum Type {
    case bool
    case int
    case string
    case float
    indirect case list([Type])
}

Swiftの基本的な型はstandard-substitutionsという省略形で表現されBool, IntはそれぞれSb, Siと表されています。

standard-substitutions ::= 'S' KNOWN-TYPE-KIND
KNOWN-TYPE-KIND ::= 'b' // Swift.Bool
KNOWN-TYPE-KIND ::= 'i' // Swift.Int

引数部分は型のリストで表す必要があるため、.listのケースを用意してあげています。

type ::= type-list 't' 
type-list ::= list-type '_' list-type*
type-list ::= empty-list
empty-list ::= 'y'

一つ目の要素のあとに_ がつき、 tがリストの終わりを表しています。たとえばもしisEvenがこのような定義だったとすると

func isEven(number: Int, hoge: String, fuga: Float) -> Bool { ... }

引数部分の型はこのように表されます。

Si_SSSft

一つ目の引数のIntを表すSiとここからリストを始めることを表す_, 引数を表すSS, Sfと続き、最後にリスト終了を表すtが書かれます。

まずlist以外のknownな型をparseするメソッドを生やしてあげると処理がシンプルになりそうです。

extension Parser {
  func parseKnownType() -> Type { ... }
}
XCTAssertEqual(Parser(name: "Si").parseKnownType(), .int)
XCTAssertEqual(Parser(name: "Sb").parseKnownType(), .bool)
XCTAssertEqual(Parser(name: "SS").parseKnownType(), .string)
XCTAssertEqual(Parser(name: "Sf").parseKnownType(), .float)

これと、先ほど作ったpeekやskipをうまく使いながらlistも対応した完全版を作ってあげます。

extension Parser {
  func parseType() -> Type { ... }
}
XCTAssertEqual(Parser(name: "Si").parseType(), .int)
XCTAssertEqual(Parser(name: "Sb").parseType(), .bool)
XCTAssertEqual(Parser(name: "SS").parseType(), .string)
XCTAssertEqual(Parser(name: "Sf").parseType(), .float)
XCTAssertEqual(Parser(name: "Sf_SfSft").parseType(), .list([.float, .float, .float]))

Step7 - 関数のシグネチャを読み取る

型のparseができるようになったところで関数のシグネチャを読み取ってみましょう。

function-signature ::= params-type params-type throws?

具体的にはこの部分です。

SbSi_t

まず返り値の型があり、そのあとに引数の型が続きます。 今回のisEvenであれば Bool, (Int) という並びで書かれているはずです。

引数の部分はIntではなく(Int) という要素数1のlistで表現されているためSi_tのようになっています。

Parserを書く前に、function-signatureを表す型を作ってあげましょう。

struct FunctionSignature: Equatable {
    let returnType: Type
    let argsType: Type
}

あとは先ほど作ったparseTypeを使って型を2つ読んであげるだけです。

extension Parser {
    func parseFunctionSignature() -> FunctionSignature { ... }
}
XCTAssertEqual(Parser(name: "SbSi_t").parseFunctionSignature(), FunctionSignature(returnType: .bool, argsType: .list([.int])))

Step8 - Parserを完成させる

あとは全体を読んであげましょう。

struct FunctionEntity: Equatable {
    let module: String
    let declName: String
    let labelList: [String]
    let functionSignature: FunctionSignature
}
extension Parser {
    func parseFunctionEntity() -> FunctionEntity { ... }
}

これでほぼ完成です!!

let sig = FunctionSignature(returnType: .bool, argsType: .list([.int]))
XCTAssertEqual(Parser(name: "13ExampleNumber6isEven6numberSbSi_tF").parseFunctionEntity(),
               FunctionEntity(module: "ExampleNumber", declName: "isEven", labelList: ["number"], functionSignature: sig)

あとはPrefixだけ飛ばしてあげればOKです。

extension Parser {
    func parse() -> FunctionEntity {
        let _ = self.parsePrefix()
        return self.parseFunctionEntity()
    }
}

Step9 - 文字列として表示する

ここは必須ではないですが、swift demangleの出力と同じように

$S13ExampleNumber6isEven6numberSbSi_tF ---> ExampleNumber.isEven(number: Swift.Int) -> Swift.Bool

こんな感じで見せられるとかっこよさそうです。特に解説はしないのでチャレンジしてみてください。

課題2

課題1が終わったら応用編にチャレンジしてみましょう!

  • 引数が複数ある場合でもちゃんとうごくか確認する

    func isEven(number: Int, hoge: String, fuga: Float) -> Bool { ... }
  • throws な関数を扱ってみる

    • SILの出力やBNFをみながらどこを変えればよいか調べながらやってみる
    func isEven(number: Int) throws -> Bool { ... }
  • 引数や返り値の型が Voidな関数を扱ってみる

    • SILの出力やBNFをみながらどこを変えればよいか調べながらやってみる

課題3

ユーザ定義のstructと、そのメソッドについて扱えるようにしてみたいと思います。 コードはExamples/ExampleAnimal.swiftにあります。

struct Dog {
  func bark() -> String { 
    return "わんわん"
  }
}

このメソッドをMangleしてみると

$S13ExampleAnimal3DogV4barkSSyF

となっていて3DogVというstructを表す部分が追加されています。

context ::= entity
entity ::= nominal-type
any-generic-type ::= context decl-name 'V' // nominal struct type

enum / classにはそれぞれ O, C がつきます。

any-generic-type ::= context decl-name 'O' // nominal enum type
any-generic-type ::= context decl-name 'C' // nominal class type
  • Dog.bark()の例でDemanglerが動くように実装してみましょう
$S13ExampleAnimal3DogV4barkSSyF ---> ExampleAnimal.Dog.bark() -> Swift.String

課題4

ここからはMangleされた文字列をできるだけ短くする手法について紹介していきます。 コードはExamples/ExampleSquare.swiftにあります。 まずは繰り返しの省略です。

func square(number: Int) -> Int {
    return number * number
}

こんな関数があった場合、課題1までの知識で素直にMangleしてみると

$S13ExampleSquare6square6numberSiSi_tF

となりそうですが、実際はシグネチャの部分が少し異なります。

$S13ExampleSquare6square6numberS2i_tF

これはSiSiという繰り返しがS2i と省略されたためです。

standard-substitutions ::= 'S' NATURAL KNOWN-TYPE-KIND
  • square(number:)の例でDemanglerが動くように実装してみましょう
$S13ExampleSquare6square1nS2i_tF ---> ExampleSquare.square(n: Swift.Int) -> Swift.Int

課題5

ここまで触れてこなかった Substitution について扱います。 コードはExamples/ExampleSub.swiftにあります。

struct Water { }

struct Stone { 
    func hogehoge(aaa: Stone, bbb: Water, ccc: Stone) -> Water {
        fatalError()
    }
}

これをMangleすると、繰り返し出てくるStoneWater が置換されて AAACなどになっていることが確認できます。

$S10ExampleSub5StoneV8hogehoge3aaa3bbb3cccAA5WaterVAC_AiCtF

Manglingの過程で出現した文字列は配列に保存されます。 最初のA で置換を開始する合図で、次の大文字アルファベットまで置換が繰り返されます。

詳細はおもちさんのスライドのこのあたりをみてみてください。

see: https://speakerdeck.com/omochi/swiftcfalsemanglingtosubstitution?slide=15

  • Substitutionを実装してみましょう。
$S10ExampleSub5StoneV8hogehoge3aaa3bbb3cccAA5WaterVAC_AiCtF ---> ExampleSub.Stone.hogehoge(aaa: ExampleSub.Stone, bbb: ExampleSub.Water, ccc: ExampleSub.Stone) -> ExampleSub.Water

その後

docs/ABI/Mangling.rstを見ながら君だけの最高のDemanglerを作ろう!

About

わいわいswiftc 番外編ワークショップ - 福岡

https://iosdiscord.connpass.com/event/103252/


Languages

Language:Swift 100.0%