FFI exercise: Binding to LevelDB
In this exercise your task is to create a Rust binding, or foreign function interface (FFI) to the LevelDB database library. Typically, and also in this exercise, "foreign" means "C".
You will learn how to:
- handle pointers passed to or from the foreign language
- use low-level C bindings
- utilize Rust's ownership system to provide safety on top ofΒ those raw primitives
Prerequisites
This exercise requires knowledge of a few Rust and C concepts:
- Ownership & Borrowing
- Lifetime annotations
- Traits
- Iterators
- Casting
- Error handling with
Result
andOption
- Unsafe Rust and raw pointers
- C strings represented as null-terminated chunks of 8-bit
char
s - using C pointers as function parameters for returning multiple results in one call
Conceptually, LevelDB is a key-value store, AKA a persistent dictionary or map.
Mental exercise: Ownership in C
How is ownership handled in C?
Hint
When does a "double free" occur?Solution
Ownership is handled only informally - typically an API's documentation and/or function names (e.g. "create", "new") will indicate whether you are responsible to free up the memory passed to you, or it is somebody else's problem. Unclear ownership (via multiple mutable pointers to the same memory), API misunderstandings or other kinds of human error can easily lead to memory being freed too often or too little, resulting in crashes or leaks.Setup
Required dependencies
Binding to C is divided into two parts: a minimal low-level interface (called "sys crate") and a higher level wrapper crate.
The sys crate is responsible for linking to the C library and exposing its contents unchanged.
The higher level crate uses the sys crate to provide a more Rust-friendly interface by safely wrapping the inherently unsafe
raw parts.
Writing a sys crate yourself is beyond the scope of this exercise. We will be using the leveldb-sys
crate provided for you.
Additionally, you'll also need the libc
crate which provides C types and other required definitions.
To use them, you will need to specify leveldb-sys
and libc
in the [dependencies]
section of your project's Cargo.toml
.
# in Cargo.toml
[dependencies]
+leveldb-sys = "*"
+libc = "*"
β Note on specifying dependencies using the asterisk
Declaring a dependency as * ("any version") is generally not recommended - we're doing it here to prevent stale version numbers and assume it's safe because these specific crates are very unlikely to introduce breaking changes. An alternative would be to install cargo-edit and then use the contained cargo-add command to add a dependency on the most recent release version:$ cargo add leveldb-sys $ cargo add libc
Building leveldb-sys
requires CMake and a C++ compiler (gcc, clang, Visual Studio etc.) to be installed on your system.
π Should you ever need to write your own sys crate you can find instructions for doing so here.
Exercise: Opening and closing a database
Preparation
Conceptually, a LevelDB database is a directory (the "database name") where all its required files are stored. "Opening" it means passing a path (whose last part is the database name) and some options to leveldb_open
, notably create_if_missing
, which will create the database directory in case it does not exist.
You'll also need these functions and enums from the leveldb-sys
crate:
leveldb_t
: opaque handle representing an opened database. The handle is thread-safe.leveldb_open
: opens a database, returningleveldb_t
leveldb_close
: closes aleveldb_t
for a clean shutdownleveldb_options_t
: opaque handle representing database optionsleveldb_options_create
: create an instance of this options structleveldb_options_destroy
: deallocatesleveldb_options_t
leveldb_options_set_create_if_missing
: sets thecreate_if_missing
flag onleveldb_options_t
.
The LevelDB C header documents some conventions used by its implementation.
Your tasks
If you're stuck, check out the Help and hints below!
β Create a new library package project for this group of exercises. Add the required dependencies.
β Implement functions for:
- opening a database, forwarding the "create if missing" flag
- closing it again
β
Refactor your code and create wrapping structs for the raw leveldb_t
and leveldb_options_t
types, taking care of the required cleanup operations.
- It should not be possible to forget the cleanup.
- The struct member storing the pointer should use an appropriate Rust type to express the fact that it exclusively stores non-
null
pointers.
β
Create a Database
struct that manages high-level operations.
Polishing your solution
How's your error handling? open
should not panic - return a custom error instead. To keep things concise, make use of the question mark operator (see also the following section on using it in tests). Convert errors where necessary.
β Test the success and error cases.
- Note: you can provoke an error by trying to open a database folder to which you don't have write access
β
What type(s) does your open
function accept as database names? What would offer the most flexibility? Get some inspiration from std::fs::File.
β Include the underlying LevelDB error message string in your error type. Mind the ownership!
β Rust Strings are valid UTF-8. What issues might occur
- as opposed to C strings?
- regarding valid file system characters? (a LevelDB database is a directory!)
The most straightforward parameter type for the database name is &str
(why not String
?). Since we're dealing with paths, what would be an alternative that still has the convenience of using string literals on the caller side?
β Change your function signature accordingly.
Hint
Which trait bounding provides the required functionality?Help and hints
- Getting mysterious crashes? Are you maybe dereferencing an uninitialized raw pointer?
c_uchar
is an alias foru8
, and Rust booleans are safe to castas u8
.- You need to pass an error pointer to
open
. The C library potentially mutates it, and initially it should point tonull
. For creating a suitable pointer Rust provides you withstd::ptr::null_mut
. - A null pointer is equal (
==
) to any other null pointer. Raw pointers also offer is_null() for checking. - If an error occurs you own the C string containing the error message. You can either free it or reuse it for future calls - which option is more convenient?
- When
open
succeeds, it gives you a valid, non-null pointer back. A natural mapping for this type isNonNull::new_unchecked
. - Owned C strings can be created with
std::ffi::CString
. Note that unlike C strings, Rust strings can contain null bytes. - To handle paths there's
std::path::Path
. For simplicity reasons, assume paths are valid UTF-8, which in the real world isn't always the case. - Errors can be converted with
map_err
andFrom::from
. - String types implement AsRef<Path>, which makes it a good fit for path parameters.
Testing
LevelDB, being a database, persists data to disk. When writing tests for your binding, creating this data in a temporary fashion is appropriate, saving you from doing cleanup work yourself. The tempdir crate provides this functionality, but if you added it to [dependencies]
it would also be installed for every user of your library, even if they didn't intend to run your tests. Fortunately, Cargo has a [dev-dependencies]
section for crates that are only required during development:
[dev-dependencies]
tempdir = "*"
Basic template
You can use this code skeleton to get started:
#![allow(unused)] fn main() { use leveldb_sys::*; use std::ptr; use std::ffi::CString; #[test] fn basic_template() { let options = unsafe { leveldb_options_create() }; unsafe { leveldb_options_set_create_if_missing(options, true as u8) }; let mut err = ptr::null_mut(); let name = CString::new("my_db").unwrap(); let (db_ptr, err_ptr) = unsafe { let db_ptr = leveldb_open( options, name.as_ptr(), &mut err, ); (db_ptr, err) }; unsafe { leveldb_options_destroy(options) }; if err_ptr == ptr::null_mut() { unsafe { leveldb_close(db_ptr) } } else { unsafe { println!("Error opening database: {}", *err_ptr); } } } }
Solution for exercise 1: open & close
#![allow(unused)] fn main() { use libc::{c_void, size_t}; use std::ffi::CString; use std::path::Path; use std::ptr; use std::ptr::NonNull; use leveldb_sys::{ leveldb_close, leveldb_create_iterator, leveldb_free, leveldb_get, leveldb_iter_destroy, leveldb_iter_next, leveldb_iter_seek_to_first, leveldb_iter_valid, leveldb_iter_value, leveldb_iterator_t, leveldb_open, leveldb_options_create, leveldb_options_destroy, leveldb_options_set_create_if_missing, leveldb_options_t, leveldb_put, leveldb_readoptions_create, leveldb_readoptions_destroy, leveldb_readoptions_t, leveldb_t, leveldb_writeoptions_create, leveldb_writeoptions_destroy, leveldb_writeoptions_t, }; struct DBHandle { ptr: NonNull<leveldb_t>, } impl Drop for DBHandle { fn drop(&mut self) { unsafe { leveldb_close(self.ptr.as_ptr()) } } } pub struct Options { ptr: NonNull<leveldb_options_t>, } impl Drop for Options { fn drop(&mut self) { unsafe { leveldb_options_destroy(self.ptr.as_ptr()) } } } impl Options { pub fn new() -> Options { unsafe { let ptr = leveldb_options_create(); Options { ptr: NonNull::new_unchecked(ptr), } } } pub fn create_if_missing(&mut self, value: bool) { unsafe { leveldb_options_set_create_if_missing(self.as_ptr(), value as u8) } } fn as_ptr(&self) -> *mut leveldb_options_t { self.ptr.as_ptr() } } pub struct Database { handle: DBHandle, } unsafe fn into_rust_string(ptr: *const i8) -> String { let error_s = CStr::from_ptr(ptr).to_string_lossy().to_string(); leveldb_free(ptr as *mut c_void); error_s } #[derive(Debug, Eq, PartialEq)] pub enum Error { OpenFail(String), InvalidString, } impl Database { pub fn open<P: AsRef<Path>>(path: P, options: Options) -> Result<Database, Error> { let mut error = ptr::null_mut(); let c_string = CString::new(path.as_ref().to_str().ok_or(Error::InvalidString)?) .map_err(|_| Error::InvalidString)?; unsafe { let db = leveldb_open(options.as_ptr(), c_string.as_ptr(), &mut error); if error == ptr::null_mut() { Ok(Database { handle: DBHandle { ptr: NonNull::new_unchecked(db), }, }) } else { Err(Error::OpenFail(into_rust_string(error))) } } } } #[cfg(test)] mod test { use super::*; use tempdir::TempDir; #[test] fn test_open() { let tmp = TempDir::new("test_open").unwrap(); let mut options = Options::new(); options.create_if_missing(true); let database = Database::open(tmp.path().join("database"), options); assert!(database.is_ok()); } } }
Exercise: Reading and writing database contents
Now that you have an open database, it's time to interact with it by storing and retrieving data.
Preparation
You'll need a few more items from the sys crate:
leveldb_readoptions_t
: opaque type to specify read operation optionsleveldb_writeoptions_t
: opaque type to specify write operation optionsleveldb_readoptions_create
: creates a defaultreadoptions_t
leveldb_readoptions_destroy
: deallocatesreadoptions_t
leveldb_writeoptions_create
: creates a defaultwriteoptions_t
leveldb_writeoptions_destroy
: deallocateswriteoptions_t
leveldb_put
: writes a binary value for a given binary keyleveldb_get
: reads a binary value for a given binary key. Returns anull
pointer for "not found", an owned object otherwise.leveldb_free
: deallocates a value object returned byleveldb_get
Your tasks
β
Implement two functions on your Database
type:
pub fn put(&self, key: &[u8], data: &[u8]) -> Result<(), Error>
pub fn get(&self, key: &[u8]) -> Result<Option<Box<[u8]>>, Error>
Be mindful of the API's ownership contract.
β Test your implementation.
Help and hints
- You only need to create (and destroy!) the read/write options objects, not configure them further in any way.
- Stuck with the wrong primitive type? Try casting it.
b"a string"
is literal syntax for creating an&[u8]
slice.- Here's how to put a slice on the heap (AKA box it):
β note that by creating alet slice = std::slice::from_raw_parts(data as *mut u8, len); let result = Box::from(slice);
Box
you've copied the data and still own (and therefore have to free) the data behind the raw pointer. - don't free
null
pointers.
Solution for exercise 2: get & put
#![allow(unused)] fn main() { // ... previous code ... pub struct WriteOptions { ptr: NonNull<leveldb_writeoptions_t>, } impl WriteOptions { pub fn new() -> WriteOptions { unsafe { let ptr = leveldb_writeoptions_create(); WriteOptions { ptr: NonNull::new_unchecked(ptr), } } } } impl Drop for WriteOptions { fn drop(&mut self) { unsafe { leveldb_writeoptions_destroy(self.ptr.as_ptr()) } } } pub struct ReadOptions { ptr: NonNull<leveldb_readoptions_t>, } impl ReadOptions { pub fn new() -> ReadOptions { unsafe { let ptr = leveldb_readoptions_create(); ReadOptions { ptr: NonNull::new_unchecked(ptr), } } } } impl Drop for ReadOptions { fn drop(&mut self) { unsafe { leveldb_readoptions_destroy(self.ptr.as_ptr()) } } } #[derive(Debug, Eq, PartialEq)] pub enum Error { // ... previous code ... GetFail(String), PutFail(String), } impl Database { // ... previous code ... pub fn get(&self, key: &[u8]) -> Result<Option<Box<[u8]>>, Error> { unsafe { let read_options = ReadOptions::new(); let mut len: size_t = 0; let mut error = ptr::null_mut(); let data = leveldb_get( self.handle.ptr.as_ptr(), read_options.ptr.as_ptr(), key.as_ptr() as *const i8, key.len(), &mut len, &mut error, ); if error == ptr::null_mut() { if data == ptr::null_mut() { Ok(None) } else { let slice = std::slice::from_raw_parts(data as *mut u8, len); let result = Box::from(slice); leveldb_free(data as *mut c_void); Ok(Some(result)) } } else { Err(Error::GetFail(into_rust_string(error))) } } } pub fn put(&self, key: &[u8], data: &[u8]) -> Result<(), Error> { unsafe { let write_options = WriteOptions::new(); let mut error = ptr::null_mut(); leveldb_put( self.handle.ptr.as_ptr(), write_options.ptr.as_ptr(), key.as_ptr() as *const i8, key.len(), data.as_ptr() as *const i8, data.len(), &mut error, ); if error == ptr::null_mut() { Ok(()) } else { Err(Error::PutFail(into_rust_string(error))) } } } } #[cfg(test)] mod test { // ... previous code ... #[test] fn test_read_write() { let tmp = TempDir::new("test_read_write").unwrap(); let mut options = Options::new(); options.create_if_missing(true); let database = Database::open(tmp.path().join("database"), options).unwrap(); let key: &[u8] = b"test"; let missing_key: &[u8] = b"test_missing"; let value: &[u8] = b"test"; database.put(key, value).unwrap(); let result = database.get(key); assert_eq!(result, Ok(Some(Box::from(value)))); let result = database.get(missing_key); assert_eq!(result, Ok(None)); } } }
Exercise: Iterate over database contents
In this last part we'll create an Iterator
for looping over everything stored in our database.
Preparation
The iterator functionality is exposed by the sys crate as follows:
leveldb_create_iterator
: Creates an opaqueleveldb_iterator_t
handleleveldb_iter_seek_to_first
: Starts the iteration by seeking to the first itemleveldb_iter_next
: Advances iteration by one elementleveldb_iter_value
: Reads the value at the current iterator positionleveldb_iter_valid
: Indicates whether the iterator is currently valid
An iterator is position invalid before seeking to the first item and after it has advanced beyond the last one. Reading its value returns non-owned data.
Your tasks
β
Implement an iterator handle. It should fully encapsulate any unsafe
code.
β
Implement an Iterator
type that holds the necessary state and makes use of the handle.
β
Implement pub fn iter(&self) -> Iterator
for your Database
struct.
β
Implement std::iter::Iterator
for your Iterator
type. Its items should be of type Box<[u8]>
.
β Write a test case to verify that your iterator returns all items lexicographically sorted by key. Also test with an empty database.
β
Make the Iterator
type reference the Database that created it. What has changed and what are the benefits?
β
Bonus task: what could be used instead of the Database
reference that achieves the same goal but consumes no memory?
Solution for exercise 3: Iterators
#![allow(unused)] fn main() { // ... previous code ... struct IteratorHandle { ptr: NonNull<leveldb_iterator_t>, } impl IteratorHandle { fn new(database: &Database, read_options: ReadOptions) -> IteratorHandle { unsafe { let iterator_ptr = leveldb_create_iterator(database.handle.ptr.as_ptr(), read_options.ptr.as_ptr()); leveldb_iter_seek_to_first(iterator_ptr); IteratorHandle { ptr: NonNull::new_unchecked(iterator_ptr), } } } fn next(&self) { unsafe { leveldb_iter_next(self.ptr.as_ptr()) }; } fn valid(&self) -> bool { unsafe { leveldb_iter_valid(self.ptr.as_ptr()) != 0 } } fn value(&self) -> (*const i8, usize) { unsafe { let mut len = 0; let data = leveldb_iter_value(self.ptr.as_ptr(), &mut len); (data, len) } } } impl Drop for IteratorHandle { fn drop(&mut self) { unsafe { leveldb_iter_destroy(self.ptr.as_ptr()) } } } impl Database { // ... previous code ... pub fn iter(&self) -> Iterator<'_> { let read_options = ReadOptions::new(); let handle = IteratorHandle::new(self, read_options); Iterator { handle: handle, start: true, database: self, } } } pub struct Iterator<'iterator> { handle: IteratorHandle, start: bool, #[allow(unused)] database: &'iterator Database, } impl<'iterator> Iterator<'iterator> { fn read_current(&self) -> Option<Box<[u8]>> { unsafe { if !self.handle.valid() { return None; }; let data = self.handle.value(); let slice = std::slice::from_raw_parts(data.0 as *mut u8, data.1); Some(Box::from(slice)) } } } impl<'iterator> std::iter::Iterator for Iterator<'iterator> { type Item = Box<[u8]>; fn next(&mut self) -> Option<Self::Item> { if self.start { self.start = false; self.read_current() } else { self.handle.next(); self.read_current() } } } #[cfg(test)] mod test { // ... previous code ... #[test] fn test_iter() { let tmp = TempDir::new("test_iter").unwrap(); let mut options = Options::new(); options.create_if_missing(true); let database = Database::open(tmp.path().join("database"), options).unwrap(); let key1: &[u8] = b"test1"; let key2: &[u8] = b"test2"; let key3: &[u8] = b"test3"; let value1: &[u8] = b"value1"; let value2: &[u8] = b"value2"; let value3: &[u8] = b"value3"; database.put(key1, value1).unwrap(); database.put(key2, value2).unwrap(); database.put(key3, value3).unwrap(); let mut iter = database.iter(); assert_eq!(iter.next(), Some(Box::from(value1))); assert_eq!(iter.next(), Some(Box::from(value2))); assert_eq!(iter.next(), Some(Box::from(value3))); assert_eq!(iter.next(), None); } } }
Bonus task solution
Lifetime relationships can be expressed without occupying space in application memory by using PhantomData<T>
:
#![allow(unused)] fn main() { use std::marker::PhantomData; pub struct Iterator<'iterator> { handle: IteratorHandle, start: bool, phantom: PhantomData<&'iterator Database>, } // usage: Iterator { handle: handle, start: true, phantom: PhantomData, } }
Full solution
#![allow(unused)] fn main() { use libc::{c_void, size_t}; use std::ffi::{CStr, CString}; use std::path::Path; use std::ptr; use std::ptr::NonNull; use leveldb_sys::{ leveldb_close, leveldb_create_iterator, leveldb_free, leveldb_get, leveldb_iter_destroy, leveldb_iter_next, leveldb_iter_seek_to_first, leveldb_iter_valid, leveldb_iter_value, leveldb_iterator_t, leveldb_open, leveldb_options_create, leveldb_options_destroy, leveldb_options_set_create_if_missing, leveldb_options_t, leveldb_put, leveldb_readoptions_create, leveldb_readoptions_destroy, leveldb_readoptions_t, leveldb_t, leveldb_writeoptions_create, leveldb_writeoptions_destroy, leveldb_writeoptions_t, }; struct DBHandle { ptr: NonNull<leveldb_t>, } impl Drop for DBHandle { fn drop(&mut self) { unsafe { leveldb_close(self.ptr.as_ptr()) } } } pub struct Options { ptr: NonNull<leveldb_options_t>, } impl Drop for Options { fn drop(&mut self) { unsafe { leveldb_options_destroy(self.ptr.as_ptr()) } } } impl Options { pub fn new() -> Options { unsafe { let ptr = leveldb_options_create(); Options { ptr: NonNull::new_unchecked(ptr), } } } pub fn create_if_missing(&mut self, value: bool) { unsafe { leveldb_options_set_create_if_missing(self.as_ptr(), value as u8) } } fn as_ptr(&self) -> *mut leveldb_options_t { self.ptr.as_ptr() } } pub struct Database { handle: DBHandle, } #[derive(Debug, Eq, PartialEq)] pub enum Error { OpenFail(String), // exercise 2 (get/put) GetFail(String), // exercise 2 (get/put) PutFail(String), InvalidString, } unsafe fn into_rust_string(ptr: *const i8) -> String { let error_s = CStr::from_ptr(ptr).to_string_lossy().to_string(); leveldb_free(ptr as *mut c_void); error_s } impl Database { pub fn open<P: AsRef<Path>>(path: P, options: Options) -> Result<Database, Error> { let mut error = ptr::null_mut(); let c_string = CString::new(path.as_ref().to_str().ok_or(Error::InvalidString)?) .map_err(|_| Error::InvalidString)?; unsafe { let db = leveldb_open(options.as_ptr(), c_string.as_ptr(), &mut error); if error == ptr::null_mut() { Ok(Database { handle: DBHandle { ptr: NonNull::new_unchecked(db), }, }) } else { Err(Error::OpenFail(into_rust_string(error))) } } } // exercise 2 (get/put) pub fn get(&self, key: &[u8]) -> Result<Option<Box<[u8]>>, Error> { unsafe { let read_options = ReadOptions::new(); let mut len: size_t = 0; let mut error = ptr::null_mut(); let data = leveldb_get( self.handle.ptr.as_ptr(), read_options.ptr.as_ptr(), key.as_ptr() as *const i8, key.len(), &mut len, &mut error, ); if error == ptr::null_mut() { if data == ptr::null_mut() { Ok(None) } else { let slice = std::slice::from_raw_parts(data as *mut u8, len); let result = Box::from(slice); leveldb_free(data as *mut c_void); Ok(Some(result)) } } else { Err(Error::GetFail(into_rust_string(error))) } } } // exercise 2 (get/put) pub fn put(&self, key: &[u8], data: &[u8]) -> Result<(), Error> { unsafe { let write_options = WriteOptions::new(); let mut error = ptr::null_mut(); leveldb_put( self.handle.ptr.as_ptr(), write_options.ptr.as_ptr(), key.as_ptr() as *const i8, key.len(), data.as_ptr() as *const i8, data.len(), &mut error, ); if error == ptr::null_mut() { Ok(()) } else { Err(Error::PutFail(into_rust_string(error))) } } } // exercise 3 (Iterator) pub fn iter(&self) -> Iterator<'_> { let read_options = ReadOptions::new(); let handle = IteratorHandle::new(self, read_options); Iterator { handle: handle, start: true, database: self, } } } // exercise 3 (Iterator) pub struct Iterator<'iterator> { handle: IteratorHandle, start: bool, #[allow(unused)] database: &'iterator Database, } // exercise 3 (Iterator) impl<'iterator> Iterator<'iterator> { fn read_current(&self) -> Option<Box<[u8]>> { unsafe { if !self.handle.valid() { return None; }; let data = self.handle.value(); let slice = std::slice::from_raw_parts(data.0 as *mut u8, data.1); Some(Box::from(slice)) } } } // exercise 2 (get/put) pub struct WriteOptions { ptr: NonNull<leveldb_writeoptions_t>, } // exercise 2 (get/put) impl WriteOptions { pub fn new() -> WriteOptions { unsafe { let ptr = leveldb_writeoptions_create(); WriteOptions { ptr: NonNull::new_unchecked(ptr), } } } } // exercise 2 (get/put) impl Drop for WriteOptions { fn drop(&mut self) { unsafe { leveldb_writeoptions_destroy(self.ptr.as_ptr()) } } } // exercise 2 (get/put) pub struct ReadOptions { ptr: NonNull<leveldb_readoptions_t>, } // exercise 2 (get/put) impl ReadOptions { pub fn new() -> ReadOptions { unsafe { let ptr = leveldb_readoptions_create(); ReadOptions { ptr: NonNull::new_unchecked(ptr), } } } } // exercise 2 (get/put) impl Drop for ReadOptions { fn drop(&mut self) { unsafe { leveldb_readoptions_destroy(self.ptr.as_ptr()) } } } // exercise 3 (Iterator) struct IteratorHandle { ptr: NonNull<leveldb_iterator_t>, } // exercise 3 (Iterator) impl IteratorHandle { fn new(database: &Database, read_options: ReadOptions) -> IteratorHandle { unsafe { let iterator_ptr = leveldb_create_iterator(database.handle.ptr.as_ptr(), read_options.ptr.as_ptr()); leveldb_iter_seek_to_first(iterator_ptr); IteratorHandle { ptr: NonNull::new_unchecked(iterator_ptr), } } } fn next(&self) { unsafe { leveldb_iter_next(self.ptr.as_ptr()) }; } fn valid(&self) -> bool { unsafe { leveldb_iter_valid(self.ptr.as_ptr()) != 0 } } fn value(&self) -> (*const i8, usize) { unsafe { let mut len = 0; let data = leveldb_iter_value(self.ptr.as_ptr(), &mut len); (data, len) } } } // exercise 3 (Iterator) impl Drop for IteratorHandle { fn drop(&mut self) { unsafe { leveldb_iter_destroy(self.ptr.as_ptr()) } } } // exercise 3 (Iterator) impl<'iterator> std::iter::Iterator for Iterator<'iterator> { type Item = Box<[u8]>; fn next(&mut self) -> Option<Self::Item> { if self.start { self.start = false; self.read_current() } else { self.handle.next(); self.read_current() } } } #[cfg(test)] mod test { use core::panic; use super::*; use tempdir::TempDir; #[test] fn test_open() { let tmp = TempDir::new("test_open").unwrap(); let mut options = Options::new(); options.create_if_missing(true); let database = Database::open(tmp.path().join("database"), options); assert!(database.is_ok()); } #[test] fn test_create_open_fails() { let mut options = Options::new(); options.create_if_missing(true); let database = Database::open("/invalid/location", options); match database { Err(Error::OpenFail(_)) => {} _ => panic!(), } } #[test] fn test_open_nonexistent_fails() { let options = Options::new(); let database = Database::open("/invalid/location", options); match database { Err(Error::OpenFail(_)) => {} _ => panic!(), } } // exercise 2 (get/put) #[test] fn test_read_write() { let tmp = TempDir::new("test_read_write").unwrap(); let mut options = Options::new(); options.create_if_missing(true); let database = Database::open(tmp.path().join("database"), options).unwrap(); let key: &[u8] = b"test"; let missing_key: &[u8] = b"test_missing"; let value: &[u8] = b"test"; database.put(key, value).unwrap(); let result = database.get(key); assert_eq!(result, Ok(Some(Box::from(value)))); let result = database.get(missing_key); assert_eq!(result, Ok(None)); } // exercise 3 (Iterator) #[test] fn test_iter() { let tmp = TempDir::new("test_iter").unwrap(); let mut options = Options::new(); options.create_if_missing(true); let database = Database::open(tmp.path().join("database"), options).unwrap(); let key1: &[u8] = b"test1"; let key2: &[u8] = b"test2"; let key3: &[u8] = b"test3"; let value1: &[u8] = b"value1"; let value2: &[u8] = b"value2"; let value3: &[u8] = b"value3"; database.put(key1, value1).unwrap(); database.put(key2, value2).unwrap(); database.put(key3, value3).unwrap(); let mut iter = database.iter(); assert_eq!(iter.next(), Some(Box::from(value1))); assert_eq!(iter.next(), Some(Box::from(value2))); assert_eq!(iter.next(), Some(Box::from(value3))); assert_eq!(iter.next(), None); } } }