4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

[Swift] 恐怖!publicなのに呼び出せない関数!

Last updated at Posted at 2018-08-26

はじめに

以前に別の記事で、「publicである限り呼べない関数はない」と書いたのですが…。
publicでも呼び出せない関数がありそうなのです…。

呼べない関数…

それは次のようなコードです:

uncallable.swift
public protocol P {}
public protocol Q {}

extension P where Self: Q {
  public func uncallable() { print("\(#function) in P.") }
}

extension Q where Self: P {
  public func uncallable() { print("\(#function) in Q.") }
}

public struct S: P, Q {}

S().uncallable() // ⛔️ error: ambiguous use of 'uncallable()'

「プロトコルPに準拠していて且つQにも準拠している場合」に呼び出せるはずの関数と、「プロトコルQに準拠していて且つPにも準拠している場合」に呼び出せるはずの関数を、同名で定義すると、どちらの関数も呼び出せなくなってしまうのです!(当たり前)
でも、これ、S().uncallable()の行が無ければコンパイルは通ってしまうんですよね。warningぐらい出してくれてもいいような気がするのですが…。

意地でも呼ぶ!(良い子は真似しちゃダメ)

呼べないと思っても悪あがきはしたい!
ということで、まずはコンパイルが通るようにS().uncallable()は削除します:

uncallable.swift
public protocol P {}
public protocol Q {}

extension P where Self: Q {
  public func uncallable() { print("\(#function) in P.") }
}

extension Q where Self: P {
  public func uncallable() { print("\(#function) in Q.") }
}

public struct S: P, Q {}

さて、Swiftのコード上では区別して呼び出せない1uncallable()ですが、これでコンパイルが通るということは、それぞれの関数は別々の名前にマングリングされているはずです2

そこに一縷の望みを託しましょう!

どんな風にマングリングされているのかな?

Appleのドキュメントにマングリング規則が載ってるよ!3
ただし、Swift 4.x現在、ABIはunstableなので、今後変更される可能性があり注意が必要です。今回は、Swift 4.2(Apple Swift version 4.2 (swiftlang-1000.0.29.2 clang-1000.10.39))で試してみました。違うバージョンではマングリングされた名前が違うかもしれません。

何はさておきコンパイル

$ swiftc uncallable.swift -ouncallable

これでuncallableという実行可能ファイルが出来上がります。ちなみに、ライブラリとしてコンパイルするとモジュール名などを含んでマングリングされるので、そうしたい場合は今後の記述の該当部分を都度読み替えてください。

マングリングを確認

念のためnmを使ってシンボルがどうなっているか確認します。自分の環境では次のようになりました:

$ nm uncallable
00000001000010a0 S _$S10uncallable1PMp
0000000100000a70 T _$S10uncallable1PPA2A1QRzrlEAAyyF
0000000100000f34 s _$S10uncallable1P_pMF
00000001000010f8 S _$S10uncallable1QMp
0000000100000c00 T _$S10uncallable1QPA2A1PRzrlEAAyyF
0000000100000f44 s _$S10uncallable1Q_pMF
0000000100001070 S _$S10uncallable1SVAA1PAAMc
0000000100001080 S _$S10uncallable1SVAA1PAAWP
0000000100000da0 T _$S10uncallable1SVAA1PAAWa
0000000100001088 S _$S10uncallable1SVAA1QAAMc
0000000100001098 S _$S10uncallable1SVAA1QAAWP
0000000100000db0 T _$S10uncallable1SVAA1QAAWa
0000000100000d90 t _$S10uncallable1SVACycfC
0000000100000f54 s _$S10uncallable1SVMF
0000000100000dc0 T _$S10uncallable1SVMa
0000000100001150 s _$S10uncallable1SVMf
0000000100000f80 S _$S10uncallable1SVMn
0000000100001158 S _$S10uncallable1SVN
0000000100000f70 s _$S10uncallableMXM
                 U _$SSS19stringInterpolationS2Sd_tcfC
                 U _$SSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC
                 U _$SSS26_builtinUTF16StringLiteral18utf16CodeUnitCountSSBp_BwtcfC
                 U _$SSS26stringInterpolationSegmentSSx_tcs23CustomStringConvertibleRzs20TextOutputStreamableRzlufCSS_Tg5
                 U _$SSSN
                 U _$Ss27_allocateUninitializedArrayySayxG_BptBwlFSS_Tg5
                 U _$Ss27_allocateUninitializedArrayySayxG_BptBwlFyp_Tg5
                 U _$Ss5print_9separator10terminatoryypd_S2StF
                 U _$Ss5print_9separator10terminatoryypd_S2StFfA0_
                 U _$Ss5print_9separator10terminatoryypd_S2StFfA1_
                 U _$SytWV
