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 initial Objective C support #443

Merged
merged 1 commit into from
Feb 2, 2017
Merged

Conversation

scoopr
Copy link
Contributor

@scoopr scoopr commented Jan 25, 2017

Hi! I made an attempt to support parsing objective c headers. This is very much incomplete,
and likely not quite yet ready for merging, but I thought I'd share the progress so far.

Comments appreciated, I'm still very much a newbie in just about everything related to this change (rust as a language, rust ast, libclang, rust-bindgen), and there were many parts of code I wasn't quite sure about.

Commit message:
It parses interfaces and protocol but ignores base classes,
methods that don’t have arguments, the arguments are currently ignored.
Also you can pass objc class instances to C functions.

Next steps are inheritance/base classes, method signatures,
properties, categories.
Then check with system headers what is missing.

@scoopr
Copy link
Contributor Author

scoopr commented Jan 25, 2017

The failure is due to me hardwiring the use objc; etc. for my testing.. I suppose the raw_line option is meant for that.

Copy link
Contributor

@emilio emilio left a comment

Choose a reason for hiding this comment

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

Woot, this is great! Thanks for working on this :)

It looks quite good! As you mentioned, it still needs a bunch of work though.

I'm totally fine with landing this incrementally, but with some constraints:

  • First, it needs docs (also, CI is failing because we force documentation).
  • Second, it needs tests (this is something old bindgen really lacked when I rewrote it, and it's definitely necessary to avoid falling in a spiral of bugs and regressions).

Again, this is awesome! Thanks a lot for working on this :)

