virtmem
virtual memory library for Arduino
virtmem Documentation

Introduction

virtmem is an Arduino library that allows your project to easily use an external memory source to extend the (limited) amount of available RAM. This library supports several memory resources, for instance, SPI ram (e.g. the 23LC1024 chip from Microchip), an SD card or even a computer connected via a serial connection. The library is made in such a way that managing and using this virtual memory closely resembles working with data from 'normal' memory.

Features

  • Extend the available memory with kilobytes, megabytes or even gigabytes
  • Supports SPI RAM (23LC series from Microchip), SD cards and RAM from a computer connected through serial
  • Easy C++ interface that closely resembles regular data access
  • Memory page system to speed up access to virtual memory
  • New memory interfaces can be added easily
  • Code is mostly platform independent and can fairly easy be ported to other plaforms (x86 port exists for debugging)

Demonstration

Before delving into specifics, here is a simple example to demonstrate how virtmem works and what it can do.

#include <Arduino.h>
#include <SdFat.h>
#include <virtmem.h>
#include <alloc/sd_alloc.h>
// Simplify virtmem usage
using namespace virtmem;
// Create virtual a memory allocator that uses SD card (with FAT filesystem) as virtual memory pool
// The default memory pool size (1 MB) is used.
SDVAlloc valloc;
SdFat sd;
struct MyStruct { int x, y; };
void setup()
{
// Initialize SdFatlib
if (!sd.begin(9, SPI_FULL_SPEED))
sd.initErrorHalt();
valloc.start(); // Always call this to initialize the allocator before using it
// Allocate a char buffer of 10000 bytes in virtual memory and store the address to a virtual pointer
VPtr<char, SDVAlloc> str = valloc.alloc<char>(10000);
// Set the first 1000 bytes to 'A'
memset(str, 'A', 1000);
// array access
str[1] = 'B';
// Own types (structs/classes) also work.
VPtr<MyStruct, SDVAlloc> ms = valloc.alloc<MyStruct>(); // alloc call without parameters: use automatic size deduction
ms->x = 5;
ms->y = 15;
}
void loop()
{
// ...
}

This Arduino sketch demonstrates how to use a SD card as virtual memory store. By using a virtual memory pointer wrapper class, using virtual memory becomes quite close to using data residing in 'normal' memory.

More examples can be found on the examples page.

Basics

Virtual memory

As the name suggests, virtmem works similar to virtual memory management found on computers.

The library uses a paging system to work efficiently with virtual memory. A memory page is a static buffer that resides in regular RAM and contains a copy of a part of the total virtual memory pool. Multiple pages are typically used (usually four), where each page contains a copy of a different virtual memory region. Similar to data allocated from the heap, data access from virtual memory happens through a virtual pointer.

intro-scheme.png
virtual memory scheme

Whenever a virtual pointer is dereferenced, the library first checks whether the requested data resides in one of the memory pages, and if not, the library will copy the data from virtual memory to a suitable memory page. All data access now happens through this memory page.

When virtual memory has to be copied to a memory page and no pages are free, the library will first have to free a page. During this process any modified data will be written back to the virtual memory pool and the requested data will be loaded to the page. This process is sometimes called swapping. Swapping a page is generally a relative time consuming process, since a large amount of data has to be transferred for example over SPI. To minimize swapping, the library always tries to free pages that were not recently loaded in (FIFO like). The time spent on swapping is further reduced by having multiple memory pages and only writing out data that was modified.

Because memory pages reside in regular RAM, (repeated) data access to paged memory is quite fast.

Installation and File structure

The virtmem/ subdirectory contains all the code needed to compile Arduino sketches, and should therefore be copied as any other library. The file layout follows the new Arduino library specification.

Other files include documentation and code for testing the library, and are not needed for compilation. A summarized overview is given below.

Directory Contents
<root> Internal development and some docs
benchmark/ Internal code for benchmarking
doc/ Files used for Doxygen created documentation
doc/html/ This manual
gtest/ and test/ Code for internal testing
virtmem/ Library code. This directory needs to be copied to your libraries folder.
virtmem/extras/ Contains python scripts needed for the serial memory allocator.

