GithubHelp home page GithubHelp logo

erhankur / api Goto Github PK

View Code? Open in Web Editor NEW

This project forked from stratifylabs/api

0.0 0.0 0.0 361 KB

API is a C++ Framework for building desktop and embedded applications

License: MIT License

CMake 3.85% C++ 89.30% C 6.85%

api's Introduction

API

Build

Overview

API is a collection of cross-platform C++ libraries for

  • Windows
  • MacOS
  • Linux
  • Stratify OS (embedded)

including

Design

The API framework is a hard to mis-use, non-onerous C++ API acheived using the following principles

  • Thread Local Error Context
  • Method Chaining
  • Filesystem Inspired Abstraction
  • Strong Arguments
  • RAII for Resource Management

Thread Local Error Context

Error Context

  • Cascading errors by returning int can be a huge pain
  • Use a single error context per thread
    • Tracks return value of read/write and other system calls
    • Monitors errno after system calls
    • Execute backtrace on error for debugging
    • Do not nest errors. If thread is_error(), operation is aborted.

This means anything that can affect the error context (including changing the value of errno) must abort if the thread already has an error.

const FileObject &FileObject::read(void *buf, int nbyte) const {
  API_RETURN_VALUE_IF_ERROR(*this); //don't modify erroneous context
  API_SYSTEM_CALL("", interface_read(buf, nbyte)); //update error context if needed
  return *this; //used for method chaining
}

Thread Locality

The process (primary thread) error context is created statically at compile time. The value of &errno provides an error context signature for each thread. An error context is dynamically allocated if a new thread has an error.

Error &PrivateExecutionContext::get_error() {
  if (&(errno) == m_error.m_signature) {
    return m_error;
  }
  if (m_error_list == nullptr) {
    m_error_list = new std::vector<Error>();
    API_ASSERT(m_error_list != nullptr);
  }
  for (Error &error : *m_error_list) {
    if (error.m_signature == &(errno)) {
      return error;
    }
    if (error.m_signature == nullptr) {
      error.m_signature = &(errno);
      return error;
    }
  }
  m_error_list->push_back(Error(&(errno)));
  return m_error_list->back();
}

Method Chaining

Having a per thread error context, allows for very powerful (and concise) code using method chaining. This approach also helps to create strong arguments that are hard for application developers to use incorrectly.

class Point {
public:
  int x() const { return m_x; }
  int y() const { return m_y; }

  Point& set_x(int value){
    m_x = value;
    return *this;
  }

  Point& set_y(int value){
    m_y = value;
    return *this;
  }

private:
  int m_x = 0;
  int m_y = 0;
}

Point p = Point().set_x(50).set_y(100);

Filesystem Inspired Abstraction

Almost any data in the API framework can be treated a FileObject. This provides a unified way to move data around between memory, the filesystem, the internet, and devices.

//defines interface for using file like objects
class FileObject {
  //write source to this file
  FileObject & write(const FileObject & source);
}

//interacts with a real file (POSIX style)
class File : public FileObject;

//uses malloc/free to manage contents
class DataFile : public FileObject;

//looks at existing memory for file contents
class ViewFile : public FileObject;

//use callbacks for file contents
class LambdaFile : public FileObject;

// these are part of the InetAPI (separate repo)
class Socket : public FileObject;
class SecureSocket : public FileObject;

Strong Arguments

API classes are designed so that the functionality is obvious without looking at the declaration. The code below illustrates one example.

//weak -- don't do this
FileSystem& rename(const StringView old_path, const StringView new_path);

//which is old and which is new?
FileSystem().rename("my_file.txt", "your_file.txt");

//strong
class Rename {
  //API_ACCESS is a getter/setter macro supporting
  //method chaining
  API_ACCESS(Rename,StringView,old_path);
  API_ACCESS(Rename,StringView,new_path);
}
FileSystem& rename(const Rename & options);

//unambiguous functionality
FileSystem().rename(
  Rename()
    .set_old_path("my_file.txt")
    .set_new_path("your_file.txt"));

