Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

An Objective-C simulator written in Swift.

NotificationsYou must be signed in to change notification settings

NSExceptional/Runtime

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

An Objective-C simulator written in Swift.

Goals

With few exceptions, this project aims to simulate, in Swift, how Objective-C works under the hood (i.e. calls toobjc_msgSend, inserted ARC functions, literal class-refs in class method calls, etc), as opposed to mirroring Objective-C style code and dynamism which Swift can accomplish already via@objc classes.

This project could theoretically be used as a dynamic runtime backend for a transpiled progamming language, and as such, this framework and its conventions were crafted with this idea in mind. Many of the constructs used here may seem to lack type-safety, but everything is perfectly safe if the code is generated by some other, more type-safe language. In short, this code is not meant to be written by hand if used for anything serious.

Features

  • Dynamic method dispatch
  • Method swizzling / replacing
  • Creating entire classes at runtime
  • Non-fragile ivars

SeePerson.Swift for an examples of everything mentioned in the readme.

Overview

Runtime metadata types provided by this framework mirrors that of the public Objective-C runtime interface as closely as possible, declaring types such asClass,Ivar,Method, etc, all of which provide about as much information as their Objective-C counterparts.

Defining classes

A base class,RootObject, is provided for other classes to inherit from if they wish. New classes are defined by declaring astruct type to enclose theClass object in, with the class object itself being declared as astatic let, followed by method variables.