Using virtual memory (tutorial)

Virtual memory in virtmem is managed by a virtual memory allocator. These are C++ template classes which are responsible for allocating and releasing virtual memory and contain the datablocks utilized for memory pages. Most of this functionality is defined in the virtmem::BaseVAlloc and virtmem::VAlloc classes. In addition to this, several allocator classes are derived from these base classes that actually implement the code necessary to deal with virtual memory (e.g. reading and writing data). For example, the class virtmem::SDVAlloc implements an allocator that uses an SD card as a virtual memory pool.

The virtmem library supports the following allocators:

Allocator Description Header
virtmem::SDVAllocP Uses a FAT formatted SD card as memory pool. Requires SD fat library. #include <alloc/sd_alloc.h>
virtmem::SPIRAMVAllocP Uses SPI ram (Microchip's 23LC series) as memory pool. Requires serialram library. #include <alloc/spiram_alloc.h>
virtmem::MultiSPIRAMVAllocP Like virtmem::SPIRAMVAlloc, but supports multiple memory chips. #include <alloc/spiram_alloc.h>
virtmem::SerialVAllocP Uses RAM from a computer connected through serial as memory pool. The computer should run the virtmem/extras/serial_host.py Python script. #include <alloc/serial_alloc.h>
virtmem::StaticVAllocP Uses regular RAM as memory pool (for debugging). #include <alloc/static_alloc.h>
virtmem::StdioVAllocP Uses files through regular stdio functions as memory pool (for debugging purposes on PCs). #include <alloc/stdio_alloc.h>

The following code demonstrates how to setup a virtual memory allocator:

#include <Arduino.h>
#include <virtmem.h>
#include <SdFat.h>
#include <alloc/sd_alloc.h>
// Create a virtual memory allocator that uses SD card (with FAT filesystem) as virtual memory pool
// The default memory pool size (defined by VIRTMEM_DEFAULT_POOLSIZE in config.h) is used.
// ...
void setup()
{
// Initialize SdFatlib
valloc.start(); // Always call this to initialize the allocator before using it
// ...
}

To use the virtmem library you should include the virtmem.h header file. Furthermore, the specific header file of the allocator has to be included (alloc/sd_alloc.h, see table above). Finally, since some allocators depend on other libraries, they also may need to be included (SdFat.h in this example).

All classes, functions etc. of the virtmem library resides in the virtmem namespace. If you are unfamiliar with namespaces, you can find some info here. This is purely for 'organizational purposes', however, for small programs it may be easier to simply pull the virtmem namespace in the global namespace:

// as above ...
using namespace virtmem; // pull in global namespace to shorten code
SDVAlloc valloc;
// as below ...

All allocator classes are singleton, meaning that only one (global) instance can be defined (however, instances of different allocators can co-exist, see Multiple allocators). Before the allocator can be used it should be initialized by calling its start function. Please note that, since this example uses the SD fat lib allocator, SD fat lib has to be initialized prior to the allocator (see virtmem::SDVAlloc).

Two interfaces exist to actually use virtual memory.

The first approach is to interface with raw memory directly through functions defined in virtmem::BaseVAlloc (e.g. read() and write()). Although dealing with raw memory might be slightly more efficient performance wise, this approach is not recommended as it is fairly cumbersome to do so.

The second approach is to use virtual pointer wrappers. These template classes were designed to make virtual memory access as close as 'regular' memory access as possible. Here is an example:

// define virtual pointer linked to SD fat memory
vptr = valloc.alloc<int>(); // allocate memory to store integer (size automatically deduced from type)
*vptr = 4;

In this example we defined a virtual pointer to an int, which is linked to the SD allocator. An alternative syntax to define a virtual pointer is through virtmem::VAlloc::TVPtr:

virtmem::SDVAlloc::TVPtr<int>::type vptr;

If your platform supports C++11, this can shortened further:

virtmem::SDVAlloc::VPtr<int> vptr; // Same as above but shorter, C++11 only!

Staying on C++11 support, using auto further reduces the syntax quite a bit:

auto vptr = valloc.alloc<int>(); // Automatically deduce vptr type from alloc call, C++11 only!

Memory allocation is done through the alloc() function. In the above example no arguments were passed to alloc(), which means that alloc will automatically deduce the size required for the pointer type (int). If you want to allocate a different size (for instance to use the data as an array) then the number of bytes should be specified as the first argument to alloc:

vptr = valloc.alloc<int>(1000 * sizeof(int)); // allocate memory to store array of 1000 integers
vptr[500] = 1337;

Besides all standard types (char, int, short, etc.), virtual pointers can also work with custom types (structs/classes):

struct MyStruct { int x, y; };
// ...
ms->x = 5;
ms->y = 15;

Note that there are a few imitations when using structs (or classes) with virtmem. For details: see virtmem::VPtr.

Finally, to free memory the free() function can be used:

valloc.free(vptr); // memory size is automatically deduced
See also

Advanced

Locking virtual data

A portion of the virtual memory can be locked to a memory page. Whenever such a lock is made, the data is guaranteed to stay in a memory page and will not be swapped out. The same data can be locked multiple times, and the data may only be swapped when all locks are released.

One reason to lock data is to improve performance. As soon as data is locked it can safely be accessed through a regular pointer, meaning that no additional overhead exists to use the data.

Another reason to lock data is to work with code that only accepts data residing in regular memory space. For instance, if a function needs to be called that requires a pointer to the data, the pointer to the locked memory page region can then be passed to such a function.

Note on memory pages

Each virtual data lock will essentially block a memory page. Since the number of memory pages is rather small, care should be taken to not to create too many different data locks.

To decrease the likelyhood of running out of free memory pages, virtmem supports two additional sets of smaller memory pages which are specifically used for data locking. They are much smaller in size, so they will not use a large deal of RAM, while still providing extra capacity for smaller data locks. Having a set of smaller memory pages is especially useful for locks created when accessing data in structures/classes.

The default size and amount of memory pages is dependent upon the MCU platform and can be customized as described in Configuring allocators.

Using virtual data locks

To create a lock to virtual memory the virtmem::VPtrLock class is used:

typedef virtmem::VPtr<char, virtmem::SDVAlloc> virtCharPtr; // shortcut
virtCharPtr vptr = valloc.alloc<char>(100); // allocate some virtual memory
memset(*lock, 10, 100); // set all bytes to '10'

The virtmem::makeVirtPtrLock function is used to create a lock. The last parameter to this function (optional, by default false) tells whether the locked data should be treated as read-only: if set to false the data will be written back to the virtual memory (even if unchanged) after the lock has been released. If you know the data will not be changed (or you don't care about changes), then it is more efficient to pass true instead.

Accessing locked data is simply done by dereferencing the lock variable (i.e. *lock).

Sometimes it is not possible to completely lock the memory region that was requested. For instance, there may not be sufficient space available to lock the complete data to a memory page or there will be overlap with another locked memory region. For this reason, it is important to always check the actual size that was locked. After a lock has been made, the effective size of the locked region can be requested by the virtmem::VPtrLock::getLockSize() function. Because it is rather unpredictable whether the requested data can be locked in one go, it is best create a loop that iteratively creates locks until all bytes have been dealt with:

typedef virtmem::VPtr<char, virtmem::SDVAlloc> virtCharPtr; // shortcut
const int size = 10000;
int sizeleft = size;
virtCharPtr vptr = valloc.alloc(size); // allocate a large block of virtual memory
virtCharPtr p = vptr;
while (sizeleft)
{
// create a lock to (a part) of the buffer
const int lockedsize = lock.getLockSize(); // get the actual size of the memory block that was locked
memset(*l, 10, lockedsize); // set bytes of locked (sub region) to 10
p += lockedsize; // increase pointer to next block to lock
sizeleft -= lockedsize; // decrease number of bytes to still lock
}

Note that a memset overload is provided by virtmem which works with virtual pointers.

After you are finished working with a virtual memory lock it has to be released. This can be done manually with the virtmem::VPtrLock::unlock function. However, the virtmem::VPtrLock destructor will call this function automatically (the class follows the RAII principle). This explains why calling unlock() in the above example was not necessary, as the destructor will call it automatically when the lock variable goes out of scope at the end of every iteration.

Accessing data in virtual memory

Note
This section is mostly theoretical. If you are skimming this manual (or lazy), you can skip this section.

Whenever virtual data is accessed, virtmem first has to make sure that this data resides in regular RAM (i.e. a memory page). In addition, if the data is changed, virtmem has to flag this data as 'dirty' so that it will be synchronized during the next page swap. To achieve these tasks, virtmem returns a proxy class whenever a virtual pointer is dereferenced, instead of returning the actual data. This proxy class (virtmem::VPtr::ValueWrapper) acts as it is the actual data, and is mostly invisible to the user:

int x = *myIntVptr;
*myIntVptr = 55;

The first line of the above example demonstrates a read operation: in this case the proxy class (returned by *myIntVPtr) will be converted to an int (automatic type cast), during which it will return a copy the data from virtual memory. The second line is a write operation: here the proxy class signals virtmem that data is changed and needs to be synchronized during the next page swap.

For accessing data members of structures (everything discussed here also applies to classes) in virtual memory the situation is more complicated. When a member is accessed through the -> operator of of the virtual pointer, we must return an actual pointer to the structure. While it is possible to make this data available through a memory page (as is done normally) in the -> overload function, synchronizing the data back is more tricky. For this reason, another proxy class is used (virtmem::VPtr::MemberWrapper) which is returned when the -> operator is called. This proxy class has also its -> operator overloaded, and this overload returns the actual data. The lifetime of this proxy class is important and matches that of the lifetime the data needs to be available in a memory page (for more details, see Stroustrup's general wrapper paper). Following from this, the proxy class will create a data lock to the data of the structure during construction and release this lock during its destruction.

Wrapping regular pointers

Sometimes it may be handy to assign a regular pointer to a virtual pointer:

typedef virtmem::VPtr<int, virtmem::SDVAlloc>::type virtIntPtr; // shortcut
void f(virtIntPtr p, int x)
{
*p += x;
}
// ...
*vptr = 55; // vptr is a virtual pointer to int
*ptr = 66; // ptr is a regular pointer to int (i.e. int*)
f(vptr, 10);
f(ptr, 12); // ERROR! Function only accepts virtual pointers

In the above example we have a (nonsensical) function f, which only accepts virtual pointers. Hence, the final line in this example will fail. Of course we could overload this function and provide an implementation that supports regular pointers. Alternatively, you can also 'wrap' the regular pointer inside a virtual pointer:

// ...
*ptr = 66; // ptr is a regular pointer to int (i.e. int*)
virtIntPtr myvptr = myvptr.wrap(ptr); // 'wrap' ptr inside a virtual pointer
f(myvptr, 12); // Success!

When a regular pointer is wrapped inside a virtual pointer, it can be used as it were a virtual pointer. If you want to obtain the original pointer then you can use the unwrap() function:

int *myptr = vptr.unwrap();

Please note that calling unwrap() on a non-wrapped virtual pointer yields an invalid pointer address. To avoid this, the isWrapped() function can be used.

Note
Wrapping regular pointers introduces a small overhead in usage of virtual pointers and is therefore disabled by default. This feature can be enabled in config.h.

Dealing with large data structures

Consider the following code:

using namespace virtmem;
struct MyStruct
{
int x, y;
char buffer[1024];
};
// ...
// allocate MyStruct in virtual memory
VPtr<MyStruct, SDVAlloc> sptr = valloc.alloc<MyStruct>();
sptr->buffer[512] = 'B'; // assign some random value

The size of MyStruct contains a large buffer and the total size exceeds 1 kilobyte. A good reason to put it in virtual memory! However, when assignment occurs in the example above, dereferencing the virtual pointer causes a copy of the whole data structure to a virtual page (more info here). This means that the memory page must be sufficient in size. One option is to configure the allocator and make sure memory pages are big enough. However, since this will increase RAM usage, this may not be an option.

For the example outlined above, another option may be to let MyStruct::buffer be a virtual pointer to a buffer in virtual memory:

using namespace virtmem;
struct MyStruct
{
int x, y;
};
// ...
// allocate MyStruct in virtual memory
VPtr<MyStruct, SDVAlloc> sptr = valloc.alloc<MyStruct>();
// ... and allocate buffer
sptr->buffer = valloc.alloc<char>(1024);
sptr->buffer[512] = 'B'; // assign some random value

Since MyStruct only stores a virtual pointer, its much smaller and can easily fit into a memory page.

Multiple allocators

While not more than one instance of a memory allocator type should be defined, it is possible to define different allocators in the same program:

// ...
// copy a kilobyte of data from SPI ram to a SD fat virtual memory pool
virtmem::memcpy(fatvptr, spiramvptr, 1024);

Configuring allocators

The number and size of memory pages can be configured in config.h. Alternatively, these settings can be passed as a template parameter to the allocator. For more info, see the description about virtmem::DefaultAllocProperties.

Virtual pointers to `struct`/`class` data members

It might be necessary to obtain a pointer to a member of a structure (or class) which resides in virtual memory. The way to obtain such a pointer with 'regular' memory is by using the address-of operator ('&'):

int *p = &mystruct->x;

You may be tempted to do the same when the structure is in virtual memory:

int *p = &vptr->x; // Spoiler: Do not do this!

However, this should never be done! The problem is that the above code will set p to an address inside one of the virtual memory pages. This should be considered as a temporary storage location and the contents can be changed anytime.

To obtain a 'safe' pointer, one should use the getMembrPtr() function:

struct myStruct { int x; };
intvptr = virtmem::getMembrPtr(mystruct, &myStruct::x);
See also
virtmem::getMembrPtr

Overloads of some common C library functions for virtual pointers

Overloads of some common C functions for dealing with memory and strings are provided by virtmem. They accept virtual pointers or a mix of virtual and regular pointers as function arguments. Please note that they are defined in the virtmem namespace like any other code from virtmem, hence, they will not "polute" the global namespace unless you want to (i.e. by using the using directive).

The following function overloads exist:

  • memcpy
  • memset
  • memcmp
  • strncpy
  • strcpy
  • strncmp
  • strcmp
  • strlen
See also
Overview of all overloaded functions.

Typeless virtual pointers (analog to void*)

When working with regular pointers, a void pointer can be used to store whatever pointer you like. With virtual pointers something similar can be achieved by using the base class for virtual pointers, namely virtmem::BaseVPtr:

typedef virtmem::VPtr<int, virtmem::SDVAlloc> virtIntPtr; // shortcut
int *intptr;
virtIntPtr intvptr;
// ...
// Store pointers in typeless pointer
void *voidptr = intptr;
virtmem::BaseVPtr basevptr = intvptr;
// ...
// Get it back
intptr = static_cast<int *>(voidptr);
intvptr = static_cast<virtIntPtr>(basevptr);

Generalized NULL pointer

When dealing with both regular and virtual pointers, it may be handy to use a single value that can set both types to zero. For this virtmem::NILL can be used:

using namespace virtmem;
// ...
char *a = NILL;

Note that on platforms that support C++11 you can simply use nullptr instead.

Pointer conversion

Similar to regular pointers, sometimes it may be necessary to convert from one pointer type to another. For this a simple typecast works as expected:

using namespace virtmem;
// ...
char buffer[128];
VPtr<char, SDVAlloc> vbuffer = valloc.alloc<char>(128);
int *ibuf = (int *)buffer; // or reinterpret_cast<int *>(buffer);
VPtr<int, SDVAlloc> vibuf = (VPtr<int, SDVAlloc>)vbuffer; // or static_cast<VPtr<int, SDVAlloc> >(vbuffer);

C++11 support

Some platforms such as the latest Arduino versions (>= 1.6.6) or Teensyduino support the fairly recent C++11 standard. In virtmem you can take this to your advantage to shorten the syntax, for instance by using template aliasing:

template <typename T> using MyVPtr = virtmem::VPtr<T, virtmem::SDVAlloc>;
MyVPtr<int> intVPtr;
MyVPtr<char> charVPtr;
// etc

In fact, this feature is already used by allocator classes:

The new auto keyword support means that we can further reduce the syntax quite a bit:

//...
auto intVPtr = valloc.alloc<int>(); // automatically deduce correct type from allocation call

Another, small feature with C++11 support, is that nullptr can be used to assign a zero address to a virtual pointer.

FAQ

Shouldn't I be worried about wear leveling when using the SD allocator?

Maybe, possibly not. A quick google reveales that most SD cards should have something called 'wear leveling', meaning that you probably don't have to worry about it. In any case, virtmem tries to reduce writes whenever possible. Furthermore, to get an indication of how much is written, have a look at the statistics functions.

How can I speed things up?

Several factors may influence the speed.

In general the fastest allocator is the SPI RAM allocator, followed by the multi SPI RAM allocator, SD allocator and serial allocator.

The speed of the first three all largely depend on SPI speeds. These are typically much higher on ARM based boards (e.g. Teensy 3.X) compared to AVR boards (e.g. Arduino Uno).

The speed of the serial allocator depends mainly on the baudrate. Increasing this number will signficantly improve read and write speeds. Note that even the AVR based boards can handle speeds much higher than the 115200 kbps that is sometimes marked as maximum speed. Also note that some boards, like Teensy 3.X and Arduino Due, have 'virtual serial ports', which are only limited by USB transfer speeds – not baudrates.

Finally, the convenience of virtual pointers will add some overhead compared to accessing data in regular memory. Beeing a software solution, more steps have to be performed for data access. Using virtual data locks can signifcantly reduce this overhead.

See also
Benchmarks

I'm getting compile errors about ambiguous types!?

When accessing virtual data, a proxy class is returned. This class behaves as much as the data as possbile, but sometimes the compiler needs some help. Simply casting it to the right type should be enough:

int a = static_cast<int>(*vptr);

I am using the serial allocator and my sketch seems to be stuck!?

The serial allocator needs to be connected with a computer which runs a script (virtmem/extras/serial_host.py). During initialization of the allocator it will wait for an connection indefinitely. For more information, please see the documentation about the serial alloactor.

What's the deal with SerialVAllocP vs SerialVAlloc, SDVAllocP vs SDVAlloc etc.?

All allocators are template classes. This was mainly done so that memory page settings can be configured, for instance:

However, often we don't need to configure anything, and the default is fine:

We don't actually have to specify virtmem::DefaultAllocProperties to stay with defaults:

virtmem::SDVAllocP<> valloc; // as above

To shorten it further, the 'non P' allocator shortcuts were made:

virtmem::SDVAlloc valloc; // as above, but no need for '<>'

Note that allocators such as virtmem::MultiSPIRAMVAllocP always require template parameters, hence, only a 'P version' exists.

Benchmarks

Some benchmarking results are shown below. Note that these numbers are generated with very simple, and possibly not so accurate tests, hence they should only be used as a rough indication (source code can be found in virtmem/examples/benchmark).

Allocator Teensy 3.2 (96 MHz) Teensy 3.2 (144 MHz) Arduino Uno
Read / Write (kB/s) Read / Write locks (kB/s) Read / Write (kB/s) Read / Write locks (kB/s) Read / Write (kB/s) Read / Write locks (kB/s)
Native (write only) 13333 20000 970
virtmem::StaticVAllocP 333 / 228 6000 / 7900 458 / 313 8905 / 11607 25 / 22 265 / 357
virtmem::SerialVAllocP 227 / 152 500 / 373 250 / 182 496 / 378 5 / 2 (14 / 9) 6 / 4 (30 / 20)
virtmem::SDVAllocP 266 / 70 1107 / 98 347 / 72 1102 / 91 23 / 15 156 / 44
virtmem::SPIRAMVAllocP 284 / 193 1887 / 1159 380 / 253 2083 / 1207 23 / 19 150 / 118

Some notes:

  • Tests were performed with- and without virtual data locks.
  • Native: Write speeds using a simple loop with a buffer. These results can be seen as a reference when regular (non virtual) data is used.
  • Data from static allocator is useful to measure overhead from virtual pointers.
  • Serial Arduino baudrates: 115200 bps and 1000000 bps (italic)
  • SD/SPI RAM: measured at maximum SPI speeds. For SPI RAM a 23LCV1024 chip was used.

License

Copyright (c) 2015-2016, Rick Helmus
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.