Skip to content

kaomoneus/cppl

 
 

Repository files navigation

[toc]

C++ Levitation Units

This is an extension to C++17, and it introduces original modularity support for C++.

Basic concepts

Levitation Packages is a replacement for C/C++ #include directives. In C++ Levitation mode the latter is still supported, but only as landing pad for legacy C++ code.

Simplest things

Example 1

Let's consider simple example program with one package and two classes.

Defining units

MyPackage/UnitA.cppl

class A {
public:
  static void sayHello() {
    // send hello messge here
  }
};

It looks pretty much like a regular C++ class. But instead it is defined as a Levitation Unit. Implicitly this class is sorrounded by a namespace MyPackage::UnitA.

To demonstrate that, let's take look at another class which uses the first one.

MyPackage/UnitB.cppl

#import MyPackage::UnitA
class B {
public:
  static void useA() {
    MyPackage::UnitA::A::sayHello();
  }
};

Here we import MyPackage::UnitA and, it allows to use its contents inside dependent unit.

Fanally we want to compile it into application. So we've got to add main function somewhere. But how?

Indeed everything in unit implicitly surrounded by namespace scopes. Whilst "main" should be defined at the global scope.

There is another special thing in C++ Levitation.

Defining global namespace

It is possible to define "global" namespace in units. It is acheived by namespace :: {...} syntax.

So here's how we can define main function.

main.cpp

#import MyPackage::UnitB

namespace :: { // enter global namespace
  int main() {
    MyPackage::UnitB::B::useA();
    return 0;
  }
}

Gathering together

In this example we have defined two classes A and B. Namely A is defined in MyPackage::UnitA and B is defined in MyPackage::UnitB. B calls static method MyPackage::UnitA::A::sayHello().

In order to tell compiler that MyPackage::UnitB depends on MyPackage::UnitA, we added #import directive in top of UnitA.cppl file.

In our example we just call MyPackage::B::useA() and then return 0.

Note: #include directives are also supported. But whenever programmer uses them, current unit and all dependent units will include whatever #include directive refers to. So it is always better to use #import directive whenever it's possible.

Note: #import definitions must be a first definitions in source file.

Special cases

Regular C++ allows to separate declaration from definition. You can include declaration everywhere, whilst definition is restricted by ODR rule and you should put it into separate file.

In C++ Levitation it is done semiautomatically. We say "semi" because we keep possibility to notify compiler whenever things are to be considered as definition. We also changed inline methods treatment.

Inline methods

In regular C++ the inline methods are implicitly inlined. So if you include declaration with inline methods in several different definitions you'll get same code inlined into several places.

In C++ Levitation all methods are considered as non-inline and externally visible, unless inline is not specified.

  class A {
  public:
    // Method below is non-inline and static, only its prototype will be
    // visible for other units.
    static void availableExternally() {
      // ...
    }

    // Method below is non-inline and non-static, same here, only
    // its prototype will be visible for other units.
    void availableExternallyToo() {
      // ...
    }
    
    // Method below inline and static. Regular inline rules applied.
    // Whenever you call it in other units whole its contents
    // copied into caller's body.
    static inline void inlineStaticMethod() {
      // ...
    }   
  };

The notion of body, #body directive

This is how we allow to notify compiler about definition part of unit.

class A {
public:
  // Non-inline method with in-place definition.
  static void inPlace() {
    // ...
  }

  // Method with external definition
  static void externallyDefined();
};
  
#body

void A::externallyDefined() {
  // Method definition
} 

Round-trip dependencies

Consider two classes A and B.

  • Class B somehow refers to A.
  • While class A in its definition parts also refers to class B.

If you'll use regular #import directive, then you'll get a circular dependency.

MyPackage/UnitB.cppl

#include <iostream>
#import MyPackage::UnitA
class B {
public:
  static void useA() {
    MyPackage::UnitA::A::sayHello();
  }
  
  static void sayer(const char* msg) {
    std::cout << msg << std::endl;
  }
};

MyPackage/UnitA.cppl (wrong)

#import MyPackage::UnitB // error, circular dependency
class A {
public:
  static void sayHello() {
    MyPackage::UnitB::B::sayer("Hello"); // attempt to use 'B' in body of 'A'
  }
};

In C++ it is resolved by implicit separation onto .h and .cpp files.

In C++ Levitation we also separate units onto declaration and definition, but it is done by cppl compiler automatically. It is possible though to control where exactly you want to import unit.

[bodydep] attribute for #import

[bodydep] says to compiler, that we want to import unit into definition part only.