structPerson{staticlet`class`=Class(        isa:Person_meta.class,        superclass:RootObject.class,        name:"Person",        ivars:[(name:"_name", type:.string),(name:"_age", type:.integer)],        methods:[_init, name, setName_, age, setAge_, description],        properties:[Property(name:"name", getter: name, setter: setName_),Property(name:"age", getter: age, setter: setAge_),],        protocols:[])        // Methods go here as static varsstaticvar_init=Method("init", returns:.object("this")){ this, _cmd, argsinfunc init$(_ this:id, _ _cmd:SEL)->id{_msgSend(this,"setName_",("Bob"))_msgSend(this,"setAge_",(18))returnmsgSend(super: true, this, _cmd)}returninit$(this, _cmd)}staticvarname=Method("name", returns:.string)......}privatestructPerson_meta{staticlet`class`=Class(        isa:nil,        superclass:nil,        name:"Person.meta",...)}

It is good practice to declare a struct for the class itself and another for the metaclass, as above, to reduce ambiguity between class members and instance members (methods, properties, etc). The metaclass stores class members.

isa: should be the class's metaclass (ornil if the class is a metaclass itself).superclass: should be the superclass.

The Metaclass

Metaclasses inherit from the super-metaclass, not the superclass. It is convention to declare the compile-time variable likeMyClass_meta and name it"MyClass.meta". So,Person inherits fromObject.class, andPerson_meta inherits fromObject_meta.class.

Each metaclass can be looked up by usingClass.named("Foo").isa or directly by name withClass.named("Foo.meta").

Methods

Declaration

Methods should be defined asstatic var/let as well (as opposed to right inside themethods: argument to the.class initializer as I have done withproperties:), in case you need to reference the method as an argument to aProperty at compile-time. Declaring them inline also makes the initializer very hard to parse visually since method declarations are typically no less than 7 or 8 lines.

Method.init() structure

TheMethod initializer takes the name of the method, the return and argument types (Type) an implementation (IMP). The return and argument types default to.void and[]. For initializers, it is convention to return.object("self") where you would useinstancetype in Objective-C. You could use.object("anything you want"), but I find that"self" makes the most sense here. In cases where you return another object of a fixed type, use.object("ClassName"). This runtime aims to provide as much metadata for method type signatures as Objective-C does for property type signatures.

IMP arguments

Like Objective-C, all methods take two fixed arguments:this in place ofself, and_cmd. However, due to limitations in the Swift type system, all methodIMPs must return the same thing,Any, and without using assembly, they must all takeAny as the variable arguments, even if a method takes no other arguments. AnIMP is invoked by passingthis,_cmd, andargs whereargs is a tuple of the non-fixed arguments to the method.

Implementation conventions

To counteract the lack of type safety and enhance readability, I find it helpful to declare a function within the scope of the methodIMP named with a traling$ to represent the actual type signature of the method (and to hold the non-trivial implementation), like so:

staticvaradd__=Method(){ this, _cmd, argsin    // Actual implementation and type signature of methodfunc add__$(_ this:id, _ _cmd:SEL, a:Int, b:Int)->Int{return a+ b}            // Cast out arguments and call methodletargs= argsas!(Int,Int)returnadd__$(this, _cmd, args.0, args.1)}

Arguments must be cast fromAny to their actual types as a tuple before being used.

Overriding methods

To override a method, simply give your subclass another method with the same name as the method you wish to override. If you need to call thesuper implementation, simply passsuper: true to your call tomsgSend:

staticvar_init=Method("init",...){ this, _cmd, argsinfunc init$(_ this:id, _ _cmd:SEL)->id{returnmsgSend(super: true, this, _cmd)print("init override:\(this)")}returninit$(this, _cmd)}
Init

If you're familiar with Swift, you may know that Swift doesn't allow you to useself before all ivars have been initialized. With some exceptions, the same is true here. That said, all ivars are initialized to0 ornil, so it is not necessary to initialize primitive integral types tonil or0.

Technically, if a class has no stored complex Swift structures in it (such asString), it should be safe to use prior to ivar initialization. I plan to make a wrapper forString andArray, etc, to counteract these edge cases.

Instance variables

Ivars are passed to theClass initializer as a tuple of their name and type. Their offset is detremined at runtime, and as a result, classes do not have fragile ivars.

Metaclasses can not have any instance variables; trying to use ivars on a metaclass is undefined behavior.

Properties

Properties take a name and one or two implementations. A property'stype comes from itsgetter.

--

Creating objects

Instances of objects are allocated by callingclass.createInstance(), i.e.:

letinstance1=Person.class.createInstance()letinstance2=Class.named("Person").createInstance()

Calling methods

Like Objective-C, this runtime uses dynamic dispatch via themsgSend and_msgSend functions._msgSend only exists as a shortcut for void-returning methods, or cases where you want to discard the return value.

letbob:id=msgSend(Person.class.createInstance(),"init")letname:String=msgSend(bob,"name")letage:Int=msgSend(bob,"age")letdescription:String=msgSend(bob,"description")

Accessing ivars

Ivar access works similarly to how it works in Objective-C. You must retrieve the offset from the runtime and add it tothis to access the ivar. A lot of casting is involved, and I've provided some operators to ease the pain:

letoffset= this|.getClass.getIvarOffset("_someInt")!letpointer:Pointer<Int>=~pointer+ offsetletivarValue= pointer.pointee

this| is shorthand forthis.pointee.~pointer is shorthand forunsafeBitCast(pointer, to: T.self). Note that the runtime uses its ownPointer type, which allows+ to offset it by bytes at at time.

The above is still pretty convoluted and heavily repeated, so I've provided yet another operator which returnsivarValue above:

letivarValue:Int= this|"_someInt"

In general,| provides some form of dereferencing an object pointer. Here is another operator which can be used to set an ivar_foo to5:

this|=(5,"_foo")

--

Type system "gotchas"

You're stuck withid

Since new classes are weakly defined as runtime metadata and not as concrete types in Swift code, you cannot declare aPointer to a custom type directly. That is, all object references are typed asPointer<Object> akaid, as defined byObject.swift (not to be confused withRootObject, which is akin toNSObject).

If you really want to declare aPointer<Vehicle> for example, you could declare members on yourVehicle struct like so, alongside thestatic let class declaration:

structVehicle{let_super:Objectlet_capacity:Int...staticlet`class`=Class(isa:...)}/// Vehicle subclassstructCar{let_super:Vehicleletmake:Stringletmodel:Stringletyear:Int...staticlet`class`=Class(isa:...)}

Now, you could possibly do the following:

letfiesta:Pointer<Car>=msgSend(Car.class.createInstance(),"init",("Ford","Fiesta",2014,...))fiesta.year=2017

Be sure to continue to declare all ivars and methods inside theClass variable. Statically declaring the layout like this is only useful for extra type-safety and direct ivar access if you wish to bypass non-fragile ivar lookup.

UsingClasses as objects

Class instances could only be made possible by makingClass a Swiftclass and not astruct, due to limitations in Swift's type system and several abstractions Swift imposes on the user. Therefore, they do not have the same underlying structure asObject does (that is,Class does not start with theisa defined by theObject declaration). To call a class method on a class, pass.ref asthis:

_msgSend(Person.class.ref,"someClassMethod")

In general, useclass.ref whenever you wish to treat aClass as an object.

Other caveats

Class objects will not be available viaClass.named(_:) until they have been accessed statically. You should "load" these classes manually by accessing all classes you define, like so:

func runtimeInit(){    // Runtime initialization    _=RootObject.class    _=Person.class...}

Ideally this shouldn't be necessary, or should be easier. Please submit a pull request if you have suggestions on how to make this easier or unnecessary!


To-do

  • More tests
  • Zeroing deallocated references
  • Suggestions welcome!

About

An Objective-C simulator written in Swift.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

 

Packages

No packages published

Contributors2

  •  
  •  

Languages


[8]ページ先頭

©2009-2025 Movatter.jp