Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for the GNUstep Objective-C runtime #27

Merged
merged 1 commit into from
Nov 29, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,31 @@
language: rust
rust:
- stable
- beta
- nightly
sudo: false
install:
- if [ "$TRAVIS_OS_NAME" = "linux" ]; then export FEATURE="gnustep"; export CC="clang"; export CXX="clang++"; else export FEATURE=""; fi
- if [[ "$FEATURE" == *"gnustep"* ]]; then git clone https:/gnustep/libobjc2.git; fi
- if [[ "$FEATURE" == *"gnustep"* ]]; then mkdir libobjc2/build; pushd libobjc2/build; fi
- if [[ "$FEATURE" == *"gnustep"* ]]; then cmake -DCMAKE_INSTALL_PREFIX:PATH=$HOME/libobjc2_staging ../; fi
- if [[ "$FEATURE" == *"gnustep"* ]]; then make install; fi
- if [[ "$FEATURE" == *"gnustep"* ]]; then export CPATH=$HOME/libobjc2_staging/include:$CPATH; export LIBRARY_PATH=$HOME/libobjc2_staging/lib:$LIBRARY_PATH; LD_LIBRARY_PATH=$HOME/libobjc2_staging/lib:$LD_LIBRARY_PATH; fi
- if [[ "$FEATURE" == *"gnustep"* ]]; then popd; fi
- if [ -n "$FEATURE" ]; then export FEATURES="--features $FEATURE"; else export FEATURES=""; fi;
script:
- cargo build $FEATURES
- cargo test $FEATURES
- cargo doc $FEATURES
env:
notifications:
email:
on_success: never
os:
- linux
- osx
addons:
apt:
packages:
- clang-3.7
- cmake
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,20 @@ repository = "http:/SSheldon/rust-objc"
documentation = "http://ssheldon.github.io/rust-objc/objc/"
license = "MIT"

exclude = [".gitignore", ".travis.yml", "ios-tests/**", "xtests/**"]
exclude = [".gitignore", "ios-tests/**", "xtests/**", ".travis.yml"]

[features]
exception = ["objc_exception"]
verify_message = []
gnustep = [ "test_ns_object/gnustep" ]

[dependencies]
malloc_buf = "0.0"

[dependencies.objc_exception]
version = "0.1"
optional = true

[dev-dependencies.test_ns_object]
version = "0.0"
path = "test_utils"
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,10 @@ will unwind into Rust resulting in unsafe, undefined behavior.
However, this crate has an `"exception"` feature which, when enabled, wraps
each `msg_send!` in a `@try`/`@catch` and panics if an exception is caught,
preventing Objective-C from unwinding into Rust.

## Support for other Operating Systems

The bindings can be used on Linux or *BSD utilizing the
[GNUstep Objective-C runtime](https://github.com/gnustep/libobjc2).
To enable it, you need to pass the required feature to cargo:
`cargo build --feature gnustep`.
2 changes: 1 addition & 1 deletion src/declare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ methods can then be added before the class is ultimately registered.
The following example demonstrates declaring a class named `MyNumber` that has
one ivar, a `u32` named `_number` and a `number` method that returns it:

```
```no_run
# #[macro_use] extern crate objc;
# use objc::declare::ClassDecl;
# use objc::runtime::{Class, Object, Sel};
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Objective-C Runtime bindings and wrapper for Rust.

Objective-C objects can be messaged using the [`msg_send!`](macro.msg_send!.html) macro:

```
```no_run
# #[macro_use] extern crate objc;
# use objc::runtime::{BOOL, Class, Object};
# fn main() {
Expand Down
48 changes: 42 additions & 6 deletions src/message.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::any::Any;
use std::mem;

use runtime::{Class, Object, Sel, Super, self};

/// Types that may be sent Objective-C messages.
Expand Down Expand Up @@ -31,7 +30,7 @@ fn msg_send_fn<R: Any>() -> unsafe extern fn(*mut Object, Sel, ...) -> R {
}
}