@@ -2074,6 +2091,18 @@ impl ToRustTy for FunctionSig {
// [1]: http://c0x.coding-guidelines.com/6.7.5.3.html
let arg_ty = if let TypeKind::Array(t, _) = *arg_ty.canonical_type(ctx).kind() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of calling canonical_type two times, let's use a match expression.

result: &mut CodegenResult<'a>,
_whitelisted_items: &ItemSet,
_: &Item) {

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: whitespace should go before attempting to merge.

Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove this newline

for method in &self.methods {
let method_name = ctx.rust_ident(&method.name);

let body = quote_stmt!(ctx.ext_cx(),msg_send![self, $method_name])
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: space after ,.

Ok(item) if ctx.resolve_item(item).kind().is_function() => item,
let signature =
match Item::parse(cur, Some(potential_id), ctx) {
Ok(item) if ctx.resolve_item(item)
Copy link
Contributor

Choose a reason for hiding this comment

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

could you leave formatting patches in a different commit? That will make this much more pleasurable to review :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oops, I thought I cleaned them all. I have the editor set up on formatting on save, and I manually cleaned up rest of these.

@@ -1070,6 +1071,7 @@ impl<'ctx> BindgenContext<'ctx> {
let name = item.canonical_path(self)[1..].join("::");
debug!("whitelisted_items: testing {:?}", name);
match *item.kind() {
ItemKind::ObjCInterface(_) => true, // TODO:
Copy link
Contributor

Choose a reason for hiding this comment

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

Any particular reason you have a full ItemKind for this?

Does an interface have the same characteristics as, let's say, a struct in C? I mean, can you forward-declare them, point to them, etc?

If so, you probably want to just fold it into Type instead, since you'd share a lot of code for detecting duplicates, going to the definition of the type, etc.

(Edit: after reviewing the rest of the commit I've seen you've also added TypeKind::ObjCInterface, you might want to just use ItemKind::Type if there isn't a better reason to have a full ItemKind).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Frankly, I wasn't sure, and I think I just looked at the try_parse! and thought ‘thats what I need’.

Forward declaration is a bit weird, using @class Foo;, this gives a cursor type CXCursor_ObjCClassRef, have not looked at that more closely.

One other interesting bit is protocols, those are for extending more methods for existing interface. Those come in as CXCursor_ObjCCategoryDecl, so I guess they could be handled completely separate (and generate their own trait)


match cursor.kind() {
CXCursor_ObjCInterfaceDecl |
CXCursor_ObjCProtocolDecl => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm, does libclang return a specific CXTypeKind for these (you can see this printing cursor.cur_type())? If so, then merging it into Type is probably the best path forward, otherwise, it'll probably need a bit of thinking to get it right.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It does, so I did that.

One annoying hack was that protocols were not types, so I forced it to think they are interfaces.

Copy link
Contributor

Choose a reason for hiding this comment

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

What kind of type do they report?

/* let referenced = location.unwrap().referenced().unwrap();
let referenced_ty = referenced.cur_type();
let declaration = referenced_ty.declaration();
println!("referenced={:?}", referenced);
Copy link
Contributor

Choose a reason for hiding this comment

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

Heh, welcome to the joy of debugging libclang :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

got me with my pants down :)

println!("cur type ={:?}", location.unwrap().cur_type());
println!("objc encoding ={:?}", ty.objc_encoding());*/

let inner = Item::from_ty_or_ref(ty.pointee_type().unwrap(),
Copy link
Contributor

Choose a reason for hiding this comment

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

This pretty much shares the body with CXType_Pointer, reference, et. al, right? If so, let's just fold it there when you're done with debugging it :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -869,6 +874,60 @@ impl Type {
parent_id,
ctx);
}
CXType_ObjCInterface => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, probably you don't want a new itemid for each Interface you parse, and I don't see any deduplication going on, so probably you should make this the actual container of your data (and use cursor.definition() to properly go to the actual definition, just in case you find a forward declaration before). You probably need to do roughly the same thing we do for CXType_Record (which is a plain struct), just storing most of the specific interface-related data you want.

@@ -697,6 +697,12 @@ impl<'ctx> Bindings<'ctx> {
try!(writer.write("\n".as_bytes()));
}

// TODO:

try!(writer.write("use objc;\n\n".as_bytes()));
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of printing this here we should use something like what we do to insert the __BindgenUnionField, etc. See CodegenResult::saw_union and similar methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@scoopr
Copy link
Contributor Author

scoopr commented Jan 27, 2017

I'm still missing documentation and testing.
How should I make it run some of the headers in -x objective-c? I suppose I could just name them with .m suffix, or add somehow add the -x flag when header_name.containts("objc")

@emilio
Copy link
Contributor

emilio commented Jan 27, 2017

Right now we rely on the automatic language detection for most of the tests, but you can annotate a test with // bindgen-flags:, and they'll get passed to the harness.

So it seems you either want m files (that's fine by me), or otherwise do something like what we do in tests/headers/dash_language.h, which does // bindgen-flags: -- -x c++.

let arg_ty = if let TypeKind::Array(t, _) = *arg_ty.canonical_type(ctx).kind() {


let arg_ty = match *arg_ty.canonical_type(ctx).kind()
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: indentation and spacing is quite odd in all this block right now, not sure if intended :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

huh, I wonder how atom/rustfmt missed that o_O

@@ -2062,6 +2077,13 @@ impl ToRustTy for Type {
let ident = ctx.rust_ident(&name);
quote_ty!(ctx.ext_cx(), $ident)
}

TypeKind::ObjCInterface(..) => {
let name = item.canonical_name(ctx);
Copy link
Contributor

Choose a reason for hiding this comment

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

This will be enough if you can't mix objective-c with namespaces. If you can, you need to handle them somehow (hint: you could use utils::build_templated_path, but pass always an empty vec![] of template arguments).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

clang will give an actual error if interface embedded in namespace. And to even get that error, it needs to be run with -x objective-c++ mode

@scoopr
Copy link
Contributor Author

scoopr commented Jan 28, 2017

Heres my latest, with some rudimentary tests and docs.
Also fixed some objc pointers in structs and global variables to emit the 'id' type there as well.

I'm using the id here to mimic the cocoa-rs convention, thought I think some have suggested trying struct Foo(id) type of wrapping of the type, and impl the traits on those types. But I think that kind of experimentation is ok to day later when it otherwise works.

@bors-servo
Copy link

☔ The latest upstream changes (presumably #456) made this pull request unmergeable. Please resolve the merge conflicts.

@emilio
Copy link
Contributor

emilio commented Jan 29, 2017

You need to add the objc dependency to the tests/expectations directory, so they compile.

@scoopr
Copy link
Contributor Author

scoopr commented Jan 29, 2017

Quite, it didn't seem quite as simple.
Either I did something wrong that I don't understand, or things really are this complicated, but I ended up adding a bindgen flag, that toggles the generated prologue from emitting either use objc; or #[macro_use] extern crate objc;, as the generated tests were run individually, and adding the extern crate to the tests lib.rs didn't seem work.

@scoopr
Copy link
Contributor Author

scoopr commented Jan 29, 2017

Ehh, botched rebase, I was sure I had run the tests one more time. Sorry for the noise

@scoopr
Copy link
Contributor Author

scoopr commented Jan 29, 2017

ngggh. of course, it needs some cfg guards

I'm not quite sure what is the best option. Should I always put in the generated objc prologue #[cfg(bindgen_objc)]? target_os="macos" would seem too harsh, as in theory this could be used on other platforms as well, even if rare. Guarding just the tests is one option too, I guess, but not sure how to implement that

@scoopr
Copy link
Contributor Author

scoopr commented Jan 30, 2017

Woo, finally a passing commit! :D

I opted to add cfg(target_os="macos") to the objc tests. It seemed to be the simplest change, though I'm not sure the .contains("objc_") is good enough toggle for it. I guess this also means that the travis config would need a osx target as well, to have those tests be exercised.

@bors-servo
Copy link

☔ The latest upstream changes (presumably #461) made this pull request unmergeable. Please resolve the merge conflicts.

Copy link
Contributor

@emilio emilio left a comment

Choose a reason for hiding this comment

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

Nice! I think I'm fine with this, mostly nits :)

@@ -98,10 +98,17 @@ fn create_bindgen_builder(header: &PathBuf)
let header_str = try!(header.to_str()
.ok_or(Error::new(ErrorKind::Other, "Invalid header file name")));

let objc_guard;
if header_str.contains("objc_") {
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of this, let's add a new line to the test configuration (see the if line.contains(bindgen-flags check above). // bindgen-osx-only maybe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ooh, that makes so much more sense, obvious even.


impl ObjCInstanceMethod {
fn new(name: &str) -> ObjCInstanceMethod {

Copy link
Contributor

Choose a reason for hiding this comment

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

please remove this initial newline.

fn new(name: &str) -> ObjCInstanceMethod {

let split_name: Vec<&str> = name.split(':').collect();

Copy link
Contributor

Choose a reason for hiding this comment

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

This may also go away if you want :)

pub struct ObjCInstanceMethod {
/// The original method selector name
/// like, dataWithBytes:length:
pub name: String,
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of making these public, could/should we make them private and add accessors?

pub name: String,

/// List of the methods defined in this interfae
pub methods: Vec<ObjCInstanceMethod>,
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of making these public, could/should we make them private and add accessors?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure

.unsafe_()
.fn_decl()
.self_()
.build(ast::SelfKind::Value(ast::Mutability::Immutable))//ast::SelfKind::Region(None, ast::Mutability::Mutable))
Copy link
Contributor

Choose a reason for hiding this comment

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

remove or reword the comment please :)

let arg_ty = if let TypeKind::Array(t, _) = *arg_ty.canonical_type(ctx).kind() {


let arg_ty = match *arg_ty.canonical_type(ctx).kind() {
Copy link
Contributor

Choose a reason for hiding this comment

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

please fix the alignment in this block.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Damnit, I fixed this once already, but somehow rustfmt got really confused in this block :(

@@ -2142,10 +2162,24 @@ impl ToRustTy for FunctionSig {
// the array type derivation.
//
// [1]: http://c0x.coding-guidelines.com/6.7.5.3.html
let arg_ty = if let TypeKind::Array(t, _) = *arg_ty.canonical_type(ctx).kind() {

Copy link
Contributor

Choose a reason for hiding this comment

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

please remove the whitespace between the comment and the let.

.with_items(trait_items)
.build();

let ty_for_impl = aster::AstBuilder::new()
Copy link
Contributor

Choose a reason for hiding this comment

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

this is the same as: quote_ty!(ctx.ext_cx(), id), which I think is clearer.

@@ -2109,6 +2125,10 @@ impl ToRustTy for Type {
let ident = ctx.rust_ident(&name);
quote_ty!(ctx.ext_cx(), $ident)
}
TypeKind::ObjCInterface(..) => {
let ident = ctx.rust_ident("id");
quote_ty!(ctx.ext_cx(), $ident)
Copy link
Contributor

Choose a reason for hiding this comment

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

just quote_ty!(ctx.ext_cx(), id) should work.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah didn't realise that, lot cleaner for sure!

Copy link
Contributor

@emilio emilio left a comment

Choose a reason for hiding this comment

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

Two little little nits more, an this is ready to go IMO :)

@fitzgen any concern with this?

@@ -87,6 +87,12 @@ fn create_bindgen_builder(header: &PathBuf)
} else if line.contains("bindgen-unstable") &&
cfg!(feature = "llvm_stable") {
return Ok(None);
} else if line.contains("bindgen-osx-only") {
Copy link
Contributor

Choose a reason for hiding this comment

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

You need to use take(3) above now :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fine :)

let inner = ctx.resolve_item(inner);
let inner_ty = inner.expect_type();
if let TypeKind::ObjCInterface(_) = *inner_ty.canonical_type(ctx).kind() {
aster::ty::TyBuilder::new().id("id")
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

Wow, I guess rustfmt really likes to mess with this assignment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that or I forgot add the chunk, nevertheless, I've disabled rustfmt, and hopefully it now makes sense

It parses interfaces and protocol but ignores base classes, and their
methods which don’t have arguments, the method signature is currently
ignored. Also you can pass objc class instances to C functions.

Next steps are inheritance/base classes, method signatures, properties,
categories. Then check with system headers what is missing.
@emilio
Copy link
Contributor

emilio commented Jan 31, 2017

Looks good to me! I've opened #464 to re-enable testing on OSX, after that lands this is good to go \o/

@emilio
Copy link
Contributor

emilio commented Feb 2, 2017

@bors-servo r+

Thanks for this! :)

@bors-servo
Copy link

📌 Commit 4736263 has been approved by emilio

@bors-servo
Copy link

⌛ Testing commit 4736263 with merge c1aaa6a...

bors-servo pushed a commit that referenced this pull request Feb 2, 2017
Add initial Objective C support

Hi! I made an attempt to support parsing objective c headers. This is very much incomplete,
and likely not quite yet ready for merging, but I thought I'd share the progress so far.

Comments appreciated, I'm still very much a newbie in just about everything related to this change (rust as a language, rust ast, libclang, rust-bindgen), and there were many parts of code I wasn't quite sure about.

Commit message:
It parses interfaces and protocol but ignores base classes,
methods that don’t have arguments, the arguments are currently ignored.
Also you can pass objc class instances to C functions.

Next steps are inheritance/base classes, method signatures,
properties, categories.
Then check with system headers what is missing.
@bors-servo
Copy link

☀️ Test successful - status-travis
Approved by: emilio
Pushing c1aaa6a to master...

@bors-servo bors-servo merged commit 4736263 into rust-lang:master Feb 2, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants