Skip to content

Commit 5437be9

Browse files
committed
Updated the README, clarified some docstrings.
1 parent cf6c5be commit 5437be9

3 files changed

Lines changed: 153 additions & 74 deletions

File tree

README.md

Lines changed: 149 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
# Read RDS files in C++
1+
# Read RDS/RDA files in C++
22

33
![Unit tests](https://github.com/LTLA/rds2cpp/actions/workflows/run-tests.yaml/badge.svg)
44
![Documentation](https://github.com/LTLA/rds2cpp/actions/workflows/doxygenate.yaml/badge.svg)
55

66
## Overview
77

8-
This repository contains a header-only C++ library for reading and writing RDS files (created with `saveRDS()`) without the need to link to R's libraries.
8+
This repository contains a header-only C++ library for reading and writing RDS files (created with `saveRDS()`) or RDA files (created with `save()`) without the need to link to R's libraries.
99
In this manner, we can use RDS as a flexible data exchange format across different frameworks that have C++ bindings,
1010
e.g., [Python](https://github.com/biocpy/rds2py), [Javascript (via Wasm)](https://github.com/jkanche/scran.js).
1111
We currently support most user-visible data structures such as atomic vectors, lists, environments and S4 classes.
@@ -19,7 +19,7 @@ Given a path to an RDS file, the `parse_rds()` function will return a pointer to
1919

2020
// Returns an object containing the file information,
2121
// e.g., R version used to read/write the file.
22-
auto file_info = rds2cpp::parse_rds(fpath, {});
22+
auto file_info = rds2cpp::parse_rds(fpath, rds2cpp::ParseRdsOptions());
2323

2424
// Get the pointer to the actual R object.
2525
const auto& ptr = file_info->object;
@@ -31,8 +31,7 @@ For example, if we wanted to process integer vectors:
3131
```cpp
3232
if (ptr->type() == rds2cpp::SEXPType::INT) {
3333
auto iptr = static_cast<const rds2cpp::IntegerVector*>(ptr.get());
34-
const auto& values = iptr->data; // vector of int32_t's.
35-
const auto& attr_names = iptr->attributes.names; // vector of attribute names.
34+
const std::vector<std::int32_t>& values = iptr->data;
3635
}
3736
```
3837

@@ -48,16 +47,26 @@ if (ptr->type() == rds2cpp::SEXPType::VEC) {
4847
auto lptr = static_cast<const rds2cpp::GenericVector*>(ptr.get());
4948
const auto& elements = lptr->data; // vector of pointers to list elements.
5049

51-
const auto& attr = lptr->attributes;
52-
const auto& attr_names = sptr->attributes.names;
53-
const auto& attr_values = sptr->attributes.values;
54-
55-
// Scanning for the list names.
56-
auto nIt = std::find(attr_names.begin(), attr_names.end(), std::string("names"));
57-
if (nIt != attr_names.end()) {
58-
size_t nindex = nIt - attr_names.begin();
59-
if (attr_values[nindex]->type() == rds2cpp::SEXPType::STR) {
60-
auto nptr = static_cast<const rds2cpp::StringVector*>(attr_values[nindex].get());
50+
for (const auto& attr : lptr->attributes) {
51+
// Symbols are referenced by their position in the 'symbols' vector.
52+
const auto& attr_name = file_info.symbols[attr.name.index].name;
53+
54+
if (attr_name == "names") {
55+
if (attr.value->type() != rds2cpp::SEXPType::STR) {
56+
// Just adding some protection for weird objects.
57+
throw std::runtime_error("oops, names should be strings!");
58+
}
59+
60+
auto nptr = static_cast<const rds2cpp::StringVector*>(attr.value.get());
61+
for (const auto& str : nptr->value) {
62+
if (!str.value.has_value()) {
63+
throw std::runtime_error("oops, names should not be missing!");
64+
}
65+
66+
const std::string& str_value = *(str.value); // value of the string.
67+
const auto& str_enc = str.encoding; // encoding of the string.
68+
// Do something with the list names...
69+
}
6170
}
6271
}
6372
}
@@ -71,8 +80,11 @@ if (ptr->type() == rds2cpp::SEXPType::S4) {
7180
auto sptr = static_cast<const rds2cpp::S4Object*>(ptr.get());
7281
sptr->class_name;
7382
sptr->package_name;
74-
const auto& slot_names = sptr->attributes.names;
75-
const auto& slot_values = sptr->attributes.values;
83+
84+
for (const auto& slot : sptr->attributes) {
85+
const auto& slot_name = file_info.symbols[slot.name.index].name;
86+
const auto& slot_val = *(slot.value); // Do something with the slot value...
87+
}
7688
}
7789
```
7890

@@ -81,9 +93,13 @@ These should be treated as file-specific globals that may be referenced one or m
8193

8294
```cpp
8395
if (ptr->type() == rds2cpp::SEXPType::ENV) {
84-
const auto& env = file_info->environments[eptr->index];
85-
const auto& vnames = env.variable_names;
86-
const auto& vvalues = env.variable_values;
96+
auto eptr = static_cast<const rds2cpp::EnvironmentIndex*>(ptr.get());
97+
const auto& env = file_info.environments[eptr->index];
98+
99+
for (const auto& var = env.variables) {
100+
const auto& var_name = file_info.symbols[var.name.index].name;
101+
const auto& var_value = *(var.value); // Do something with the variable...
102+
}
87103
}
88104
```
89105

@@ -107,47 +123,64 @@ auto vec = new rds2cpp::IntegerVector;
107123
file_info.object.reset(vec);
108124

109125
// Storing data in the integer vector.
110-
vec->data = std::vector<int32_t>{ 0, 1, 2, 3, 4, 5 };
126+
vec->data = std::vector<std::int32_t>{ 0, 1, 2, 3, 4, 5 };
111127

112-
rds2cpp::write_rds(file_info, "some_file_path.rds");
128+
rds2cpp::write_rds(file_info, "some_file_path.rds", rds2cpp::WriteRdsOptions());
113129
```
114130
115131
Here's a more complicated example that saves a sparse matrix (as a `dgCMatrix` from the **Matrix** package) to file.
116132
117133
```cpp
118134
rds2cpp::RdsFile file_info;
119-
auto ptr = new rds2cpp::S4Object;
120-
file_info.object.reset(ptr);
121-
auto& obj = *ptr;
135+
auto ptr = std::make_unique<rds2cpp::S4Object>();
122136
137+
auto& obj = *ptr;
123138
obj.class_name = "dgCMatrix";
124139
obj.package_name = "Matrix";
125140
126-
auto ivec = new rds2cpp::IntegerVector;
127-
obj.attributes.add("i", ivec);
128-
ivec->data = std::vector<int32_t>{ 6, 8, 0, 3, 5, 6, 0, 1, 3, 7 };
129-
130-
auto pvec = new rds2cpp::IntegerVector;
131-
obj.attributes.add("p", pvec);
132-
pvec->data = std::vector<int32_t>{ 0, 0, 2, 3, 4, 5, 6, 8, 8, 8, 10 };
133-
134-
auto xvec = new rds2cpp::DoubleVector;
135-
obj.attributes.add("x", xvec);
136-
xvec->data = std::vector<double>{ 0.96, -0.34, 0.82, -2, -0.72, 0.39, 0.16, 0.36, -1.5, -0.47 };
137-
138-
auto dims = new rds2cpp::IntegerVector;
139-
obj.attributes.add("Dim", dims);
141+
auto ivec = std::make_unique<rds2cpp::IntegerVector>();
142+
ivec->data = std::vector<std::int32_t>{ 6, 8, 0, 3, 5, 6, 0, 1, 3, 7 };
143+
obj.attributes.emplace_back(
144+
rds2cpp::register_symbol("i", rds2cpp::StringEncoding::UTF8, file_info.symbols),
145+
std::move(ivec)
146+
);
147+
148+
auto pvec = std::make_unique<rds2cpp::IntegerVector>();
149+
pvec->data = std::vector<std::int32_t>{ 0, 0, 2, 3, 4, 5, 6, 8, 8, 8, 10 };
150+
obj.attributes.emplace_back(
151+
rds2cpp::register_symbol("p", rds2cpp::StringEncoding::UTF8, file_info.symbols),
152+
std::move(pvec)
153+
);
154+
155+
auto xvec = std::make_unique<rds2cpp::DoubleVector>();
156+
xvec->data = std::vector<double>{ .96, -.34, .82, -2., -.72, .39, .16, .36, -1.5, -.47 };
157+
obj.attributes.emplace_back(
158+
rds2cpp::register_symbol("x", rds2cpp::StringEncoding::UTF8, file_info.symbols),
159+
std::move(xvec)
160+
);
161+
162+
auto dims = std::make_unique<rds2cpp::IntegerVector>();
140163
dims->data = std::vector<int32_t>{ 10, 10 };
141-
142-
auto dimnames = new rds2cpp::GenericVector;
143-
obj.attributes.add("Dimnames", dimnames);
144-
dimnames->data.emplace_back(new rds2cpp::Null);
145-
dimnames->data.emplace_back(new rds2cpp::Null);
146-
147-
auto factors = new rds2cpp::GenericVector;
148-
obj.attributes.add("factors", factors);
149-
150-
rds2cpp::write_rds(file_info, "my_matrix.rds");
164+
obj.attributes.emplace_back(
165+
rds2cpp::register_symbol("Dim", rds2cpp::StringEncoding::UTF8, file_info.symbols),
166+
std::move(dims)
167+
);
168+
169+
auto dimnames = std::make_unique<rds2cpp::GenericVector>();
170+
dimnames->data.emplace_back(new Null);
171+
dimnames->data.emplace_back(new Null);
172+
obj.attributes.emplace_back(
173+
rds2cpp::register_symbol("Dimnames", rds2cpp::StringEncoding::UTF8, file_info.symbols),
174+
std::move(dimnames)
175+
);
176+
177+
obj.attributes.add(
178+
rds2cpp::register_symbol("factors", rds2cpp::StringEncoding::UTF8, file_info.symbols),
179+
std::make_unique<rds2cpp::GenericVector>()
180+
);
181+
182+
file_info.object = std::move(ptr);
183+
rds2cpp::write_rds(file_info, "my_matrix.rds", {});
151184
```
152185

153186
We can also create environments by registering the environment before creating indices to it.
@@ -156,20 +189,74 @@ We can also create environments by registering the environment before creating i
156189
rds2cpp::RdsFile file_info;
157190

158191
// Creating an environment with a 'foo' variable containing c('bar', NA, 'whee')
159-
file_info.environments.resize(1);
160-
auto& current_env = file_info.environments[0];
192+
file_info.environments.emplace_back();
193+
auto& current_env = file_info.environments.back();
161194

162-
auto sptr = new rds2cpp::StringVector;
163-
current_env.add("foo", sptr);
164-
sptr->add("bar");
165-
sptr->add(); // NA string
166-
sptr->add("whee");
195+
auto sptr = std::make_unique<rds2cpp::StringVector>();
196+
sptr->data.emplace_back("bar", rds2cpp::StringEncoding::UTF8);
197+
sptr->data.emplace_back(); // NA string.
198+
sptr->data.emplace_back("whee", rds2cpp::StringEncoding::ASCII);
167199

168-
// Referencing the environment:
169-
auto eptr = new rds2cpp::EnvironmentIndex(0);
170-
file_info.object.reset(eptr);
200+
// The object is just a reference to the first environment:
201+
file_info.object.reset(new rds2cpp::EnvironmentIndex(0));
171202

172-
rds2cpp::write_rds(file_info, "my_env.rds");
203+
rds2cpp::write_rds(file_info, "my_env.rds", {});
204+
```
205+
206+
## Reading/writing RDA files
207+
208+
RDA files (a.k.a., Rdata) use the same serialization format as RDS files.
209+
The only difference is that the object is always a list of name/object pairs.
210+
We can read these objects into memory with the `parse_rda()` function:
211+
212+
```cpp
213+
auto file_info = rds2cpp::parse_rda(fpath, rds2cpp::ParseRdaOptions());
214+
215+
for (const auto& obj : file_info.objects) {
216+
const auto& obj_name = file_info.symbols[obj.name.index].name;
217+
switch (obj.value->type()) {
218+
case rds2cpp::SEXPType::INT:
219+
// This is an integer vector...
220+
break;
221+
case rds2cpp::SEXPType::STR:
222+
// This is a character vector...
223+
break;
224+
default:
225+
// and so on...
226+
}
227+
}
228+
```
229+
230+
Similarly, we can write name/object pairs into an RDA file.
231+
232+
```cpp
233+
#include <numeric>
234+
235+
auto ivec = std::make_unique<rds2cpp::IntegerVector>(5);
236+
std::iota(ivec->data.begin(), ivec->data.end(), 1);
237+
238+
auto list = std::make_unique<rds2cpp::GenericVector>(2);
239+
list->data[0].reset(new Null);
240+
list->data[1].reset(new rds2cpp::LogicalVector(10));
241+
242+
auto svec = std::make_unique<rds2cpp::StringVector>(1);
243+
svec->data[0].value = "FOOBAR";
244+
245+
rds2cpp::RdaFile file_info;
246+
file_info.objects.emplace_back(
247+
rds2cpp::register_symbol("alpha", rds2cpp::StringEncoding::UTF8, file_info.symbols),
248+
std::move(ivec)
249+
);
250+
file_info.objects.emplace_back(
251+
rds2cpp::register_symbol("bravo", rds2cpp::StringEncoding::UTF8, file_info.symbols),
252+
std::move(list)
253+
);
254+
file_info.objects.emplace_back(
255+
rds2cpp::register_symbol("charlie", rds2cpp::StringEncoding::UTF8, file_info.symbols),
256+
std::move(svec)
257+
);
258+
259+
rds2cpp::write_rda(file_info, "my_env.Rda", rds2cpp::WriteRdaOptions());
173260
```
174261
175262
## Building projects
@@ -221,9 +308,8 @@ target_link_libraries(mylib INTERFACE ltla::rds2cpp)
221308

222309
If you're not using CMake, the simple approach is to just copy the files in the [`include/`](include) subdirectory -
223310
either directly or with Git submodules - and include their path during compilation with, e.g., GCC's `-I`.
224-
225-
You'll also need to add the [**byteme**](https://github.com/LTLA/byteme) header-only library to the compiler's search path.
226-
Normally, when using CMake, this is automatically linked to Zlib; this will now need to be done manually.
311+
You'll need to add the various dependencies listed in [`extern/CMakeLists.txt`](extern/CMakeLists.txt) to the compiler's search path.
312+
You'll also need to link to Zlib.
227313

228314
## Known limitations
229315

docs/Doxyfile

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -793,8 +793,6 @@ WARN_LOGFILE =
793793
INPUT = ../include/rds2cpp/StringEncoding.hpp \
794794
../include/rds2cpp/RObject.hpp \
795795
../include/rds2cpp/SEXPType.hpp \
796-
../include/rds2cpp/Environment.hpp \
797-
../include/rds2cpp/Symbol.hpp \
798796
../include/rds2cpp/RdsFile.hpp \
799797
../include/rds2cpp/Version.hpp \
800798
../include/rds2cpp/RdaFile.hpp \
@@ -886,14 +884,7 @@ RECURSIVE = YES
886884
# Note that relative paths are relative to the directory from which doxygen is
887885
# run.
888886

889-
EXCLUDE = ../include/scran/utils/block_indices.hpp \
890-
../include/scran/differential_analysis/auc.hpp \
891-
../include/scran/differential_analysis/cohens_d.hpp \
892-
../include/scran/differential_analysis/lfc.hpp \
893-
../include/scran/differential_analysis/delta_detected.hpp \
894-
../include/scran/differential_analysis/Factory.hpp \
895-
../include/scran/dimensionality_reduction/pca_utils.hpp \
896-
../include/scran/feature_selection/blocked_variances.hpp
887+
EXCLUDE =
897888

898889
# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
899890
# directories that are symbolic links (a Unix file system feature) are excluded

include/rds2cpp/RObject.hpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,15 @@ struct SymbolIndex final : public RObject {
104104

105105
/**
106106
* @param name Name of the symbol, see `Symbol::name`.
107-
* @param name Encoding of the symbol, see `Symbol::encoding`.
107+
* @param encoding Encoding of the symbol, see `Symbol::encoding`.
108108
* @param symbols Vector containing the global set of symbols, typically `RdsFile::symbols` or `RdaFile::symbols`.
109109
*
110110
* @return A new symbol is added to `symbols` and a `SymbolIndex` is returned that points to the new entry of `symbols`.
111111
*
112112
* This is a convenient helper to register new symbols in `RdsFile` prior to calling `write_rds()` (or to `RdaFile` before `write_rda()`).
113113
* It returns a `SymbolIndex` that can be used in various fields like `LanguageArgument::name`, `PairListElement::tag`, `EnvironmentVariable::name`, etc.
114+
* For efficiency, users can re-use the same `SymbolIndex` in multiple locations within the same `RdsFile` and `RdaFile` to reference this combination of `name` and `encoding`.
115+
* However, it is perfectly fine to create a new `SymbolIndex` in such cases as any duplicated combinations will be automatically removed by `write_rds()` and `write_rda()`.
114116
*/
115117
inline SymbolIndex register_symbol(std::string name, StringEncoding encoding, std::vector<Symbol>& symbols) {
116118
auto idx = symbols.size();

0 commit comments

Comments
 (0)