Go Slices
In previous related posts, we have seen how to call go from R, use go in R 📦 and use go strings.
This post is about using go slices, which are somewhat similar to R vectors, i.e. their data are contiguous in memory, and they are self aware of their length and capacity.
This post has an associated playground repo in the rstats-go organisation. I’m using the same layout as the previously mentionned posts:
- pure
go
code in ago
package “slice” - mix of
go
andc
code usingcgo
in the “main” package. Most of this will eventually be automatically generated somehow.
romain@sherlock ~/git/rstats-go/_playground_slice $ tree src
src
├── Makevars
├── go
│ └── src
│ ├── main
│ │ ├── main.c
│ │ └── main.go
│ └── slice
│ └── slice.go
├── goslice.h
└── goslice.so
4 directories, 6 files
The easy part : using R vectors in go
Let’s first have a look at the “easy” part, using R vectors in go, as go slices. We have these two go functions that take slice of int32
and float64
(which are the relevant data types for using R integer
and numeric
vectors. )
func SumInt( x []int32 ) int32 {
var sum int32
for _,v := range x {
sum += v
}
return sum
}
func SumDouble( x []float64 ) float64 {
var sum float64
for _,v := range x {
sum += v
}
return sum
}
They belong to our pure go package “slice”.
We then need the corresponding proxy functions in the “main” package.
//export SumInt
func SumInt( x []int32 ) int32 {
return slice.SumInt(x)
}
//export SumDouble
func SumDouble( x []float64 ) float64 {
return slice.SumDouble(x)
}
Nothing too fancy here, just go functions of the same name (but in package main) that are marked as exported to the C side via //export
.
The next part is the SEXP compatible functions. This does strict checks to assert that we give it the correct kind of vector, i.e. you don’t get automatic conversion as in Rcpp
because I now believe this was a bad idea.
SEXP sum_int( SEXP x ){
if( TYPEOF(x) != INTSXP ) error("expecting an integer vector") ;
GoSlice gox = { INTEGER(x), LENGTH(x), LENGTH(x) } ;
return ScalarInteger( SumInt(gox) ) ;
}
SEXP sum_double( SEXP x ){
if( TYPEOF(x) != REALSXP ) error("expecting a numeric vector") ;
GoSlice gox = { REAL(x), LENGTH(x), LENGTH(x) } ;
return ScalarReal( SumDouble(gox) ) ;
}
This uses the type GoSlice
from cgo
to embed information from the R vectors into something that go can use as a slice.
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
Then we have the typical .Call
wrappers:
#' @export
sum_int <- function(x) {
.Call("sum_int", x, PACKAGE = "goslice")
}
#' @export
sum_double <- function(x) {
.Call("sum_double", x, PACKAGE = "goslice")
}
So now we may use go to sum integer or numeric vectors :
sum_int( 1:10 )
## [1] 55
sum_double( rnorm(10) )
## [1] -0.8462124
The not so easy part : sending back go slices as R vectors
The other way around is trickier. We want to make some slice in go and send it back to the R side as an R vector. We have to copy the data into an R object, we can’t just borrow it because it may be reclaimed by go’s garbage collector.
The go function we use takes an integer and makes a slice of that size with some numbers inside.
func Numbers( n int32 ) []int32 {
a := make( []int32, n) ;
for i := int32(0); i<n; i++ {
a[i] = 2*i ;
}
return a ;
}
Up to that point, this is just pure go code. It uses int32
because that’s what R calls integers.
The associated proxy function is more involved this time
//export Numbers
func Numbers( n int32 ) C.SEXP {
// call a go function and get a slice
res := slice.Numbers(n)
// handle the raw data from the slice to the C side and let it build an
// R object from it
return C.IntegerVectorFromGoSlice( unsafe.Pointer(&res[0]), C.int(len(res)) ) ;
}
The first step just calls the go function to get the slice we want to send back to R.
The second step calls a C function IntegerVectorFromGoSlice
we will provide. As the name implies, IntegerVectorFromGoSlice
makes an integer vector from a go slice. It takes raw (unsafe) pointer to the start of the slice and its length and return an R object (a SEXP). The function allocates the vector using allocVector
, copies the memory with memmove
and returns the object.
SEXP IntegerVectorFromGoSlice( void* data, int n ){
SEXP res = allocVector( INTSXP, n) ;
memmove( INTEGER(res), data, sizeof(int)*n ) ;
return res ;
}
Finally, we can write the SEXP
compatible function:
SEXP numbers( SEXP n ){
if( TYPEOF(n) != INTSXP || LENGTH(n) != 1 ) error("expecting a single integer") ;
return Numbers( INTEGER(n)[0] ) ;
}
and the .Call
wrapper :
#' @export
numbers <- function(n){
.Call("numbers", n, PACKAGE = "goslice")
}
So now we can finally generate that sequence of numbers:
numbers(10L)
## [1] 0 2 4 6 8 10 12 14 16 18
Enjoy the slices