Init String from a non-null terminated C char array

I have the following C header file which describes a non-null terminated string in C

typedef struct RMFFI_NntString {
  const char *data;
  uint64_t len;
} RMFFI_NntString;

I was hoping to create an unowned wrapper to this. Originally, I was using String.init(bytesNoCopy:, but after it's deprecation in macOS 13, have learnt that it might actually be copying bytes. I've had some discussion on this here. But, am starting this new thread, so as not to take that one off topic.

Ideally, I'd like my API to this wrapper to be an extension to Swift String with a new init - String.init?(rmffiNntString: RMFFI_NntString) {}. My first attempt at this was

extension String {
public init?(rmFfiNntString: RMFFI_NntString) {
  self.init(decoding: buffer, as: UTF8.self)
  let data = Data(bytesNoCopy: UnsafeMutableRawPointer(mutating: stringData), count: Int(rmFfiNntString.len), deallocator: .none)

  self.init(data: data, encoding: .utf8)
}
}

This works. But, couple of replies on the other thread suggest I should be using String.init(decoding:as:). However, I don't see a direct way to do this, and the best I've been able to come up with is

public init?(rmFfiNntString: RMFFI_NntString) {
  guard let stringData = rmFfiNntString.data else {
    return nil
  }
        
 let string = UnsafeBufferPointer(start: stringData, count: Int(rmFfiNntString.len))
             .withMemoryRebound(to: UInt8.self) { buffer in
             // Need to rebind as cchar is Int8, while the decoding protocol for UTf8 expects UInt8
                return String.init(decoding: buffer, as: UTF8.self)
               }
        
 // Have to do two init's - although the second one might be just a simple copy?       
 self.init(string)
}

From David Smith's reply on the other thread, it looks like using String(data:...) might produce a bridged String, while String(decoding:... will always produce a native Swift String.

Would there be a better way to create a native Swift String than the one outlined above? Thanks

I would suggest starting with UnsafeRawBufferPointer rather than rebinding, but other than that, that's a fine approach. Wrapping in Unsafe*BufferPointer does not copy anything, it's basically just a pointer + length pair.

Thank you. I now have the following which works fine :slight_smile:

extension String {
 public init?(rmFfiNntString: RMFFI_NntString) {
    guard let data = rmFfiNntString.data else {
      return nil
    }
        
    let buffer = UnsafeRawBufferPointer(start: data, count: Int(rmFfiNntString.len))
    self.init(decoding: buffer, as: UTF8.self)
 }
}
1 Like