0000000100000f98 s ___swift_reflection_version
0000000100000000 T __mh_execute_header
0000000100000a60 T _main
                 U _swift_bridgeObjectRelease
                 U _swift_bridgeObjectRetain
0000000100000f2e s _symbolic ____ 10uncallable1SV
0000000100000f0a s _symbolic $S10uncallable1PP
0000000100000f1c s _symbolic $S10uncallable1QP
                 U dyld_stub_binder

$Sから始まっているものは全てSwiftに関する型や関数などがマングリングされたものです。
このうち、結論から言うと、$S10uncallable1PPA2A1QRzrlEAAyyFextension P where Self: Quncallable()で、$S10uncallable1QPA2A1PRzrlEAAyyFextension Q where Self: Puncallable()です。

直接呼べるか試してみよう!(このステップは省略可)

まずは、みんな大好きC言語で呼び出しを試みましょう。"wanna_call.c"というファイルを用意します。dlopenを使えば簡単4:

wanna_call.c
# include <dlfcn.h>

struct S {};
struct S dummy = {};

int main(int argc, char *argv[]) {
  void *handle = dlopen("./uncallable", RTLD_NOW);
  if (handle == 0) { return 1; }
  
  void (*uncallable_in_P)(struct S) = dlsym(handle, "$S10uncallable1PPA2A1QRzrlEAAyyF");
  uncallable_in_P(dummy);
  
  void (*uncallable_in_Q)(struct S) = dlsym(handle, "$S10uncallable1QPA2A1PRzrlEAAyyF");
  uncallable_in_Q(dummy);
  
  dlclose(handle);
  return 0;
}

dlopenで実行可能ファイルuncallableをライブラリとして読み込んで、dlsymを使ってそれぞれマングリングされた名前で関数ポインタを取得し、呼び出します5

これをコンパイルして…

$ clang wanna_call.c -owanna_call

実行すれば…

$ ./wanna_call 
uncallable() in P.
uncallable() in Q.

できました!
C言語からSwiftの"uncallable"な関数をcallできました!

Swiftで直接呼ぶ!

Swiftでマングリングされた関数を直接呼ぶ方法があるのか?
それが、あるのです。
昔の記事でも紹介した@_silgen_nameというattributeを使います。
@_silgen_nameを使えば、シンボル名を直接指定することができるのです。
具体的には、"uncallable.swift"を次のようにしてみましょう:

uncallable.swift
public protocol P {}
public protocol Q {}

extension P where Self:Q {
  public func uncallable() { print("\(#function) in P.") }
}

extension Q where Self:P {
  public func uncallable() { print("\(#function) in Q.") }
}

public struct S: P, Q {}

@_silgen_name("$S10uncallable1PPA2A1QRzrlEAAyyF")
public func uncallableInP<T>(_ obj:T) -> Void where T:P & Q

@_silgen_name("$S10uncallable1QPA2A1PRzrlEAAyyF")
public func uncallableInQ<T>(_ obj:T) -> Void where T:Q & P

uncallableInP(S())
uncallableInQ(S())

こうすることで、func uncallableInP(_:)$S10uncallable1PPA2A1QRzrlEAAyyFというシンボルの関数を指すことになり、func uncallableInQ(_:)$S10uncallable1QPA2A1PRzrlEAAyyFというシンボルの関数を指すことになります。
なお、@_silgen_nameのattributeを付けた関数は本体を書くこともできますが、今回は別に本体があるので、もちろん省略します。
あとはそれぞれSのインスタンスをuncallableInP(_:)uncallableInQ(_:)の引数にして呼び出せば完璧です。まぁ、このコードなら何を渡してもいいんですが。

このコードで、コンパイルをし直して…

$ swiftc uncallable.swift -ouncallable

実行すれば…

$ ./uncallable
uncallable() in P.
uncallable() in Q.

できました!
でも、これ、モジュール名を付けたり変えたりするたびにシンボル名も変更しなきゃいけないので、常用はできないですね。

おわりに

自分で「呼べない関数」を作って、最後には呼んでしまうという企画倒れ的な記事でしたが、Swiftも頑張ればそれなりに自由な設計ができそうだということが実感できましたよ。
…そもそも最初のuncallable()が本当に普通に呼び出せないのかどうかが怪しいのですが。


  1. 区別できるような方法があれば教えていただけると幸いです。

  2. マングリングが同名になる(衝突する)とSwiftコンパイラはクラッシュするように設計されているようです。

  3. 記事執筆時点では多少不具合があったので直してもらいました(PR#19015)。
    ひとまず、マングリングされた名前を実際に見てみるところから始めましょう。

  4. dlopenの使い方は他のサイトをご覧ください。

  5. C言語の関数ポインタの宣言って、いつ見ても読みづらいですよね。あと、Swiftに慣れてしまってセミコロン;を書き忘れてしまうようになりました。

4
3
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?