XPC so universal app can interact with ARM or Intel dylib?

Hello -

TLDR - Is there any sample code to demonstrate how one goes about creating dedicated XPCServices to wrap ARM and Intel-specific dylibs?

  • We have an app we're looking at moving to a universal binary.
  • In that same app we have a framework that currently wraps R functionality by directly linking to /Library/Frameworks/R.framework/Current .
  • R now has dedicated Intel and ARM builds (https://mac.r-project.org/)

After watching the 2020 WWDC session "Port your Mac app to Apple silicon" (https://developer.apple.com/videos/play/wwdc2020/10214/?time=2006), it sounds like, for us to deploy a universal binary I should look at wrapping the R interaction bits into dedicated ARM and Intel XPC services so the appropriate architecture for R will run.

Is anyone aware of any sample code or extended documentation demonstrating the ins and outs of how to think about this?

Thank you

Replies

R now has dedicated Intel and ARM builds

it sounds like, for us to deploy a universal binary I should look at wrapping the R interaction bits into dedicated ARM and Intel XPC services

Huh? I don’t understand your logic here. If R has both Apple silicon and Intel builds, the most obvious path forward is to link the Apple silicon ‘slice’ of your app to R’s Apple silicon build and the Intel slice of your app to R’s Intel build. That way each architecture loads the matching architecture of R and you don’t need any complicated glue.

Is there any reason that wouldn’t work?

ps The advice in that WWDC session is for folks who have an Apple silicon product that depends on a library that’s Intel-only. The app can’t load the library because, within any single process, all code must be the same architecture. The solution is for the app to include an Intel XPC Service that loads the library. However, that shouldn’t be necessary if there’s an Apple silicon version of R.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hi, Quinn -

First, thanks for the response.

If R has both Apple silicon and Intel builds, the most obvious path forward is to link the Apple silicon ‘slice’ of your app to R’s Apple silicon build and the Intel slice of your app to R’s Intel build.

I wasn't aware you could compile one universal app and link to two different libraries with different architectures like that. Is there any documentation on how to approach that? The only discussion I've found has been that WWDC session from last year.

Right now when we build now we point to the /Library/Frameworks/R.Framework/Current symlink which resolves to whichever single version of R is listed as "Current" (EX: /3.1). When the user runs the app we then pull in "Current" and reference whichever version of R the user has installed - could be Intel and, as of last week, could be ARM. We don't know. We simply hit "Current" and pull in whichever version of R the user has designated as appropriate.

If I understand this we'd have something like...

  • R 3.1 (Intel)
  • R 4.1 (ARM)

both living in /Library/Frameworks/R.Framework. I'm struggling to grasp how we build against R as you describe. Right now we have a single reference to a single library in the build dependencies. BTW - not doubting you at all - I simply haven't heard of this approach before.

I'd love to have more information on this. We're doing this for an open source app sponsored through an NIH research grant. There's an audience out there in our space who would benefit from having a way to interact with R like this from a Mac app and it's something we could at least share with other collaborators and colleagues.

Thank you, Eric

  • @eskimo -

    Sorry, one additional point of clarification. We are linking against R.framework, not embedding it.

    General -> Frameworks and Libraries -> R.Framework (Do Not Embed)
Add a Comment

If I understand this we'd have something like...

Hmmm, that’s pretty weird. Framework versions are rather unusual in and of themselves; our current advice is that you ship a single framework version (A) and have Current always point to that. And having different versions with different architecture is doubly weird; we usually handle architecture differences by building a universal framework.

I’d like to download these frameworks and take a more in-depth look. Can you point me in the right direction?

I wasn't aware you could compile one universal app and link to two different libraries with different architectures like that.

That’s certainly possible, although it’s well off the beaten path. Lemme do the above before I send you off into the weeds.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

@eskimo - You rule. Honestly, we're struggling with wrapping our heads around how to think about this, so I really sincerely appreciate you taking the time to respond to us and doubly appreciate you being willing to investigate.

The R framework(s) are available via https://mac.r-project.org/ . There are separate Intel and ARM versions

  • Intel 4.1 - https://mac.r-project.org/high-sierra/R-4.1-branch/R-4.1-branch.pkg
  • ARM 4.1 - https://mac.r-project.org/big-sur/R-4.1-branch/R-4.1-branch.pkg

Our audience (medical/biomedical informatics researchers, primarily) tend do want to use their own versions of R with their own packages, etc. installed, so as a result we do not distribute R directly with our app (https://sites.northwestern.edu/stattag/). Instead, we link on build to "Current" and our app links to the R distribution on the user's machine at runtime. Our focus is on reproducible research, so the rationale here is that we want to allow users to govern the specific application (R, Stata, etc.) versions for their project. There are some other challenges with that, but our big question right now is how to approach ARM vs. Intel.

I think that means we have three scenarios:

  • Universal app runs on Intel machine with Intel R
  • Universal app runs on ARM machine with Intel R (I had previously tested this on the DTK before there was an ARM R and it worked fine)
  • Universal app runs on ARM machine with ARM R

Now that there's the possibility a user could have an ARM machine with either an Intel or an ARM build of R set as "Current," I'm not even sure how to think about this problem.

We're in the process of getting an ARM Mac Mini (we two developers both have Intel machines right now) and we hope to be able to do more extensive testing soon.

Any insights / ideas / critiques are very, very welcome.

Again, thank you Eric (and Luke, Leah, and Abi)

@eskimo -

I've opened up access to a demo app on GitHub that takes our R wrapper (RCocoa) and drops it into simple app that lets you enter basic R code to return a single R value. It defaults the R code for convenience

  • https://github.com/StatTag/RCocoaExampleLinkAtRuntime (dummy app - exposes some of the R bits we use in our app)
  • https://github.com/StatTag/RCocoa (R wrapper framework)

RCocoa is the R wrapper framework that references R. The dummy app links to the RCocoa project.

RCocoa was written by my collaborator Luke and presented at useR! in 2019 (https://www.r-project.org/conferences/useR-2019/posters). Exposing R for incorporation into statistical applications has been super super helpful to our users.

Thank you, Eric

Luke, Leah

I’m sure that generates some office teasing (-:

The R framework(s) are available via

Thanks for the links, and the general background. Looking at those installer packages I see a structure like this for Apple silicon:

/Library/
  Frameworks/
    R.framework/
      R -> Versions/Current/R
      Versions/
        Current -> 4.1-arm64
        4.1-arm64/
          R

and this for Intel:

/Library/
  Frameworks/
    R.framework/
      R -> Versions/Current/R
      Versions/
        Current -> 4.1
        4.1/
          R

This is a very weird way to structure a framework. Most framework developers ship a single universal product, with both Intel and Apple silicon binaries merged together. Having said that, a lot of open source build systems have problems creating universal binaries, and so it’s understandable how things panned out this way.

Still, the real challenge is this:

Our focus is on reproducible research, so the rationale here is that we want to allow users to govern the specific application (R, Stata, etc.) versions for their project.

macOS supports two styles of dynamic linking:

  • Load-time dynamic linking — You can declare an import via a Mach-O load command. For example, here’s how the Pacifist app imports the Cocoa framework:

    % otool -L /Applications/Pacifist.app/Contents/MacOS/Pacifist
    …
        /System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa …
        …
    
  • Runtime dynamic linking — You can dynamically load libraries using various APIs (most notably, dlopen and dlsym).

The first option is by far the easiest, because the dynamic linker takes care of binding you to all your symbols. However, this option only works if the library exists at a known path. It doesn’t give you any control over the library resolution process.

In contrast, runtime dynamic linking gives you full control over the library resolution process but you have to manually bind yourself to all of your symbols.

If you want to give the user full control over which version of R they use then you have two challenges:

  • Where is that framework on disk?

  • Is its architecture compatible with your app?

Ideally the user should be able to choose any framework, with any architecture, without having to reconfigure their system. I can, for example, imagine a user with an Apple silicon Mac who wants to use native R for their new projects but requires Intel R for their existing projects. That pretty much torpedoes the idea of importing the R framework via a Mach-O import; you really have to do this all using runtime dynamic linking.

Beyond that, if you want to use the Intel framework when your app is running natively on Apple silicon, you’ll need a second process for that. Using an XPC Service will help with the IPC and the process lifecycle management, but this is still not easy. The main challenge is performance. Looking at RCocoa it seems that you export low-level primitives from the framework. That’s problematic for any IPC setup because IPC takes a cheap operation (a function call) and turns it into an expensive one (an IPC request/response).

My general advice when doing this is to move as far up the abstraction ladder as you can. For example, instead of exposing an increment primitive and calling it 1000 times, you’d implement an add X primitive that you call once. It’s hard for me to say how feasible this is because of my limited understanding of R and how your product interacts with it.

If you’d like to dig into this in more detail, I’m going to recommend that you open a DTS tech support incident so that I can allocate more time to it. It might even involve *gasp* a phone call (-:

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Hi, @eskimo -

Thank you again for the fantastic response here. My apologies for just now responding (out last week).

Luke, Leah

I’m sure that generates some office teasing (-:

LOL. I got that reference. (insert Steve Rogers meme) I'm also shocked none of us ever realized that...

I can't thank you enough for you detailed and extraordinarily considerate response. This is some amazing guidance I'll bring to our team meeting this morning.

If you’d like to dig into this in more detail, I’m going to recommend that you open a DTS tech support incident so that I can allocate more time to it. It might even involve gasp a phone call (-:

Oh, I'm in! I'll submit it after our team meeting this morning. I'd love to see us do this in a way that benefits our users and also helps others understand how to approach similar needs.

Thank you again, Eric (and Team Skywalker)

@eskimo - Finally got our M1 through academic purchasing! DTS request opened! Follow-up: 781056948

DTS request opened!

Neat-o! It’ll be routed to an engineer (possibly even me) this morning (Pacific time).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"