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:

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:

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 for u8, and Rust booleans are safe to cast as u8.
  • You need to pass an error pointer to open. The C library potentially mutates it, and initially it should point to null. For creating a suitable pointer Rust provides you with std::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 is NonNull::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 and From::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 options
  • leveldb_writeoptions_t: opaque type to specify write operation options
  • leveldb_readoptions_create: creates a default readoptions_t
  • leveldb_readoptions_destroy: deallocates readoptions_t
  • leveldb_writeoptions_create: creates a default writeoptions_t
  • leveldb_writeoptions_destroy: deallocates writeoptions_t
  • leveldb_put: writes a binary value for a given binary key
  • leveldb_get: reads a binary value for a given binary key. Returns a null pointer for "not found", an owned object otherwise.
  • leveldb_free: deallocates a value object returned by leveldb_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):
    let slice = std::slice::from_raw_parts(data as *mut u8, len);
    let result = Box::from(slice);
    
    ❗ note that by creating a 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 opaque leveldb_iterator_t handle
  • leveldb_iter_seek_to_first: Starts the iteration by seeking to the first item
  • leveldb_iter_next: Advances iteration by one element
  • leveldb_iter_value: Reads the value at the current iterator position
  • leveldb_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);
    }
}

}