When extending torch with C++, you might want your C++ function to
return types that are not defined in torch.h,
or ones where no automatic casting from and to raw
pointers
has yet been implemented in lantern/types.h.
torchexport
can be extended to support custom types defined
in user packages, provided that you implement all the necessary
casting:
-
frow_raw
andmake_raw
: On thecsrc
side, you must specify how your type is to be converted to avoid*
pointer, as well as how to convert fromvoid*
to your custom type. Note that for C structs, you can also directly return the object without casting them to avoid*
. In such cases you can define these types in thecsrc/include/<package>/common.h
file. If this file exists, you will also have access to these types on the Rcpp side. -
Rcpp
type: On the Rcpp side ,you need to implement the casting betweenvoid*
and a custom Rcpp type that can manage the memory pointed by thisvoid*
. - Rcpp type to
SEXP
: if you ever return this type to R (as opposed to only using it in Rcpp) you need to implement casting from the Rcpp custom type to aSEXP
. -
SEXP
to Rcpp type: if you use this type as an argument on the R side, you must implement the casting fromSEXP
to your custom type.
Example
In this example we will implement the tensor_pair
type,
an alias for
std::tuple<torch::Tensor, torch::Tensor>
. This type
is not implemented in torch
, thus we need to implement it
ourselves. In fact, this type was needed in the torchsparse package;
that’s why the names of the files will sometimes start with
torchsparse_
.
The csrc
side
First we will create a file called torchparse_types.h
in
the csrc/include/torchsparse
directory. This file will
contain the declaration of your custom type and the declarations for the
functions that allows casting this type to and from void*
pointers.
For example, it can look like this:
#include <torch/torch.h>
// declares the alias, but could also be a `class CustomType`.
using tensor_pair = std::tuple<torch::Tensor,torch::Tensor>;
// In this namespace we declare the function that creates a `void*`
// from an instance of your type. This `void*` pointer must own all
// its memory.
namespace make_raw {
void* TensorPair (const tensor_pair& x);
}
// In this namespace we declare a function that takes a void* pointer and
// returns a reference to your type. It's a good idea to return by
// reference.
namespace from_raw {
tensor_pair& TensorPair (void* x);
}
Now that we declared the type and it’s casting functions, we will
implement these functions. The name of the file doesn’t matter here, but
it’s nice to use torchsparse_types.cpp
. This file lives in
csrc/src
. Don’t forget to add it to the
CMakeLists.txt
file so it also gets compiled.
The implementation of make_raw::TensorPair
and
from_raw::TensorPair
look like this:
#include <torch/torch.h>
#include "torchsparse/torchsparse_types.h"
#include <torchsparse/sparse.h>
#include <lantern/types.h>
namespace make_raw {
// This is mostly the same as:
// return (void*) new tensor_pair(x);
// but in a fancy C++ way.
void* TensorPair (const tensor_pair& x) {
return make_ptr<tensor_pair>(x);
}
}
// This simply tells the compiler to consider that `void*` is a pointer to
// `tensor_pair` and then returns this reference.
namespace from_raw {
tensor_pair& TensorPair (void* x) {
return *reinterpret_cast<tensor_pair*>(x);
}
}
// ---- there's more.
Additionally in this file we will implement functions that will allow us to free the memory pointed by this pointer, as well as tools to help us cast this type to something that we can return to R.
// Takes a void* pointer and deletes the memory it points to.
// First need to cast to the correct type.
// [[torch::export]]
void delete_tensor_pair(void* x) {
delete reinterpret_cast<tensor_pair*>(x);
}
// Extract a single Tensor from this type. This will allow us to
// convert this type into a list of tensors to pass to R.
// [[torch::export]]
torch::Tensor tensor_pair_get_first(tensor_pair x) {
return std::get<0>(x);
}
// [[torch::export]]
torch::Tensor tensor_pair_get_second(tensor_pair x) {
return std::get<1>(x);
}
We are done with the type implementation on the csrc
side. We can now implement a function that uses this type. For
example:
// [[torch::export(register_types=c("tensor_pair", "TensorPair", "void*", "torchsparse::tensor_pair"))]]
tensor_pair sparse_relabel(torch::Tensor col, torch::Tensor idx) {
return relabel(col, idx);
}
Note the arguments in the // [[torch::export]]
special
comment. Here we are telling torchexport
what to generate.
Namely, on encountering the type
tensor_pair
(first argument), it
should:
- Use
make_raw::TensorPair
andfrom_raw::TensorPair
to cast betweenvoid*
andtensor_pair
. (TensorPair
is the second argument.) - Use
void*
as the C type. In general it will always bevoid*
here, unless you can cast to another C type and still re-create the object. (void*
is the third argument.) - Use
torchsparse::tensor_pair
as the Rcpp type. (torchsparse::tensor_pair
is the third argument.) This we haven’t implemented yet; it’ll be our next step.
Note: You only need to register the type once, for a
single function that uses it. If we want to export other functions that
return tensor_pair
we won’t need to register the type again
in the [[torch::export]]
comment.
We can now cmake --build . --target install --parallel 8
to compile the csrc
library, and everything should go
fine.
The Rcpp
side
Now, on to the Rcpp side. We need to implement the custom Rcpp type
that will hold the void*
pointer returned from the
csrc
side, take care of the corresponding memory, and cast
to SEXP
when needed.
First we declare this type in
src/torchsparse_types.h
:
#pragma once
#include <torch.h>
namespace torchsparse {
class tensor_pair {
public:
// this is the slot to hold the void*
std::shared_ptr<void> ptr;
// the constructor from a void*
tensor_pair (void* x);
// casting operator Rcpp->SEXP
operator SEXP () const;
// returns the void* from the type.
void* get ();
};
}
The type having been declared, we can now implement its member
functions in src/torchsparse_types.cpp
:
#include <Rcpp.h>
#include "torchsparse_types.h"
#include "exports.h"
namespace torchsparse {
void* tensor_pair::get() {
return ptr.get();
}
// Creates a list of two torch::Tensor's from this object.
tensor_pair::operator SEXP () const {
Rcpp::List out;
out.push_back(rcpp_tensor_pair_get_first(*this));
out.push_back(rcpp_tensor_pair_get_second(*this));
return out;
}
// initialize the `ptr` slot and **very important** register the custom
// deleter `rcpp_delete_tensor_pair` that will free the pointer's memory
// once `ptr` is gone (and hence once the `torchsparse::tensor_pair` instance
// is gone).
tensor_pair::tensor_pair (void* x) : ptr(x, rcpp_delete_tensor_pair) {};
}
You should now be able to devtools::load_all()
and call
rcpp_sparse_relabel()
. This function will return a list
with two torch
Tensors.
In this case, we don’t need to provide functionality for casting from
a SEXP
to the Rcpp type. If we had to, we’d have to
implement the tensor_pair::tensor_pair (SEXP x)
constructor, and probably have a function similar to
rcpp_tensor_pair_get_first
, but doing things the other way
around: i.e., taking two torch
Tensors and returning a
tensor_pair
.
Finally, this tutorial is more like a set of notes (from when I was
implementing that functionality in torchsparse
) than a
“tutorial”. Please open an issue if something is not clear, and we will
be very happy to help!