MyPackage/UnitA.cppl (good)

#import [bodydep] MyPackage::UnitB // OK
class A {
public:
  static void sayHello() {
    MyPackage::UnitB::B::sayer("Hello"); // attempt to use 'B' in body of 'A'
  }
};

Thus we got rid of circular dependency.

  • UnitA consists of two nodes: declaration and definition
  • Same for UnitB

So in current example we defined:

  • UnitB definition depends on
    • UnitB declaration
    • UnitA declaration
  • UnitA definition depends on
    • UnitA declaration
    • UnitB declaration
  • UnitB declaration depends on
    • UnitA declaration
  • UnitA declaration has no dependencies

See illustration on paste.pics

So after all there are no cycles.

Another way to import unit only for body part is to put it after #body directive. In this case [bodydep] attribute is not required.

MyPackage/UnitA.cppl (good, 2nd option)

class A {
public:
  static void sayHello() {
    MyPackage::UnitB::B::sayer("Hello"); // attempt to use 'B' in body of 'A'
  }
};
#body
#import MyPackage::UnitB // OK

Project structure, limitations

Good laws are limitations of our worst to release our best.

In C++ Levitation mode source locations are limited by following rule:

Source file path corresponds to its unit location.

For example, unit com::MyOuterScope::MyPackage should be located at path <project-root>/com/MyOuterScope/MyPackage.cppl.

Getting started

Getting C++ Levitation Compiler from sources

C++ Levitation Compiler implementation is based on LLVM Clang frontend.

  1. Clone C++ Levitation repository

    git clone https://github.com/kaomoneus/cppl.git cppl
    cd cppl
    git checkout levitation-master
  2. Create directory for binaries, for example '../cppl.build'

  3. cd ../cppl.build

  4. Run cmake (assuming you want use 'cppl.instal' as directory with installed binaries, and 'cppl' is accessable as '../cppl').

    cmake -DLLVM_ENABLE_PROJECTS=clang \
          -DCMAKE_INSTALL_PREFIX=cppl.install \
          -G "Unix Makefiles" ../cppl
  5. make

  6. make check-clang

  7. make install

  8. alias cppl=<path-to-cppl.install>\bin\cppl

How to build executable with C++ Levitation Compiler

In order to build C++ Levitation code user should provide compiler with following information:

  • Project root directory (by default it is current directory).
  • Number of parallel jobs. Usually it is double of number of available CPU cores.
  • Name of output file, by default is 'a.out'.

Consider we want to compile project located at directory 'my-project' with main located at 'my-project/my-project.cpp'.

Assuming we have a quad-core CPU we should run command:

cppl -root="my-project" -j8 -o app.out

If user is not fine with long and complicated command-lines, then she could rename 'my-project.cpp' to 'main.cpp' and change directory to 'my-project'.

Then build command could be reduced to

cppl -j8 -o app.out

Or even just

cppl

In latter case compiler will use single thread compilation and saves executable as a.out.

Building library

Related tasks: L-4, L-27

Just like a traditional C++ compilers, cppl produces set of object files.

Library creation is a bit out of compilers competence.

But, it is possible to inform compiler that we need object files to be saved somewhere for future use.

As long as we working with non-standard C++ source code, we also need to generate .h file with all exported declarations.

Or if we going to use it with other C++ Levitation project it will also generate .cppl declaration files.

Finally, we obtain set of object files, a set of regular C++ .h files, and as an alternative set of truncated .cppl files. Having this at hands it is possible to create library with standard tools.

For example, building static library with gcc tools and Bash consists of 2 steps (assuming current directory is project root, and compiler uses single thread):

  1. cppl -h=my-project.h -c=lib-objects
  2. ar rcs my-project.a $(ls lib-objects/*.o)

The only difference to regular C++ approach is step 1. On this step we ask cppl to produce legacy object files and .h file.

  • -h=<filename> asks compiler to generate C++ header file, and save it with '<filename>' name.
  • -c=<directory> asks compiler to produce object files and store them in directory with '<directory>' name. It also tells compiler, that there is no main file. Theoretically it is still possible to declare int main() somewhere though.

On step 2 ar tool is instructed to create a static library my-project.a and include into it all objects from lib-objects directory.

Theory of operation. Manual build (TODO)

Packages

No packages published

Languages

  • C++ 45.7%
  • LLVM 29.2%
  • C 11.0%
  • Assembly 10.4%
  • Python 1.3%
  • Objective-C 0.7%
  • Other 1.7%