1

I am working with a library written in C, Firebird, in my Swift project. I have a fixed size C array, of size 1, in a struct

typedef struct
{
    ...
    ISC_SHORT sqln /* number of fields allocated */
    ISC_SHORT sqld;     /* actual number of fields */
    XSQLVAR sqlvar[1];  /* first field address */
} XSQLDA;

Imported in Swift as a tuple

public struct XSQLDA {
    ...
    public var sqln: ISC_SHORT /* number of fields allocated */
    public var sqld: ISC_SHORT /* actual number of fields */
    public var sqlvar: (XSQLVAR) /* first field address */
}

The purpose of my code is to retrieve data describing a statement. For the statement SELECT foo, bar FROM MY_TABLE;, XSQLDA sqlvar array will contains two XSQLVAR, with description of the foo column and bar column.

In C, the struct needs an allocated size of sizeof (XSQLDA) + (n - 1) * sizeof (XSQLVAR), as defined in the following macro in the library header

#define XSQLDA_LENGTH(n)    (sizeof (XSQLDA) + (n - 1) * sizeof (XSQLVAR))

Creating a XSQLDA struct in Swift

let pointer = UnsafeMutableRawPointer.allocate(
            byteCount: MemoryLayout<XSQLDA>.stride + (Int(n) - 1) * MemoryLayout<XSQLVAR>.stride,
            alignment: MemoryLayout<XSQLVAR>.alignment)

var xsqldaPointer = pointer.bindMemory(to: XSQLDA.self, capacity: 1)

Passing this pointer to this struct to a C function of the library

isc_dsql_describe(&self.status, &self.statement, 1, xsqldaPointer);

This function will populate the sqlvar array of XSQLDA structure.

After this call, I iterate over the elements in the array

var sqlVariable: XSQLVAR = .init()
for i in 0...Int(xsqldaPointer.pointee.sqld) {
    withUnsafePointer(to: &xsqldaPointer.pointee.sqlvar) { ptr in
        sqlVariable = ptr.advanced(by: i).pointee
        print(String(cString: &sqlVariable.sqlname.0))
    }
}

Over the iterations, the address of the ptr, aka sqlvar th element, even advanced, stay the same. It seems that Swift can only access the first one.

Running the same function in C works properly. I looked in the memory with BitSlicer, and there are only one element when I run the Swift code, and n elements with the C code.

According to the documentation, the C code is

for (i=0, var = out_sqlda->sqlvar; i < out_sqlda->sqld; i++, var++)
for (i = 1; i < my_struct_instance->count; i++) {
    var = my_struct_instance->arr[i];
}

I think the problem is coming from memory protection from Swift, but not sure. Any idea ?

I don't understand why to declare a fixed size array of size 1, which will contains an unknown number of elements. Why not a pointer ?

The function documentation is there, and a code example of this function is there. I'm aware that the documentation is about Interbase, by embarcadero. I looked the C code examples in the github, and the C API guide referenced by Firebird there

ugocottin
  • 64
  • 8
  • 2
    What are `my_struct` and `my_function` in the documentation and in the code example? – OOPer Jan 31 '21 at 01:48
  • Welcome to the site! Very thorough and well asked question, well done. What is the data structure declaration, in C? – Alexander Jan 31 '21 at 01:50
  • 1
    Your code seems a bit too obfuscated. If you're calling Firebird functions, could you please use the actual types and names as defined in the Firebird headers. And a word of warning: you're linking to the InterBase documentation. Firebird was forked from InterBase 21 years ago, and they have been separate products since. – Mark Rotteveel Jan 31 '21 at 08:36
  • `Why not a pointer ?` - for example, because code should be documentation. The intention, the semantics - is to be array, so that is how it got declared. – Arioch 'The Jan 31 '21 at 12:41
  • Thanks for your feedback. I've updated my question according your comments. – ugocottin Jan 31 '21 at 14:22
  • What do you get if you add `print(i, ptr.advanced(by: i))` before `sqlVariable = ptr.advanced(by: i).pointee` ? – OOPer Jan 31 '21 at 19:24
  • 1
    Basically it is a question of correctly marshalling from high-level language to low-level language, of cottectly defining low-level bytes-oriented structures in high-level languages trying to be bytes-agnostic. This is an interesting question about Swift - for Swifters. I suggest you to find communities that do exactly that, redefining well-known APIs in idiomatic Swift, and work it out with them. Alternatively, make a bridge DLL in C++, that would consume low-level FB API and expose some high-level API for Swift. – Arioch 'The Feb 01 '21 at 09:11

2 Answers2

2

The problem did not come from the for loop, but from the memory allocation.

let pointer = UnsafeMutableRawPointer.allocate(
            byteCount: MemoryLayout<XSQLDA>.stride + (Int(n) - 1) * MemoryLayout<XSQLVAR>.stride,
            alignment: MemoryLayout<XSQLVAR>.alignment)

var xsqldaPointer = pointer.bindMemory(to: XSQLDA.self, capacity: 1)

The previous code allocate memory bounded to XSQLDA type, but the allocated memory will contain the XSQLVAR variables.

The proper way to allocate a XSQLDA is the following:

let pointer = UnsafeMutableRawPointer
    .allocate(byteCount: XSQLDA_LENGTH(numberOfFields), alignment: 1)
    .assumingMemoryBound(to: XSQLDA.self)

With the following function:

func XSQLDA_LENGTH(_ numberOfFields: Int16) -> Int {
    return MemoryLayout<XSQLDA>.size + Int(numberOfFields - 1) * MemoryLayout<XSQLVAR>.size
}
ugocottin
  • 64
  • 8
0

I have zero Swift knowledge (and my C knowledge is also limited), but the correct approach is likely to define sqlvar as an array, not as a scalar field. The XSQLVAR sqlvar[1] defines an array that defaults to a single value, but when XSQLDA_LENGTH(n) is called with n > 1, it actually defines an array of size n.

If possible, you should make it an array in your Swift tuple, and allocate n XSQLVARs in that array, and make sure that sqln is set to n. Then, after using isc_dsql_prepare, and sqld > sqln, you need to reallocate an XSQLDA with n equal to sqld and use isc_dsql_describe to populate it.

Alternatively, you can forego this and always use a XSQLDA of size 1 on prepare, and instead use isc_dsql_sql_info to 'manually' create appropriately sized XSQLDA descriptors for input and output fields.

This is the approach I take in Jaybird, the Java database driver for Jaybird. There, I either create an XSQLDA with size 1 (to use on prepare), or one with the right number of parameters/fields to manually create the descriptors based on the result of isql_dsql_sql_info, and allocate the necessary memory (that is because the way the Java implementation works actually shadows a native memory structure instead of directly accessing the native memory).

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197