Chapter 3 Building a Real Command Line Tool — Part 1
Technically we already have built a “real” command line tool, but it
is fairly useless. The one we’ll be building starting with this chapter
will be a tool that we can divine the sunrise and sunset times for a
given location on a given date. Now, we could totally do this just in
Swift, but there’s a handy and small R package —
{daybreak}
22 — that has
sunrise/sunset compute functions and wrapping it into a command line app
will help us ease into task of interfacing with more complex projects.
3.1 A Fresh Start
If you followed the advice in the previous chapter to make an
copy/archive/snapshot of the last project (before we made the finished
product), follow the advice in this handy
article23 and rename the project
to ephemerids
. Sadly, it’s a Medium platform post, so consider opening
the footnoted link in a private browsing window.
Once you’ve renamed and opened the new project the first thing we need to do is poke some holes in the “hardened runtime” — a series of limitations Apple prefers apps to run under to, ostensibly, make your computing environment safer.
Since {daybreak}
requires compilation it has a shared library which
does not have an Apple-required digital signature that is intended to
somewhat assure you code you’re running is not evil. To use it with our
compiled Swift+R app we’ll need to disable “library validation” which
you can do under the “ephemerids” target’s “Signing & Capabilities”:
With that step performed, we can start the wrapping process.
3.2 Mapping Out Steps To Our Goal
As noted in the introduction to this chapter, we’re building a command line app to let us get sunrise and sunset times for a given place on a given date and we’re going to be using an R package for some of the functionality. That means we will:
- (at some point) take in command line arguments from the user — we’ll do this in a later chapter to keep this one simple and just hard code inputs for now.
- need to ensure the
{daybreak}
package is installed. Since we’re relying on the local R installation,{daybreak}
needs to be available for use, so we’ll want to make sure it’s there and prompt the user to install it if isn’t, vs crash and burn with no helpful message saying why. - load the
{daybreak}
package into our embedded R session so we have access to thesun_rise_set
function that we’ll be using from the package. - call the
sun_rise_set
function and display output to the user
We’ll also build some utility functions (that we can reuse in other
projects) to load R packages and make it a bit easier to work with R
SEXP
s.
3.3 Gotcha #2: “Complex” C Macros
We’ll be working with strings and one popular way to get a C string from
a compatible R SEXP
24 is:
CHAR(Rf_asChar(x))
CHAR
is a macro defined in Rinternals.h
like this:
#define CHAR(x) ((const char *) STDVEC_DATAPTR(x))
Since we’ll be working with SEXP
s we’ll also have to remember to keep
any of the ones we make in Swift safe from R’s garbage
collector25. That would normally
mean making heavy use PROTECT
and UNPROTECT
macros which are also
defined in Rinternals.h
:
#define PROTECT(s) Rf_protect(s)
#define UNPROTECT(n) Rf_unprotect(n)
Unfortunately, CHAR
, PROTECT
, and UNPROTECT
are deemed “complex C
macros” as they aren’t just defining constants. Complex C macros wont
work in Swift’s C bridge26, which
means we need to work around this in various ways.
We’ll work around the CHAR
gotcha in swift-r-brige.c
by adding a
function wrapper (since we can still use CHAR()
on the C side):
// it's likely the compiler would optimize this anyway
// but by using `inline` we're asking it to replace
// a call of `CHAR_Rf_asChar` with its body, thus avoiding the // overhead of a function call and mimicking what `CHAR` was
// originally doing.
inline const char *CHAR_Rf_asChar(SEXP x) {
return(CHAR(Rf_asChar(x)));
}
(make sure to put the matching function declaration — sans inline
in
the .h
file).
For PROTECT
and UNPROTECT
we’ll put one set of Swift-R workaround
helpers in a new file called r-utils.swift
which should look like
this:
import Foundation
let PROTECT = Rf_protect
let UNPROTECT = Rf_unprotect
We’re defining those mainly so we can just use PROTECT
and UNPROTECT
as we would in C. There are some side-effects of doing this, one of
which is that SEXP
s protected with this Swift-wrapped PROTECT
are
going to be treated as Optional
(more on Optional
s later) values.
Another, more major side effect is that if R Core ever changes what
those two macros do we’ll need to change or abandon this workaround.
To stick closer to what R Core intended, we’ll define another set on the C side:
inline SEXP RPROTECT(SEXP x) {
return(PROTECT(x));
}
inline void RUNPROTECT(int x) {
UNPROTECT(x); }
(again, make sure to put the matching function declaration — sans
inline
in the .h
file).
Use either of them but try to stick to a consistent use pattern.
3.4 Gotcha #3: C Types (a.k.a. I got dem Int32
blues)
R uses 32-bit integers so anywhere there are int
s in R’s C code
they’re 32-bit. If we try to use plain Swift Int32
when working with
R’s C interface we’ll eventually see something like this:
Cannot convert value of type
'Swift.UnsafeMutablePointer<__ObjC.Int32>'
(aka 'UnsafeMutablePointer<UInt32>')
to expected argument type
'Swift.UnsafeMutablePointer<Swift.Int32>'
Swift’s types/objects are not C types. Swift has explicit C-compatible numeric types defined as type aliases:
CInt
is a Cint
(also usable explicitly asSwift.Int32
)CBool
is a Cbool
CChar
is a Cchar
(also usable explicitly asSwift.Int8
)CFloat
is a Cfloat
(also usable explicitly asSwift.Float
)
We’ll use the C…
types when needed when bridging R’s C code and Swift.
3.5 Requiring Packages
While there are plenty of useful tools in the default global R
environment, our daybreak::sun_rise_set()
function is not in there
yet, so we need some way to do the equivalent of library(daybreak)
or
require(daybreak)
. We’ll need to do this for other packages to, so a
generic require()
Swift function seems like a good utility to build.
3.5.1 Noise Cancellation and Error Handling
Loading packages into R can be a noisy operation since R likes to be helpful and warn you about any potential namespace conflicts and other dependent packages being loaded as it makes a package’s functionality available on search path.
Many packages (unfortunately) also spew messages upon load. None of this verbosity is helpful to users of our command line app so we need to ensure packages are loaded as quietly as possible.
We’ll want to use require()
vs library()
since we get a helpful
TRUE/FALSE
value on success vs an error, and we can use this feedback
to know whether to prompt the user to install any given package.
Finally, if a package is not available R will provide a helpful warning
from require()
, so we’ll need to stop that from happening.
So, we’ll need to build the Swift equivalent of:
= suppressWarnings(
res require(
package = "daybreak",
warn.conflicts = FALSE,
quietly = TRUE
)
)
if (!res) {
cat("The {daybreak} package is not available. Please remotes::install_github('hrbrmstr/daybreak')\n")
}
which means:
- using a pairlist27 to build
the
require()
function call since we’re using named parameters - using that call as a parameter to a call to a Swift-wrapped
suppressWarnings
- provide a mechanism for Swift to know about package load failures so
we can use this to prompt the user. For this we’ll rely on Swift
Error
s28.
We’ll start with the easy task first and add an RError
enumeration to
r-utils.swift
:
enum RError: Error {
case tryEvalError(String)
}
We’re calling it tryEvalError
since we’ll be using R_tryEvalSilent()
to evaluate the created expressions since it won’t die on error or
display error messages. On error it returns NULL
and sets a
pass-by-reference error parameter to a non-zero value.
We’ll raise (throw) and error via:
throw RError.tryEvalError("Some helpful error message.")
3.5.2 Using defer{}
with PROTECT()
Whenever PROTECT
is used, e.g. PROTECT(Rf_ScalarLogical(1))
there
needs to be a corresponding UNPROTECT()
, and this balanced
PROTECT
/UNPROTECT
dance must be complete regardless of any logic
branches or error catches in your code.
Swift provides something like R’s own on.exit()
capability with
defer { }
. Anything in a defer
block is run after any code return
path in a given function. Like on.exit()
these are stacked (in
reverse).
We can immediately postcede all PROTECT
calls with a
defer { UNPROTECT(#) }
line, (#
likely being 1
though that won’t
always be the case, e.g. when PROTECT
ing Rf_allocVector
calls). This
way they are guaranteed to be unprotected even if we throw an error, and
we don’t have to remember to unprotect anything later on. You may still
have need to non-defer an UNPROTECT
but deferring should work well in
most cases.
3.5.3 Our Swift require()
Implementation
Add this to r-utils.swift
// Swift helper to require(package)
func require(_ pkg: String) throws -> Bool {
var rErr: CInt = 0 // holds our error return value from evaluation
// setup the call to require
let requireCall: SEXP = PROTECT(Rf_allocVector(SEXPTYPE(LANGSXP), 3+1))!
defer { UNPROTECT(1) }
SETCAR(requireCall, Rf_install("require"))
// with params
var reqParams: SEXP = CDR(requireCall);
SETCAR(reqParams, PROTECT(Rf_mkString(pkg)))
defer { UNPROTECT(1) }
SET_TAG(reqParams, Rf_install("package"))
reqParams = CDR(reqParams)
SETCAR(reqParams, PROTECT(Rf_ScalarLogical(1)))
defer { UNPROTECT(1) }
SET_TAG(reqParams, Rf_install("warn.conflicts"))
reqParams = CDR(reqParams)
SETCAR(reqParams, PROTECT(Rf_ScalarLogical(1)))
defer { UNPROTECT(1) }
SET_TAG(reqParams, Rf_install("quietly"))
// now call ^^ via suppressWarnings
// requireCall is PROTECTed above but Rf_install() may allocate which could kill it so make sure it is safe
let suppressWarningsCall = PROTECT(Rf_lang2(Rf_install("suppressWarnings"), PROTECT(requireCall)))
defer { UNPROTECT(2) }
let res: SEXP = PROTECT(R_tryEvalSilent(suppressWarningsCall, R_GlobalEnv, &rErr))!
defer { UNPROTECT(1) }
// see exposition below for the error handling
if (rErr == 0) {
if (SEXPTYPE(TYPEOF(res)) == LGLSXP) {
return(Rf_asLogical(res) == 1)
} else {
throw RError.tryEvalError(
"R evaluation error attempting to load. Expected TRUE/FALSE, but found '\(String(cString: Rf_type2char(SEXPTYPE(TYPEOF(res)))))"
)
}
} else {
throw RError.tryEvalError("R evaluation error attempting to load \(pkg): \(String(cString: R_curErrorBuf()))")
}
}
Upon no error result from our R function execution we double check that
our return value type is, in fact, a logical. SEXP
s can be anything,
and even though we kinda know require
is returning a logical, R has no
other way to communicate that except by checking the type.
3.6 Testing require()
Make main.swift
look like this:
import Foundation
initEmbeddedR()
do {
if try require("daybreak") {
// eventually do something
} else {
print("The {daybreak} package is not available. Please remotes::install_github('hrbrmstr/daybreak')")
}
} catch {
debugPrint("Error: \(error)")
}
R_RunExitFinalizers()
R_CleanTempDir()
Rf_endEmbeddedR(0)
and run the product. You should see
The {daybreak} package is not available. Please remotes::install_github('hrbrmstr/daybreak')
if you haven’t installed the package yet and see nothing if you have.
We’re now ready to call functions in our newly available package.
3.7 Bridging SEXP
s, Making Time, and Finding Daylight
Before we can see daylight displayed in the terminal we’ll want to wrap
a call do daybreak::sun_rise_set()
with a Swift function we’ll call
sunRiseSet()
.
The daybreak::sun_rise_set()
function takes a string and two doubles
as parameters and returns two decimal hour (GMT) values in a list:
sun_rise_set("2021-01-02", -70.8636, 43.2683)
## $rise
## [1] 12.25761
##
## $set
## [1] 21.28906
Every time we take values from Swift to R we need to use functions like
Rf_mkString
, Rf_asInteger
, Rf_asReal
, etc. — sometimes with
PROTECT
ion — and every time we take values from R to Swift we need
to use corresponding CHAR_Rf_asChar
(the one we made to wrap the usual
way of getting C strings back), Rf_ScalarInteger
, Rf_ScalarReal
,
etc. and often put them into a Swift numeric object (e.g. String
,
Int
, Double
).
These are, frankly, kinda ugly and make code far more verbose than it
really needs to be. Let’s build some
extensions29 to String
, Int
,
and Double
to give these value machinations a face-lift.
For each of these objects, we’ll want three extensions:
init?(_ sexp: SEXP) {}
which will take aSEXP
as an object creation input and — after ensuring it’s length == 1 and is a compatible type create a compatible Swift object or returnnil
(the?
afterinit
signals this object initializer may not always work and could return anOptional
30).var SEXP: SEXP { }
which is a computed property31 that will let us do things like12.SEXP
and get back anINTSXP
.var protectedSEXP: SEXP {}
which returns aPROTECT
ed version of the givenSEXP
.
Create a new Swift file called sexp-utils.swift
and make it look like:
import Foundation
extension String {
init?(_ sexp: SEXP) {
if ((sexp != R_NilValue) && (Rf_length(sexp) == 1)) {
switch (TYPEOF(sexp)) {
case STRSXP: self = String(cString: CHAR_Rf_asChar(sexp))
default: return(nil)
}
} else {
return(nil)
}
}
var SEXP: SEXP { return(Rf_mkString(self)) }
var protectedSEXP: SEXP { return(RPROTECT(Rf_mkString(self))) }
}
extension Int {
init?(_ sexp: SEXP) {
if ((sexp != R_NilValue) && (Rf_length(sexp) == 1)) {
switch (TYPEOF(sexp)) {
case INTSXP: self = Int(Rf_asInteger(sexp))
default: return(nil)
}
} else {
return(nil)
}
}
var SEXP: SEXP { return(Rf_ScalarInteger(CInt(self))) }
var protectedSEXP: SEXP { return(RPROTECT(Rf_ScalarInteger(CInt(self)))) }
}
extension Double {
init?(_ sexp: SEXP) {
if ((sexp != R_NilValue) && (Rf_length(sexp) == 1)) {
switch (TYPEOF(sexp)) {
case REALSXP: self = Rf_asReal(sexp)
case INTSXP: self = Double(Int(sexp)!) // a tad more dangerous
case STRSXP: self = Double(String(sexp)!)! // a tad more dangerous
default: return(nil)
}
} else {
return(nil)
}
}
var SEXP: SEXP { return(Rf_ScalarReal(self)) }
var protectedSEXP: SEXP { return(RPROTECT(Rf_ScalarReal(self))) }
}
}
(Note the use of RPROTECT
in the above to avoid having to declare the
SEXP
as optional or force-unwrap an Optional
like we would if we had
just used PROTECT
. We could have just used Rf_protect()
but this is
ultimately safer.)
At the beginning of this section we noted that sun_rise_set
returns a
decimal hour. Those aren’t very useful to humans so we’ll need to
convert them to HH:MM
.
We’ll also noted that we’ll be writing a Swift function to wrap the R
function and we’ll definitely want to have sunRiseSet()
use an actual
Date
as input for type safety and communicating explicit intent.
Let’s extend Date
to make it easier to get an ISO8601 date
string32 and also extend
Double
to make it easier to process decimal dates and use the bits
we’ll add to Date
to get the string we’re looking for right from a
Decimal date.
Create another file called utils.swift
and make it look like:
import Foundation
extension Date {
init(fromISODate: String) {
self = Date.iso8601Formatter.date(from: fromISODate)!
}
var ISODate: String {
Date.iso8601Formatter.string(from: self)
}
static let iso8601Formatter: ISO8601DateFormatter = {
let fmt = ISO8601DateFormatter()
fmt.formatOptions = [ .withFullDate, .withDashSeparatorInDate ]
return(fmt)
}()
}
extension Double {
var whole: Self { rounded(.toNearestOrAwayFromZero) }
var fraction: Self { truncatingRemainder(dividingBy: 1) }
var decimalTimeToHM: String {
return(String(format: "%02d:%02d", Int(whole), Int(fraction*60)))
}
}
We’re now ready to build our sunRiseSet()
function.
Create one, final file: sunriseset.swift
.
Our function will return two values, so we will need a struct
that, so
start by making sunriseset.swift
look like this:
import Foundation
struct sunriset {
let rise: Double
let set: Double
}
Now, we need to build a call to sun_rise_set
using Rf_lang4()
which
takes a callable expression and 3 unnamed parameters and use
VECTOR_ELT()
to get the values of each returned list element.
While lat.protectedSEXP
is not only a tad less verbose than the
equivalent PROTECT(Rf_ScalarReal(lat))
,
PROTECT(Rf_ScalarInteger(CInt(self)))
starts to get a little more
gnarly than the Int
equivalent (which isn’t being used here) and
having the details of the date-to-String
-to-SEXP
conversion hidden
away is absolutely much nicer than polluting this, now, simple function
call with all that code.
func sunRiseSet(date: Date, lng: Double, lat: Double) -> sunriset {
var err: CInt = 0
let call_ƒ = RPROTECT(
Rf_lang4(
Rf_install("sun_rise_set"),
date.ISODate.protectedSEXP,
lng.protectedSEXP,
lat.protectedSEXP
)
)
defer { UNPROTECT(4) }
let res: SEXP = PROTECT(R_tryEvalSilent(call_ƒ, R_GlobalEnv, &err)) ?? R_NilValue
defer { UNPROTECT(1) }
return(sunriset(
rise: Double(VECTOR_ELT(res, 0)) ?? -1,
set: Double(VECTOR_ELT(res, 1)) ?? -1
))
}
3.8 The main()
Event
Since we’re hard coding values in this chapter, our main.swift
is
pretty light:
import Foundation
initEmbeddedR()
do {
if try require("daybreak") {
let res = sunRiseSet(date: Date(), lng: -70.8636, lat: 43.2683)
print("Sunrise: \(res.rise.decimalTimeToHM) GMT\n Sunset: \(res.set.decimalTimeToHM) GMT")
} else {
print("The {daybreak} package is not available. Please remotes::install_github('hrbrmstr/daybreak')")
}
} catch {
print("Error: \(error)")
}
R_RunExitFinalizers()
R_CleanTempDir()
Rf_endEmbeddedR(0)
The decimalTimeToHM
extension to Double
makes quick work of the
printing.
Running the above should produce:
Sunrise: 12:15 GMT
Sunset: 21:20 GMT
3.9 Up Next
We’ll modify this project to take arguments from the command line in the next chapter.
Before diving in, try wrapping the other functions of the {daybreak}
package.
Remember, code examples can be found on GitHub33.
{daybreak} https://github.com/hrbrmstr/daybreak↩︎
How to Rename an Xcode Project Without Losing Your Mind https://ahadsheriff.medium.com/how-to-rename-an-xcode-project-without-losing-your-mind-88e558fb7566↩︎
R Internals: SEXPs https://cran.r-project.org/doc/manuals/r-release/R-ints.html#SEXPs↩︎
Writing R Extensions: Handling R Objects in C https://cran.r-project.org/doc/manuals/R-exts.html#Handling-R-objects-in-C↩︎
Using Imported C Macros in Swift https://developer.apple.com/documentation/swift/imported_c_and_objective-c_apis/using_imported_c_macros_in_swift↩︎
Writing R Extensions: Evaluating R Expressions from C https://cran.r-project.org/doc/manuals/R-exts.html#Evaluating-R-expressions-from-C↩︎
Swift: Error Handling https://docs.swift.org/swift-book/LanguageGuide/ErrorHandling.html↩︎
Swift: Extensions https://docs.swift.org/swift-book/LanguageGuide/Extensions.html↩︎
Apple Developer Documentation: Optional https://developer.apple.com/documentation/swift/optional↩︎
Swift: Properties https://docs.swift.org/swift-book/LanguageGuide/Properties.html↩︎
ISO8601: Calendar dates https://en.wikipedia.org/wiki/ISO_8601#Calendar_dates↩︎
SwiftR Book Examples https://github.com/hrbrmstr/swiftr-book-examples↩︎