#[cfg(target_arch = "x86")]
#[cfg(all(target_arch = "x86", not(feature = "gnustep")))]
fn msg_send_super_fn<R: Any>() -> unsafe extern fn(*mut Object, Sel, ...) -> R {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this have to be cfg'd out? It didn't look like any runtime methods were removed in gnustep, so I'd expect it to still compile.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GNUstep runtime doesn't implement sending messages to super via objc_msgSendSuper. The GNUstep-specific implementation for sending messages to super is at ll. 182–194.

let size = mem::size_of::<R>();
if size == 0 || size == 1 || size == 2 || size == 4 || size == 8 {
Expand All @@ -55,7 +54,7 @@ fn msg_send_fn<R>() -> unsafe extern fn(*mut Object, Sel, ...) -> R {
}
}

#[cfg(target_arch = "x86_64")]
#[cfg(all(target_arch = "x86_64", not(feature = "gnustep")))]
fn msg_send_super_fn<R>() -> unsafe extern fn(*const Super, Sel, ...) -> R {
if mem::size_of::<R>() <= 16 {
unsafe { mem::transmute(runtime::objc_msgSendSuper) }
Expand Down Expand Up @@ -83,7 +82,7 @@ fn msg_send_fn<R: Any>() -> unsafe extern fn(*mut Object, Sel, ...) -> R {
}
}

#[cfg(target_arch = "arm")]
#[cfg(all(target_arch = "arm", not(feature = "gnustep")))]
fn msg_send_super_fn<R: Any>() -> unsafe extern fn(*mut Object, Sel, ...) -> R {
use std::any::TypeId;

Expand All @@ -98,15 +97,15 @@ fn msg_send_super_fn<R: Any>() -> unsafe extern fn(*mut Object, Sel, ...) -> R {
}
}

#[cfg(target_arch = "aarch64")]
#[cfg(all(target_arch = "aarch64", not(feature = "gnustep")))]
fn msg_send_fn<R>() -> unsafe extern fn(*mut Object, Sel, ...) -> R {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

None of the other msg_send_fn's are cfg'd out, how come only this one is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

objc_msgSend needs to be implemented as platform specific assembly. The GNUstep runtime has implementations for the following architectures:

  • x86
  • am64
  • arm
  • mips

We don't have an implementation for aarch64, so we don't want to define it here. Also, a mips implementation is still missing because I didn't have the time to figure out the calling conventions (specifically when to use objc_msgSend_stret())

// stret is not even available in arm64.
// https://twitter.com/gparker/status/378079715824660480

unsafe { mem::transmute(runtime::objc_msgSend) }
}

#[cfg(target_arch = "aarch64")]
#[cfg(all(target_arch = "aarch64", not(feature ="gnustep")))]
fn msg_send_super_fn<R>() -> unsafe extern fn(*const Super, Sel, ...) -> R {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aarch64 was changed to arm here, was that intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, this should still be aarch64.

unsafe { mem::transmute(runtime::objc_msgSendSuper) }
}
Expand Down Expand Up @@ -134,6 +133,10 @@ pub trait MessageArguments {
macro_rules! message_args_impl {
($($a:ident : $t:ident),*) => (
impl<$($t),*> MessageArguments for ($($t,)*) {
#[cfg(any(not(feature="gnustep"),
any(target_arch = "arm",
target_arch = "x86",
target_arch = "x86_64")))]
unsafe fn send<T, R>(self, obj: *mut T, sel: Sel) -> R
where T: Message, R: Any {
let msg_send_fn = msg_send_fn::<R>();
Expand All @@ -145,6 +148,25 @@ macro_rules! message_args_impl {
})
}

#[cfg(all(feature="gnustep",
not(any(target_arch = "arm",
target_arch = "x86",
target_arch = "x86_64"))))]
unsafe fn send<T, R>(self, obj: *mut T, sel: Sel) -> R
where T: Message, R: Any {
let mut receiver = obj as *mut Object;
let nil: *mut Object = ::std::ptr::null_mut();
let ref slot = *runtime::objc_msg_lookup_sender(&mut receiver as *mut *mut Object, sel, nil);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why can't this use objc_msgSend? I've been trying to read up on gnustep's concept of slots but haven't found a great introduction in the documentation. Does gnustep not support objc_msgSend?

Otherwise the only reason to use the slot lookup seems to be so that you can keep the slot and call the method repeatedly, but since we get the slot anew each time that doesn't seem like it'd be a benefit.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can implement without slots (just using objc_msgSend) I think that'd cut down on complexity, and maybe later we can add a new API for caching method calls that can abstract over it in a platform-independent way, using slots on GNUstep and a different approach for Apple's runtime.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can only use objc_msgSend on the platforms quoted in the earlier comment (hence it's conditional on the target arch). For everything else, we need to fall back to this mechanism which looks up the slot first and then calls the IMP.

You're right that the slot mechanism is immensely useful for caching IMPs, but I don't see how to build a platform-independent abstraction over this. The reason is that caching IMPs is generally unsafe on the Apple runtime. Things like isa-swizzling, method_exchangeImplementation or simply adding a KVO observer might leave you with an outdated IMP and no way to detect it.
With the GNUstep runtime, the slot version will be incremented, giving you a way to invalidate stale cached IMPs.

let imp_fn = slot.method;
let imp_fn: unsafe extern fn(*mut Object, Sel $(, $t)*) -> R =
mem::transmute(imp_fn);
let ($($a,)*) = self;
objc_try!({
imp_fn(receiver as *mut Object, sel $(, $a)*)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does calling this function on a slot handle unrecognized selectors properly? The reason this crate is using objc_msgSend instead of looking up function pointers manually is because objc_msgSend will call forwardInvocation: and doesNotRecognizeSelector: when invoking the function manually won't.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it handles unrecognised selectors. The runtime has a hook __objc_msg_forward3, which is implemented in the gnustep-base library to provide forwarding via forwardingTargetForSelector:, or messageSignatureForSelector: and forwardInvocation:.

})
}

#[cfg(not(feature="gnustep"))]
unsafe fn send_super<T, R>(self, obj: *mut T, superclass: &Class, sel: Sel) -> R
where T: Message, R: Any {
let msg_send_fn = msg_send_super_fn::<R>();
Expand All @@ -156,6 +178,20 @@ macro_rules! message_args_impl {
msg_send_fn(&sup, sel $(, $a)*)
})
}

