[toc]
This is an extension to C++17, and it introduces original modularity support for C++.
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.
Let's consider simple example program with one package and two classes.
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.
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;
}
}
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.
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.
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() {
// ...
}
};
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
}
Consider two classes A
and B
.
- Class
B
somehow refers toA
. - While class
A
in its definition parts also refers to classB
.
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]
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
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
.
C++ Levitation Compiler implementation is based on LLVM Clang frontend.
-
Clone C++ Levitation repository
git clone https://github.com/kaomoneus/cppl.git cppl cd cppl git checkout levitation-master
-
Create directory for binaries, for example '../cppl.build'
-
cd ../cppl.build
-
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
-
make
-
make check-clang
-
make install
-
alias cppl=<path-to-cppl.install>\bin\cppl
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.
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):
cppl -h=my-project.h -c=lib-objects
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 declareint 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.