1

For context: I'm trying to use the very handy LibXL. I've used it with success in Obj-C and C++ but am now trying to port over to Swift. In order to better support Unicode, I need to sent all strings to the LibXL api as wchar_t*.

So, for this purpose I've cobbled together this code:

extension String {
    ///Function to convert a String into a wchar_t buffer.
    ///Don't forget to free the buffer!
    var wideChar: UnsafeMutablePointer<wchar_t>? {
        get {
            guard let _cString = self.cString(using: .utf16) else {
                return nil
            }
            let buffer = UnsafeMutablePointer<wchar_t>.allocate(capacity: _cString.count)
            memcpy(buffer, _cString, _cString.count)
            return buffer
        }
    }

The calls to LibXL appear to be working (getting a print of the error messages returns 'Ok'). Except when I try to actually write to a cell in a test spreadsheet. I get can't write row 0 in trial version:

if let name = "John Doe".wideChar, let passKey = "mac-f.....lots of characters...3".wideChar {
            xlBookSetKeyW(book, name, passKey)
            print(">: " + String.init(cString: xlBookErrorMessageW(book)))
        }

        if let sheetName = "Output".wideChar, let path = savePath.wideChar, let test = "Hello".wideChar {
            let sheet: SheetHandle = xlBookAddSheetW(book, sheetName, nil)
            xlSheetWriteStrW(sheet, 0, 0, test, sectionTitleFormat)
            print(">: " + String.init(cString: xlBookErrorMessageW(book)))
            let success = xlBookSaveW(book, path)
            dump(success)
            print(">: " + String.init(cString: xlBookErrorMessageW(book)))
        }

I'm presuming that my code for converting to wchar_t* is incorrect. Can someone point me in the right direction for that..?

ADDENDUM: Thanks to @MartinR for the answer. It appears that the block 'consumes' any pointers that are used in it. So, for example, when writing a string using

("Hello".withWideChars({ wCharacters in
   xlSheetWriteStrW(newSheet, destRow, destColumn, wCharacters, aFormatHandle)
})

The aFormatHandle will become invalid after the writeStr line executes and isn't re-useable. It's necessary to create a new FormatHandle for each write command.

Todd
  • 1,770
  • 1
  • 17
  • 42
  • Research your issue before posting a question. https://stackoverflow.com/questions/31043593/how-to-convert-wchar-t-to-string-in-swift – SaganRitual Mar 23 '18 at 14:30
  • To be fair I did, that’s how I got the extension - I should edit to show various sources. Possibly the link you provided didn’t appear because it’s wchar_t to String. But it could still be of use so I’ll check it out further. – Todd Mar 23 '18 at 14:36

1 Answers1

1

There are different problems here. First, String.cString(using:) does not work well with multi-byte encodings:

print("ABC".cString(using: .utf16)!)
// [65, 0]  ???

Second, wchar_t contains UTF-32 code points, not UTF-16. Finally, in

let buffer = UnsafeMutablePointer<wchar_t>.allocate(capacity: _cString.count)
memcpy(buffer, _cString, _cString.count)

the allocation size does not include the trailing null character, and the copy copies _cString.count bytes, not characters.

All that can be fixed, but I would suggest a different API (similar to the String.withCString(_:) method):

extension String {
    /// Calls the given closure with a pointer to the contents of the string,
    /// represented as a null-terminated wchar_t array.
    func withWideChars<Result>(_ body: (UnsafePointer<wchar_t>) -> Result) -> Result {
        let u32 = self.unicodeScalars.map { wchar_t(bitPattern: $0.value) } + [0]
        return u32.withUnsafeBufferPointer { body($0.baseAddress!) }
    }
}

which can then be used like

let name = "John Doe"
let passKey = "secret"

name.withWideChars { wname in
    passKey.withWideChars { wpass in
        xlBookSetKeyW(book, wname, wpass)
    }
}

and the clean-up is automatic.

Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382