#[cfg(feature="gnustep")]
unsafe fn send_super<T, R>(self, obj: *mut T, superclass: &Class, sel: Sel) -> R
where T: Message, R: Any {
let sup = Super { receiver: obj as *mut Object, superclass: superclass };
let ref slot = *runtime::objc_slot_lookup_super(&sup, sel);
let imp_fn = slot.method;
let imp_fn: unsafe extern fn(*mut Object, Sel $(, $t)*) -> R =
mem::transmute(imp_fn);
let ($($a,)*) = self;
objc_try!({
imp_fn(obj as *mut Object, sel $(, $a)*)
})
}
}
);
}
Expand Down
30 changes: 28 additions & 2 deletions src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,27 @@ pub struct Sel {
ptr: *const c_void,
}


/// A structure describing a safely cacheable method implementation
/// in the GNUstep Objective-C runtime.
#[cfg(feature="gnustep")]
#[repr(C)]
pub struct Slot {
/// The class to which the slot is attached
pub owner: *const Class,
/// The class for which this slot was cached.
pub cached_for: *mut Class,
/// The type signature of the method
pub types: *const c_char,
/// The version of the method. Will change if overriden, invalidating
/// the cache
pub version: c_int,
/// The implementation of the method
pub method: Imp,
/// The associated selector
pub selector: Sel
}

