From 7a7da425d3729dc15685f42d4d2f6a1ad366f5a8 Mon Sep 17 00:00:00 2001 From: Niels Grewe Date: Fri, 4 Sep 2015 23:01:27 +0100 Subject: [PATCH] Support for the GNUstep Objective-C runtime (SSheldon/rust-objc#27). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Support for the GNUstep Objective-C runtime Restore accidentally lost attribute Cross-platform support for message sends with the GNUstep runtime. Add feature gates for the platform specific objc_msgSend implementation used by the GNUstep runtime (mips not currently supported because I'm unaware of the calling conventions). In all other cases, fall back to the cross-platform two-stage message sending. This now works and passes some of the tests (I had a buggy old gcc version yesterday, it seems). Every test that assumes NSObject to be present fails, though because that is not part of the GNUstep runtime. We'll either have to link gnustep-base for the tests to run properly, or provide stub implementation. Fix calling objc_slot_lookup_super(), which had the argument type wrong. Trick the linker into linking gnustep-base to pull in NSObject, eventhough we never reference the symbol for it. Also a bit of documentation. Fix libobjc2 repository location. Satisfy test dependencies using a stub NSObject implementation. Requires a patched gcc crate at the moment. Word Update to track proposed gcc-rs API Changes to the gcc crate were merged (cf. alexcrichton/gcc-rs#54) Experiment with travis builds Slim down a bit Shell script syntax is hard More shell script tweaks Tweak libobjc2 install location Stage libobjc2 into a local directory Conditionalize features, fix missing ‘;’ again. GNUstep base is no longer required for running tests. Fix gcc-rs revision Depend on a specific gcc-rs revision from github until a release includes the changes needed. Update dependencies to the released gcc-rs version. Exclude .travis.yml from publishing Restore original arch (aarch64 was incorrectly replaced with arm) Move NSObject dependency for tests with the GNUstep runtime into a sub-crate Rename ‘gnustep_runtime’ to ‘gnustep’ for brevity. --- .travis.yml | 27 ++++++++++++++++++++++++ Cargo.toml | 7 ++++++- README.md | 7 +++++++ src/declare.rs | 2 +- src/lib.rs | 2 +- src/message.rs | 48 +++++++++++++++++++++++++++++++++++++------ src/runtime.rs | 30 +++++++++++++++++++++++++-- src/test_utils.rs | 9 ++++++++ test_utils/Cargo.toml | 21 +++++++++++++++++++ test_utils/NSObject.m | 45 ++++++++++++++++++++++++++++++++++++++++ test_utils/build.rs | 23 +++++++++++++++++++++ test_utils/lib.rs | 3 +++ 12 files changed, 213 insertions(+), 11 deletions(-) create mode 100644 test_utils/Cargo.toml create mode 100644 test_utils/NSObject.m create mode 100644 test_utils/build.rs create mode 100644 test_utils/lib.rs diff --git a/.travis.yml b/.travis.yml index 0b833aa65..cb1d5c06f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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://github.com/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 diff --git a/Cargo.toml b/Cargo.toml index eed7ea262..84ee9560a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,11 +10,12 @@ repository = "http://github.com/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" @@ -22,3 +23,7 @@ malloc_buf = "0.0" [dependencies.objc_exception] version = "0.1" optional = true + +[dev-dependencies.test_ns_object] +version = "0.0" +path = "test_utils" diff --git a/README.md b/README.md index cd261fa14..42d540557 100644 --- a/README.md +++ b/README.md @@ -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://www.github.com/gnustep/libobjc2). +To enable it, you need to pass the required feature to cargo: +`cargo build --feature gnustep`. diff --git a/src/declare.rs b/src/declare.rs index b620ee4a6..4133fd35d 100644 --- a/src/declare.rs +++ b/src/declare.rs @@ -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}; diff --git a/src/lib.rs b/src/lib.rs index a3bca51c4..d34ba055f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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() { diff --git a/src/message.rs b/src/message.rs index 7b5210fca..32be698f1 100644 --- a/src/message.rs +++ b/src/message.rs @@ -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. @@ -31,7 +30,7 @@ fn msg_send_fn() -> unsafe extern fn(*mut Object, Sel, ...) -> R { } } -#[cfg(target_arch = "x86")] +#[cfg(all(target_arch = "x86", not(feature = "gnustep")))] fn msg_send_super_fn() -> unsafe extern fn(*mut Object, Sel, ...) -> R { let size = mem::size_of::(); if size == 0 || size == 1 || size == 2 || size == 4 || size == 8 { @@ -55,7 +54,7 @@ fn msg_send_fn() -> 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() -> unsafe extern fn(*const Super, Sel, ...) -> R { if mem::size_of::() <= 16 { unsafe { mem::transmute(runtime::objc_msgSendSuper) } @@ -83,7 +82,7 @@ fn msg_send_fn() -> unsafe extern fn(*mut Object, Sel, ...) -> R { } } -#[cfg(target_arch = "arm")] +#[cfg(all(target_arch = "arm", not(feature = "gnustep")))] fn msg_send_super_fn() -> unsafe extern fn(*mut Object, Sel, ...) -> R { use std::any::TypeId; @@ -98,7 +97,7 @@ fn msg_send_super_fn() -> unsafe extern fn(*mut Object, Sel, ...) -> R { } } -#[cfg(target_arch = "aarch64")] +#[cfg(all(target_arch = "aarch64", not(feature = "gnustep")))] fn msg_send_fn() -> unsafe extern fn(*mut Object, Sel, ...) -> R { // stret is not even available in arm64. // https://twitter.com/gparker/status/378079715824660480 @@ -106,7 +105,7 @@ fn msg_send_fn() -> unsafe extern fn(*mut Object, Sel, ...) -> R { unsafe { mem::transmute(runtime::objc_msgSend) } } -#[cfg(target_arch = "aarch64")] +#[cfg(all(target_arch = "aarch64", not(feature ="gnustep")))] fn msg_send_super_fn() -> unsafe extern fn(*const Super, Sel, ...) -> R { unsafe { mem::transmute(runtime::objc_msgSendSuper) } } @@ -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(self, obj: *mut T, sel: Sel) -> R where T: Message, R: Any { let msg_send_fn = msg_send_fn::(); @@ -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(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); + 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)*) + }) + } + + #[cfg(not(feature="gnustep"))] unsafe fn send_super(self, obj: *mut T, superclass: &Class, sel: Sel) -> R where T: Message, R: Any { let msg_send_fn = msg_send_super_fn::(); @@ -156,6 +178,20 @@ macro_rules! message_args_impl { msg_send_fn(&sup, sel $(, $a)*) }) } + + #[cfg(feature="gnustep")] + unsafe fn send_super(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)*) + }) + } } ); } diff --git a/src/runtime.rs b/src/runtime.rs index 6f2a905b9..0b6b1e743 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -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 { } @@ -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; @@ -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 { @@ -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); diff --git a/src/test_utils.rs b/src/test_utils.rs index 5522db716..4d16406f4 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -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 { diff --git a/test_utils/Cargo.toml b/test_utils/Cargo.toml new file mode 100644 index 000000000..076603868 --- /dev/null +++ b/test_utils/Cargo.toml @@ -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://github.com/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 diff --git a/test_utils/NSObject.m b/test_utils/NSObject.m new file mode 100644 index 000000000..ce839784a --- /dev/null +++ b/test_utils/NSObject.m @@ -0,0 +1,45 @@ +#include +#include +/** + * 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 diff --git a/test_utils/build.rs b/test_utils/build.rs new file mode 100644 index 000000000..e5418fc89 --- /dev/null +++ b/test_utils/build.rs @@ -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(); +} diff --git a/test_utils/lib.rs b/test_utils/lib.rs new file mode 100644 index 000000000..418573658 --- /dev/null +++ b/test_utils/lib.rs @@ -0,0 +1,3 @@ +#![crate_name = "test_ns_object"] +#![crate_type = "lib"] +