// strong argument calls also include a call operator shortcut
FileSystem& operator()(const Rename & options){
  return rename(options);
}

//called like this
FileSystem()(Rename()
  .set_old_path("my_file.txt")
  .set_new_path("your_file.txt"));

Use RAII for resource management

RAII = resource acquisition is initialization

The constructor/deconstructor paradigm built into the C++ language is an excellent way to manage resources. If anything is "opened", it is done so in the constructor and then closed in the desctructor. This goes for lock/unlock, malloc/free, initialize/finalize and so on.

Here are a few examples:

//open/close
DataFile my_file;
{
  //file is opened on construction
  //OR that is, it is "initialized" on "acquisition"
  File f("myfile.txt"); 
  //read from f, write to my_file
  my_file.write(f); 
} //f is closed when leaving scope

printf("my file is %s\n", my_file.data().add_null_terminator());

//lock/unlock
Mutex mutex;
{
  MutexGuard mutex_guard(mutex); //lock mutex
  File f("file.txt");
  printf("file size is %d\n", f.size());
} // f is closed then mutex is unlocked

One limitation to this approach comes when, for example, you want to construct a File but open it later. The API allows for constructing an unopened file and swapping it for something valid later.

Keep in mind, you can move a File but you cannot copy the object. This will be true throughout the API for any class that is associated with a system resource.

//this will just be an unopened file for now
File f;

if( f.is_valid() == false ){
 //yep, it's not valid (also not erroneous)
}

//This is how we open it later
f = File("myfile.txt").move();

//this is also OK
f = std::move(File("myfile.txt"));

//But we can never make copies
//this won't compile because the
//copy constructor is deleted
File f_copy = f;

File Copy Example

Using traditional C/POSIX style programming to copy a file looks something like this:

int file_fd = open("file.txt", O_READONLY);
if( f < 0 ){ /*cascade the error up the chain*/ }

int new_file_fd = open("new_file.txt", O_APPEND | O_CREAT | O_TRUNC, 0666);
if( new_file_fd < 0 ){ /*cascade the error up the chain*/ }

char buffer[64];
int bytes_read = 0;

while( (bytes_read = read(file_fd, buffer, 64)) > 0 ){
  if( write(new_file_fd, buffer, bytes_read) < 0 ){
    //cascade the error up the chain
  }
}

if( close(file_fd) < 0){ /*cascade the error up the chain*/ }
if( close(new_file_fd) < 0){ /*cascade the error up the chain*/ }

The API way is much more concise.

//copy file.txt -> new_file.txt
File(File::IsOverwrite::yes, "new_file.txt").write(File("file.txt"));
if( api::ExecutionContext::is_error() ){
  //something didn't work -- error context has the details
}

The error context of the thread will record the precise location of the error and provide a backtrace.

Conventions

getters and setters

The API framework uses set_value() for setting and value() for access to a value. If the method needs to do something extra to return the value, get_value() is used.

The chrono::ClockTime has some good examples:

//access seconds -- nothing to calculate or fetch
s32 seconds() const { return m_value.tv_sec; }

//setting the seconds
ClockTime &set_seconds(u32 seconds) {
  m_value.tv_sec = seconds;
  return *this;
}

// the age isn't readily available, so we do get_age() rather than age()
ClockTime get_age() const { return get_system_time() - *this; }

is_valid()

If a class can have a valid/invalid state, the method is_valid() is used to check.

Examples:

#include <thread.hpp>
#include <fs.hpp>

File f;
if( f.is_valid() ){}

Thread t;
if( t.is_valid() ){}

Documentation

The code and a few guides (README.md docs in the repo) are all the documentation that is available. For many years, I believed in doxygen style comments, but most of the documentation just ended up being redundant. In fact, this snippet was taken directly from the code before the comments were removed.

/*! \details Compares <= to another MicroTime object. */
bool operator<=(const MicroTime &a) const {
  return microseconds() <= a.microseconds();
}

Comments in the code exists to explain why and give insights that are not obvious. They are not there to explain what. The API and code should do that on its own.

api's People

Contributors

tyler-gilbert avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.