/// A marker type to be embedded into other types just so that they cannot be
/// constructed externally.
enum PrivateMarker { }
Expand Down Expand Up @@ -79,7 +100,7 @@ pub struct Super {
pub type Imp = extern fn(*mut Object, Sel, ...) -> *mut Object;

#[link(name = "objc", kind = "dylib")]
extern {
extern "C" {
pub fn sel_registerName(name: *const c_char) -> Sel;
pub fn sel_getName(sel: Sel) -> *const c_char;

Expand Down Expand Up @@ -123,6 +144,11 @@ extern {
pub fn method_getNumberOfArguments(method: *const Method) -> c_uint;
pub fn method_setImplementation(method: *mut Method, imp: Imp) -> Imp;
pub fn method_exchangeImplementations(m1: *mut Method, m2: *mut Method);

#[cfg(feature="gnustep")]
pub fn objc_msg_lookup_sender(receiver: *mut *mut Object, selector: Sel, sender: *mut Object, ...) -> *mut Slot;
#[cfg(feature="gnustep")]
pub fn objc_slot_lookup_super(sup: *const Super, selector: Sel) -> *mut Slot;
}

impl Sel {
Expand Down Expand Up @@ -444,7 +470,7 @@ mod tests {
assert!(method.name().name() == "description");
assert!(method.arguments_count() == 2);
assert!(method.return_type() == <*mut Object>::encode());
assert!(method.argument_type(1).unwrap() == Sel::encode());
assert_eq!(method.argument_type(1).unwrap(), Sel::encode());

let methods = cls.instance_methods();
assert!(methods.len() > 0);
Expand Down
9 changes: 9 additions & 0 deletions src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ use id::StrongPtr;
use runtime::{Class, Object, Sel};
use {Encode, Encoding};




#[cfg(feature="gnustep")]
#[link(name = "NSObject", kind = "static")]
extern {
}


pub fn sample_object() -> StrongPtr {
let cls = Class::get("NSObject").unwrap();
unsafe {
Expand Down
21 changes: 21 additions & 0 deletions test_utils/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "test_ns_object"
version = "0.0.1"
authors = ["Niels Grewe"]

description = "Mock implementation of NSObject for tests"
repository = "http:/SSheldon/rust-objc"
license = "MIT"

build = "build.rs"

[features]
gnustep = [ "gcc" ]

[lib]
name = "test_ns_object"
path = "lib.rs"

[build-dependencies.gcc]
gcc = "0.3"
optional = true
45 changes: 45 additions & 0 deletions test_utils/NSObject.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include <objc/runtime.h>
#include <stdint.h>
/**
* This is a mock implementation of NSObject, which will be linked against
* the tests in order to provide a superclass for them.
*/
__attribute__((objc_root_class))
@interface NSObject
{
Class isa;
}
@end

@implementation NSObject

+ (id)alloc
{
return class_createInstance(self, 0);
}

- (id)init
{
return self;
}

- (id)self
{
return self;
}

- (uintptr_t)hash
{
return (uintptr_t)(void*)self;
}

- (void)dealloc
{
object_dispose(self);
}

- (NSObject*)description
{
return nil;
}
@end
23 changes: 23 additions & 0 deletions test_utils/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#[cfg(feature="gnustep")]
extern crate gcc;
#[cfg(feature="gnustep")]
use std::path::PathBuf;


#[cfg(not(feature="gnustep"))]
fn compile() {
}

#[cfg(feature="gnustep")]
fn compile() {
gcc::Config::new().flag("-lobjc")
.flag("-fobjc-runtime=gnustep-1.8")
.flag("-fno-objc-legacy-dispatch")
.file("NSObject.m")
.compile("libNSObject.a");
let path = ::std::env::var_os("OUT_DIR").map(PathBuf::from).unwrap();
println!("cargo:rustc-link-search=native={}", path.display());
}
fn main() {
compile();
}
3 changes: 3 additions & 0 deletions test_utils/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#![crate_name = "test_ns_object"]
#![crate_type = "lib"]