2011-11-01
Using Records and C structs in ATS
ATS has record types which are like tuples but each item is referenced via a name. They closely approximate C structs and in the generated C code are represented in this way. The following uses a record to hold x
and y
values representing a point
on a 2D plane:
fun print_point (p: @{x= int, y= int}): void =
printf("%d@%d\n", @(p.x, p.y))
implement main () = {
val p1 = @{x=10, y=20}
val () = print_point (p1)
}
A literal record object in this example is created using the @{ ... }
syntax. Dereferencing record fields is done using '.
'. The generated C code shows that the representation for the record is a C struct:
typedef struct {
ats_int_type atslab_x ;
ats_int_type atslab_y ;
} anairiats_rec_0 ;
The @
syntax used for the record literal and type marks the record as a 'flat' record. Flat records have value semantics and variables of this type have a size in bytes equivalent to the size of the underlying C structure. This is shown by the generated C code for the main
function above:
ATSlocal (anairiats_rec_0, tmp4) ;
__ats_lab_mainats:
tmp4.atslab_x = 10 ;
tmp4.atslab_y = 20 ;
/* tmp3 = */ print_point_0 (tmp4) ;
Here the variable tmp4
is the p1
in our ATS code. It is an instance of the C struct representing the record and is created on the stack. It is initialized and passed to the print_point
function by value:
ats_void_type
print_point_0 (anairiats_rec_0 arg0) {
ATSlocal (ats_int_type, tmp1) ;
ATSlocal (ats_int_type, tmp2) ;
...
}
Records can also be defined using a '{...}
syntax (note the '
instead of @
) for boxed records which are heap allocated and the memory managed by the garbage collector. Boxed records have pointer semantics and have a size in bytes equivalent to the size of a pointer:
implement main () = {
val x = sizeof<@{x=int}>
val y = sizeof<'{x=int}>
val a = int_of_size x
val b = int_of_size y
val () = printf ("%d %d\n", @(a, b))
}
This outputs "4 8" showing the flat type as having the size of an int
and the boxed type having the size of a pointer. Most usage of records in ATS I've done is using flat records as I tend to avoid the use of the garbage collector.
To pass a reference to a flat record so it can be modified by a function you need to mark the function argument as 'by reference' using &
:
typedef point = @{x= int, y= int}
fun print_point (p: point): void =
printf("%d@%d\n", @(p.x, p.y))
fun add1 (p: &point): void = {
val () = p.x := p.x + 1
val () = p.y := p.y + 1
}
implement main () = {
var p1 = @{x=10, y=20}
val () = add1 (p1)
val () = print_point (p1)
}
In this example I use typedef
to create a type alias for the record so I can refer to the type as point
. The add1
function takes a point
by reference (as indicated by the &
prefix). This works like C++ reference arguments. The function effectively takes a pointer to an instance of the struct and can modify the instance passed to the function. For this to work the point
passed to the function must be an lvalue
. That is, it must be mutable. This is done in ATS by making it a var
vs a val
.
Note that it's a type error to create an uninitialized point
object and pass it to add1
. For example, the following gives a type check error:
implement main () = {
var p1: point?
val () = add1 (p1)
val () = print_point (p1)
}
Types that are unintialized have a ?
suffix added to it. The type of p1
, due to it being unintialized, is point?
. Since add1
takes a point
this fails type checking. Initializing it allows it to pass:
implement main () = {
var p1: point
val () = p1.x := 5
val () = p1.y := 10
val () = add1 (p1)
val () = print_point (p1)
}
As well as passing by reference to functions you can pass a pointer and deal with the pointer management directly. This requires using the proof system and I hope to go through dealing with pointers and their associated proofs in a later post:
fun add1 {l:agz} (pf: !point @ l | p: ptr l): void = {
val () = p->x := p->x + 1
val () = p->y := p->y + 1
}
implement main () = {
var p1 = @{x=10, y=20}
val () = add1 (view@ p1 | &p1)
val () = print_point (p1)
}
When interfacing with C API's you often have to deal with C structures. In ATS you can declare a type as being defined as a struct in C with a matching ATS record definition so that ATS can be used to access the struct. The following example shows a struct declared in C, a function that uses it in C, and how this is wrapped in ATS:
%{^
typedef struct Point {
int x;
int y;
} Point;
void print_point (Point* p) {
printf("%d@%d\n", p->x, p->y);
}
%}
typedef point = $extype_struct "Point" of {x= int, y= int}
extern fun print_point (p: &point): void = "mac#print_point"
implement main () = {
var p1: point
val () = p1.x := 10;
val () = p1.y := 20;
val () = print_point (p1)
}
The $extype_struct
keyword creates a type that is represented by a C struct with the given name. By using the of {x= int, y= int}
suffix we define the record layout as seen via ATS. This will stop ATS from creating its own structure to map the type and instead uses the C structure. The generated C code for the main
function looks like:
ATSlocal (Point, tmp1) ;
__ats_lab_mainats:
/* Point tmp1 ; */
ats_select_mac(tmp1, x) = 10 ;
ats_select_mac(tmp1, y) = 20 ;
print_point ((&tmp1)) ;
Note it uses the C struct type directly. I use this approach in my 0MQ wrapper to wrap the zmq_msg_t
and zmq_pollitem_t
structures.