Using Linux C APIs in Swift: Glob

With Apple's announcement that Swift is coming to Linux, it's time to think about how Swift can be leveraged as a system language rather than simply an app language.

In order to function as a systems language, we need an easy interface to the primitives on a system. On the BSD-like OS X, that means using the myriad BSD functions found in /usr/share/man/man3 (well, the lower-case ones mostly). In this post, I'll show how to wrap glob(3) with Swift, since I needed it for a recent project and it's a reasonably simple introduction to Swift-C interaction.

The glob API

On OS X, you can access the manpage via $ man 3 glob. The manpage shows three different functions you can call, though one of them (glob_b) uses a block rather than a function pointer, so I'm not sure whether that's going to make the port over to Linux. In any case, let's focus only on glob and globfree.

The glob function

The glob function, as the manpage indicates, has 4 arguments and an error-indicating return value. The arguments are, in order: the pattern, some behavior flags, an error function pointer, and the pass-by-reference result object. The return value is non-zero for errors, so our implementation will just pretend it didn't find any results upon error.

Let's begin by setting up our Glob class:

class Glob: CollectionType {
  var globFlags = GLOB_TILDE | GLOB_BRACE | GLOB_MARK

  init(pattern: String) {
    var globObj = glob_t()
    glob(
      pattern.cStringUsingEncoding(NSUTF8StringEncoding)!,
      globFlags,
      nil,
      &globObj
    )
    // store results somewhere
    globfree(&globObj)
  }
}

This doesn't quite compile, because we need to adhere to the CollectionType protocol. We can see, though, that we've successfully wrapped glob and globfree in Swift. We'll take care of that gross exclamation point in the string decoding and the storing of results in a minute, but first, why are we adhering to the CollectionType protocol?

The CollectionType Protocol

There are a number of enumerable protocols in Swift, including SequenceType and CollectionType. In our case, it makes more sense to say we want a "collection" of paths rather than a "sequence" of paths. To me, a sequence means that once we've received an element in a sequence, we'll never need it again. I can anticipate use cases where that might not be true, so that's why I'm using CollectionType. Why do this? Once we adhere to either SequenceType or CollectionType, we get for...in functionality for free. Think of it as NSFastEnumeration for Swift.

To adhere to the CollectionType protocol, we need to provide two properties—startIndex and endIndex—and a subscript. We can do that like this:

class Glob: CollectionType {
  // non-CollectionType code elided...

  var paths = [String]()

  var startIndex: Int {
    return paths.startIndex
  }

  var endIndex: Int {
    return paths.endIndex
  }

  subscript(i: Int) -> String {
    return paths[i]
  }
}

Adding a Little Safety

Since we're interacting with a C API, we're faced with things like char arrays, which aren't exactly Strings, since they don't have an encoding. With this as a potential (though extremely unlikely) scenario, we want to extract the data from our glob_t object, sanitize it, and store it in a [String]. We also want to guard against weird input so we can get rid of the exclamation point in our initializer:

class Glob: CollectionType {
  // CollectionType declarations elided

  private var globFlags = GLOB_TILDE | GLOB_BRACE | GLOB_MARK
  var paths = [String]()

  init (pattern: String) {
    var globObj = glob_t()
    if let cPattern = pattern.cStringUsingEncoding(NSUTF8StringEncoding) {
      if executeGlob(cPatt, globPtr: &globObj) {
        populatePaths(globObj)
      }
    }
    globfree(&globObj)
  }

  private
  func executeGlob(cPattern: [CChar],
                   globPtr: UnsafeMutablePointer<glob_t>) -> Bool {
    return 0 == glob(cPattern, globFlags, nil, globPtr)
  }

  private
  func populatePaths(gt: glob_t) {
    for var i = 0; i < Int(gt.gl_matchc); i++ {
      if let path = String.fromCString(gt.gl_pathv[i]) {
        paths.append(path)
      }
    }
  }
}

Conclusion

We now have an approachable Swift API for glob:

for path in Glob("~/dev/{ruby,js,swift}") {
  println(path)
}

The full source code, with tests, is available as a Gist.

With the right techniques, you can abstract away C APIs in your Swift code, making your entire codebase safer and more idiomatic. It's true that glob is a fairly simple API, but the primitives afforded us by the Swift language and the Foundation framework let us design some awfully clean abstractions.


Category: Development
Tags: Swift, iOS