State of gnustep interop

Does anyone know the latest state of interoping with gnustep? 2 years ago I saw a post about how to use dynamicCallable's to pass messages to the gnustep objc runtime.

I have also seen the gnustep lead contributor mention that this support is an ongoing conversation with the Swift team that will get there eventually, but that methods to support this already exist.

I just wanted to get some feedback on what's currently possible / in the works.

I'm not aware of anyone who's working on it at the moment.

On Apple platforms, a huge proportion of the system APIs are in Objective-C, and many developers have a large existing codebase of Objective-C code, which together offer a very compelling reason to provide Objective-C interoperability. On other platforms, Objective-C is quite low on the list of languages that somebody could theoretically have written code in that a Swift programmer would want to use. As a result, I think the trade-offs for the language and its implementation are quite different.

In particular, it's not clear to me that burdening basic types like AnyObject with Objective-C interoperation is the right thing to do on non-Apple platforms. If we want to support Objective-C interoperation there, it might be better to introduce a language concept of a "foreign" class reference that doesn't share an object model with Swift classes. That concept could also be used to import reference-y types from other languages, like C, C++, and maybe even Java and so on.

10 Likes

If the goal is to be able to take a mixed ObjC/Swift application written for an Apple platform and compile it to run on some other platform, that's probably not good enough and you really want AnyObject to behave the same as it does on Apple platforms. I think that's achievable, but it's going to burden the language in ways that I think it's fair to say we don't want to make the default on (say) Linux. In particular, it's not going to be the same ABI as the standard Linux ABI, which should be able to assume that an AnyObject is Swift-native and which should not have GNUStep as a dependency of the Swift runtime.

2 Likes

Your thoughts essentially reinforce the need to have a more loosely coupled interoping strategy. At this point it sounds like any swift <=> gnustep interop would be in the form of a C interface header wrapper similar to c++.

fwiw, I didn't think having a gnu step dependency on the swift runtime was the right approach.

I don't think it has to go through a C wrapper; I just think we likely wouldn't want to converge the object models, and so ObjC class references wouldn't be convertible to AnyObject (which relies on assumptions about the object model). We'd still be doing direct interoperation, it's just that the type system would correctly reflect the technical differences in the system.

We would have to figure out how to import types like id *, since UnsafeMutablePointer<AnyObject> wouldn't work.

Hey everyone. This is something I am very interested in. I am the lead developer on the GNUstep project. I and others have been diligently working to make GNUstep as close to 10.14 (Catalina) as possible. We are not that far away from this at the moment. I have been very busy, as you can imagine. Swift integration is something I would very much like to see happen. Preferably, it would be so that code originally built for macOS using Swift could be seamlessly compiled on systems which support GNUstep.

10 Likes

Is it possible to just have wrappers for GNUStep API instead of the approach Apple takes where they expose Obj-C methods and classes as first class methods and classes in Swift through modifications to their ObjC API/Runtime and special interop code in the Apple Swift runtime, we instead could interact with the GNUStep API as if it were C (it all really is just C when complied, right?) We could instead write wrappers around it all. Maybe having a root GNUStepObjC object which handles calling method into the Obj-C runtime? I am not even sure if that is possible, but I would really enjoy seeing if someone could attempt it.

Maybe this could be done in a similar fashion as GitHub - rhx/SwiftGtk: A Swift wrapper around gtk-3.x and gtk-4.x that is largely auto-generated from gobject-introspection

It would make code that is not fully compatible with targets to Apple's ObjC runtime, but might yield at least some results today.

This is one area that I am not fully versed enough to try it my self.

But if GNUStep had Swift support, I would be making so many apps for it.

1 Like

I agree with John that this should be approached as a more general question of interoperation with other languages. There are many people in the community who'd like to interop with .NET, JVM, JavaScript, WebAssembly etc. Having special kajiggers in the compiler for each and every interop combination is not scalable.

Something akin to type providers in F# is frequently mentioned here and there. I think pitching general mechanisms similar to that will have a much higher chance in getting support from a wider community.

3 Likes

For the "objective" part of Objective-C, I wonder if a PythonKit style approach (using dynamic callable and dynamic member lookup) would work? It works well with dynamic languages.

5 Likes

I agree that a "type providers"-like plugin system would be great. The technical hurdle is that we'd need to provide a stable interface for plugins to use for both:

  • generating Swift declarations in response to lookups and
  • generating LLVM IR to use those things.

The latter worries me a lot.

2 Likes

Is this part required? I didn't expect type providers would have to interact with the Swift compiler on such a low level. Would that only be needed for languages targeting "native" binaries? I assumed that something like TypeScript or JVM running in a separate VM would not need any access to LLVM codegen.

1 Like

Well, you have to do something to interact with the language: it has types that need to be initialized/copied/moved correctly, it has its own ABI rules for how you do calls, and so on. It's certainly not true that every imported language is going to have a C-like interface with exported function symbols whose ABI signature exactly matches what we would naturally use for the imported Swift function.

It's possible that with the right extensions to Swift — extensions that would be independently useful, like "copy constructors", and like declaring C functions and variables with a particular signature and mangling — we could get away with providers just synthesizing Swift declarations, statements, and expressions instead of having to directly synthesize LLVM IR. That would be a huge simplification.

6 Likes

Hello,

The main benefit of making something like AnyObject work with both Swift and another Objective-C implementation is that the same pointer can be shared between those universes without wrappers and the management of the wrapper class.

When we bound the Apple APIs to C#, we did not have this luxury, so our mapping had to keep a wrapper around the underlying representation, something like this:

// C# Code
class NSObject {
    void *handle_to_native_object;
    bool owns;
    
