Skip to content

Notes: Swift runtime enablement

Vladimir Mutafov edited this page May 7, 2019 · 2 revisions

Goal

The iOS Runtime should be able to access Swift classes, get/set their properties and call their functions without them being accessible from the ObjC runtime.

Swift binding

Unlike Objective-C which methods are resolved at runtime (dynamic dispatch) Swift has implemented different types of method dispatch (https://www.raizlabs.com/dev/2016/12/swift-method-dispatch/). Swift classes have a vtable member which lists available methods in the class. Vtables are created at compile time and contain function pointers accessed by index. The compiler uses that lookup table to translate method calls to appropriate function pointers and with enough information eliminates the dynamic dispatch making the call direct and this faster. This is why Swift symbols are mangled - the mangled name contains not just the symbol name but also some metadata - the types of parameters etc. (Great article on Swift mangling - https://www.mikeash.com/pyblog/friday-qa-2014-08-15-swift-name-mangling.html)

Now we know we can’t just use a function’s name to call it runtime from another language. Knowing that we have the symbol is loaded (.swift files are compiled and linked), we can find the function pointer by the symbol’s mangled name. For example:

I created the following class (SwiftClass.swift):

class SwiftClass {
	func swiftMethod() {
	  print(“this is a swift print”)
        }
}

I found the function’s mangled by running nm -a on the SwiftClass.o (outputted by the compiler - you can see its full path in the build log in XCode) - _T06SwiftC0A5ClassC11swiftMethodyyF

Then in the same XCode project (for the sake of simplycity so I have the swift files compiled by default) but in an Objc file (.m) I did the following:

    char *buf = "_T06SwiftC0A5ClassC11swiftMethodyyF";
    void *hndl = dlopen (NULL, RTLD_LAZY);
    void (*fptr) (void) = dlsym (hndl, buf);
    fptr();

And swiftMethod() was called.

The same approach can be used with libffi both for methods and for properties (via setters and getters which mangled symbols may as well be expected). This is the officla mangling guide from the swift repo - https://github.com/apple/swift/blob/master/docs/ABI/Mangling.rst .

Metadata generation

I researched 4 possible ways of creating metadata for swift classes:

Option 1 - SourceKit + C in current metadata generator

SourceKit is written in C and it can be easily used in the metadata-generator. It’s used by XCode for the most of the IDE features as well as embedded by some swift frameworks as SourceKitten for linters, IDE features and metaprogramming. Looks like the best option so far. SourceKit is one of the system libraries and all we need is to link it and add it’s header to the project (https://github.com/apple/swift/blob/master/tools/SourceKit/tools/sourcekitd/include/sourcekitd/sourcekitd.h)

This is the result I had with SourceKit for a SwiftClass I created:

SwiftClass.swift

class SwiftClass: NSObject {
	let prop:Int = 0
}

OUTPUT:

{
  key.offset: 0,
  key.length: 49,
  key.diagnostic_stage: source.diagnostic.stage.swift.parse,
  key.syntaxmap: [
    {
      key.kind: source.lang.swift.syntaxtype.keyword,
      key.offset: 0,
      key.length: 5
    },
    {
      key.kind: source.lang.swift.syntaxtype.identifier,
      key.offset: 6,
      key.length: 10
    },
    {
      key.kind: source.lang.swift.syntaxtype.typeidentifier,
      key.offset: 18,
      key.length: 8
    },
    {
      key.kind: source.lang.swift.syntaxtype.keyword,
      key.offset: 30,
      key.length: 3
    },
    {
      key.kind: source.lang.swift.syntaxtype.identifier,
      key.offset: 34,
      key.length: 4
    },
    {
      key.kind: source.lang.swift.syntaxtype.typeidentifier,
      key.offset: 39,
      key.length: 3
    },
    {
      key.kind: source.lang.swift.syntaxtype.number,
      key.offset: 45,
      key.length: 1
    }
  ],
  key.substructure: [
    {
      key.kind: source.lang.swift.decl.class,
      key.accessibility: source.lang.swift.accessibility.internal,
      key.name: "SwiftClass",
      key.offset: 0,
      key.length: 48,
      key.runtime_name: "_TtC9SwiftTest10SwiftClass",
      key.nameoffset: 6,
      key.namelength: 10,
      key.bodyoffset: 28,
      key.bodylength: 19,
      key.inheritedtypes: [
        {
          key.name: "NSObject"
        }
      ],
      key.elements: [
        {
          key.kind: source.lang.swift.structure.elem.typeref,
          key.offset: 18,
          key.length: 8
        }
      ],
      key.substructure: [
        {
          key.kind: source.lang.swift.decl.var.instance,
          key.accessibility: source.lang.swift.accessibility.internal,
          key.name: "prop",
          key.offset: 30,
          key.length: 16,
          key.typename: "Int",
          key.nameoffset: 34,
          key.namelength: 4
        }
      ]
    }
  ]
}

I created a sample project which shows how sourcekit can be used - https://github.com/tdermendjiev/SwiftMetadataGenerator .

With that in mind we can create a new recursive ast visitor similar to the clang’s one.

  1. swift-ast https://github.com/yanagiba/swift-ast

I found a swift library providing similar features to the clang’s front end (RecursiveASTVisitor etc.) The metadata generator would have to be written in swift though.

  1. Calling swiftc Calling the swift compiler front end from command line may also give us the AST of the parsed swift file.

  2. Embedding swift-clang Embedding the swift’s parser should provide us with the similar functionality we use in the current metadata-generator (it’s again clang but with the notions of swift’s Decl-s).

Important note: I haven’t digged deep enough in options 2,3 and 4 because sourcekit looked way easier to be embedded and used than the others. A great reference for working with sourcekit is SourceKitten’s code - https://github.com/jpsim/SourceKitten .

Known limitations:

Exending Swift classes

Initially users won’t be able to extend swift classes because of the static binding. @vmutafov proposed a solution similar to the static binding generator in the android-runtime, i.e. generating swift classes/files before compiling using soucekit LSP - https://github.com/apple/sourcekit-lsp

Useful links:

https://www.slideshare.net/InfoQ/swift-under-the-hood

https://forums.swift.org/t/best-way-to-call-a-swift-function-from-c/9829

https://medium.com/@MarcioK/swift-ruby-interoperability-9a0ce9a70fd2

https://www.apteligent.com/technical-resource/the-complete-guide-to-function-mangling-in-ios/

https://stackoverflow.com/questions/24245262/call-a-method-from-a-string-in-swift

http://eswick.com/blog.html?post=2

https://ericasadun.com/2015/05/24/swift-so-that-whole-sourcekit-thing/

https://forums.swift.org/t/sourcekit-lsp-errors-trying-to-find-primary-sourcefile/18822

https://www.mikeash.com/pyblog/friday-qa-2014-08-15-swift-name-mangling.html

https://github.com/apple/swift/blob/master/docs/ABI/Mangling.rst