   void destruct () {
     if (owns) { NSObject.Release (handle_to_native_object); 
   }
}

class UIView: NSObject {
   public UIView (void *handle, bool owns);
   public UIView (bool do_not_initialize_me);
}

The above is a light level description, and not the full details, as it gets a lot hairier. For details see for example [UIView Constructor (UIKit) | Microsoft Learn].

Your constructors then need to deal with various different scenarios: creating a new object from scratch (the constructor in C# must call the Objective-C allocation + initialization of the object), when an existing object is being surfaced from a return value, you must track whether you own the object or not, and a special process (linked above) for subclasses that want to initialize the chain in a subclass.

And that is just the initialization. Then you need to deal with the owneship and reference counting, which can introduce cycles. I suspect that a Swift wrapper for a native refcounted system will run into the same set of problems that we had, which are not simple to solve. Possible, but with a lot of drawbacks.

If I was in charge of GNUstep, I would go as far as building my own Swift toolchain with the AnyObject semantics that bring the same level of interoperatbility as Swift has with Objective-C on Apple platform, the other path is just too painful, and it is far from a straight shot. That said, if someone wants to explore that particular avenue and learn more about the gory details of cycles, and how we had to deal with those, you can see the engine and code here GitHub - xamarin/xamarin-macios: Bridges the worlds of .NET with the native APIs of macOS, iOS, tvOS, and watchOS.

As for F# type providers, you should think of them as a dynamic stub generation, and that almost everything that you can do with that dynamic system can be done with a pre-processor, for the problem at hand, they are mostly a distrraction (the Gtk+ bindings for Swift, or the various Godot bindings for Swift use this pre-processor approach, and it works fine).

Miguel

2 Likes

The problems you're describing are the result of two things:

  • not being able to directly import ObjC types into the language
  • specifically being forced to import them as C# class types, and thus manage a set of wrapping C# class instances

Importing non-Apple-platform ObjC classes as Swift classes that just don't conform to AnyObject doesn't come with these problems. It does raise some ObjC-specific problems, like how to import id. It also doesn't guarantee that ObjC-interoperating code written for Apple platform will compile for these other platforms. On the other hand, it doesn't require building code in a magic mode that's ABI-incompatible with other code compiled for those platforms and may impose substantial performance burdens on them, considering that their ObjC and Swift standard libraries won't have spent as much time tuning bridging as has happened on Apple platforms.

1 Like

Are there any white papers or other resources that detail how Apple's Swift implementation communicates with Objective-C under the hood (and not just how it imports it).

I found some resources like https://developer.apple.com/swift/blog/?id=39

I am looking for something similar that details the exact changes that had to be made on the Objective-C Runtime and Swift Runtime to ensure that they bridge.

I guess I must have misunderstood what you meant by conformance to AnyObject.

What makes the C# binding harder is the two different identities for an object (the NSObject address, and the C# address). Are you saying that you could import GNUStep Objective-C APIs with the same address for the objects, but without adding that machinery to AnyObject?

Correct. The representation of an imported ObjC class type would just be an ObjC object pointer, and we would manage the lifetime of that using objc_retain and objc_release, which are not otherwise safe to use with Swift objects on non-Apple platforms.

1 Like

This is a very broad topic because there are any number of ways that the Swift and ObjC standard libraries can interact with each other. Even sticking with core language features, I'm afraid I have to give a somewhat abbreviated explanation.

It's useful to think of class types as being organized into clusters that share a "class model" which gives general rules for how you can correctly implement basic class operations on them:

  • managing the ownership of an instance reference
  • the representation of types, if any
  • retrieving the dynamic type of an instance reference
  • performing upcasts and downcasts in the class hierarchy
  • testing reference equality
  • making a dynamically-dispatched method call
  • defining a class or subclass

Generally, class models must be consistent within a class hierarchy. Models tend to demand common resources, like storing a pointer to a particular structure at offset 0 in an object, and this makes them incompatible with each other. For example, polymorphic C++ classes store a v-table pointer at offset 0, while ObjC classes have an "is-a" (a heavily constrained pointer to an ObjC class object).

Like any type, class references can be handled totally generically, e.g. as an Any or in code that's generic over an unconstrained type T. Such code has no idea that it's working with a class reference at all, and so features that are specific to class references are generally disabled, or at least abstracted. For example, if you make a class type conform to a non-class-constrained protocol, a protocol requirement might be implemented with a non-final method; uses of that requirement will call a "thunk" function which does the necessary class method dispatch, so that the user doesn't need to know they're dealing with a class reference.

At the other end, when we have a concrete type like NSWindow, we can presumptively trust that type to tell us the right class model to use, and no interoperation is required.

A big source of complexity for Swift on Apple platforms is that we want Swift and ObjC classes to share at least some aspects of the class model: we want to have the type AnyObject in Swift and the type id in ObjC to be able to hold any Swift or ObjC class reference. Without this, we'd have a lot of artificial burdens when using the Apple platform APIs from Swift. So even totally native Swift classes have to have ObjC-compatible class objects and "is-a" references, and if you look at them through the ObjC runtime APIs, they always have an implicit ObjC superclass, get registered with the ObjC runtime, and so on. You can even do ObjC method dispatch on them, and it'll find methods that are (implicitly or explicitly) @objc. That's not a burden we want to force on platforms that don't have ObjC as a major part of the platform API.

3 Likes

I assume that when compiling Swift, there are flags that turn ObjC interop on. Is it reasonable to allow those to be turned on if the platform (PureDarwin/GNUStep/Windows ObjC) would like to provide the features on their end to enable the interop?

Well, I’m worried about creating a ton of different and ABI-incompatible dialects on non-Darwin platforms and maybe ending up in a situation where somebody needs a complex interface just to make Swift talk to Swift. But there’s no technical reason we can’t do that, no.

1 Like