Skip to content

Latest commit

 

History

History
1140 lines (1010 loc) · 42.7 KB

File metadata and controls

1140 lines (1010 loc) · 42.7 KB

ACT logo Automatic Component Toolkit

Tutorial: Generating a C++ shared library and bindings for different languages

Table of Contents

1. Introduction

This tutorial demonstrates how to set up and develop a shared library using ACT, and it can be followed step by step. The focus is not on specific features and details of ACT: its parameters, elements, options and switches are of the interface definition language are documented in the Documentation. Instead, it lays out a potential workflow around ACT.

If you just want to see a project based no ACT, you can find the final project in the folder LibPrimes_component.

Consider you want to implement and use a library that provides datatypes and algorithms related to prime numbers. Such a library will be implemented in this tutorial. In particular, the library will be able to perform the prime-factorization of positive integers and generate prime numbers.

Prime factor decomposition

Given a positive integer n, find distinct prime numbers p_1, ..., p_k, and positive integers e_1, ..., e_k such that

n = p_1 ^ e_1 * p_2 ^ e_2 * ... * p_k ^ e_k

The values e_j are called multiplicity.

NOTE There are much more efficient algorithms and more suitable software packages to perform these tasks.

2. Requirements

  • CMake
  • A C++ compiler / development environment. This tutorial was tested with Visual Studio 2017, but should also work with GCC and make or other development tools.
  • ACT: This tutorial is tested to work with release 1.6.0 of ACT. You can get it from the releases page. Decide on a location for your tutorial's component to live in, and download the binary for your platform into this folder. Alternatively, stick it somwhere in your $PATH.

3. The Library's Implementation

3.1. The definition of the component

An ACT component's interface is fully described by its IDL file. This section sets up an IDL-file for LibPrimes.

First, copy the snippet, a bare-bone IDL-file, and save it into libPrimes.xml in your component's folder.

<?xml version="1.0" encoding="UTF-8"?>
<component xmlns="http://schemas.autodesk.com/netfabb/automaticcomponenttoolkit/2018" 
	libraryname="Prime Numbers Library" namespace="LibPrimes" copyright="PrimeDevelopers" year="2019" basename="libprimes"
	version="1.0.0">
	<license>
		<line value="All rights reserved." />
	</license>
	<bindings>
		<binding language="CDynamic" indentation="tabs" />
		<binding language="CppDynamic" indentation="tabs" />
		<binding language="Cpp" indentation="tabs" />
		<binding language="Pascal" indentation="2spaces" />
		<binding language="Python" indentation="tabs" />
		<binding language="CSharp" indentation="tabs" />
		<binding language="Go" indentation="tabs" />
	</bindings>
	<implementations>
		<implementation language="Cpp" indentation="tabs"/>
		<implementation language="Pascal" indentation="2spaces" stubidentifier="impl"/>
	</implementations>
	<errors>
		<error name="NOTIMPLEMENTED" code="1" description="functionality not implemented" />
		<error name="INVALIDPARAM" code="2" description="an invalid parameter was passed" />
		<error name="INVALIDCAST" code="3" description="a type cast failed" />
		<error name="BUFFERTOOSMALL" code="4" description="a provided buffer is too small" />
		<error name="GENERICEXCEPTION" code="5" description="a generic exception occurred" />
		<error name="COULDNOTLOADLIBRARY" code="6" description="the library could not be loaded" />
		<error name="COULDNOTFINDLIBRARYEXPORT" code="7" description="a required exported symbol could not be found in the library" />
		<error name="INCOMPATIBLEBINARYVERSION" code="8" description="the version of the binary interface does not match the bindings interface" />
	</errors>
	<class name="Base">
	</class>
	<global baseclassname="Base" acquiremethod="AcquireInstance"
		releasemethod="ReleaseInstance" versionmethod="GetVersion" errormethod="GetLastError">
		<method name="GetVersion" description = "retrieves the binary version of this library.">
			<param name="Major" type="uint32" pass="out" description="returns the major version of this library" />
			<param name="Minor" type="uint32" pass="out" description="returns the minor version of this library" />
			<param name="Micro" type="uint32" pass="out" description="returns the micro version of this library" />
		</method>
		<method name="GetLastError" description="Returns the last error recorded on this object">
			<param name="Instance" type="class" class="Base" pass="in" description="Instance Handle" />
			<param name="ErrorMessage" type="string" pass="out" description="Message of the last error" />
			<param name="HasError" type="bool" pass="return" description="Is there a last error to query" />
		</method>
		<method name="AcquireInstance" description="Acquire shared ownership of an Instance">
			<param name="Instance" type="class" class="Base" pass="in" description="Instance Handle" />
		</method>
		<method name="ReleaseInstance" description="Releases shared ownership of an Instance">
			<param name="Instance" type="class" class="Base" pass="in" description="Instance Handle" />
		</method>
	</global>
</component>

It's elements define the following:

  • The attributes of \<component> define essential properties and naming conventions of the component, and contains all other info about the component.
  • \<error> define the error codes to be used in the library that will be exposed for it's consumers. The errors listed in this snippet are required.
  • \<global> defines the global functions that can be used as entry points into the component. It must contain an acquiremethod, a releasemethod, a versionmethod, and an errormethod with the signatures in this snippet. They will be explained in this section. The syntax for methods will be explained when we add new classes and functions to the IDL-file now.
  • The baseclassname-attribute of the \<global>-section points to the \<class> "Base", which will be the baseclass for all other classes defined in this component.

3.1.1 A struct for prime factors

In the \<component> element, add a struct PrimeFactor that encodes a prime number with a multiplicity.

<struct name="PrimeFactor">
	<member name="Prime" type="uint64" />
	<member name="Multiplicity" type="uint32" />
</struct>

3.1.1 Calculator

After the Base-class in the \<component>-element add a class Calculator which implements the base class for the different calculators we will expose in our API.

<class name="Calculator" parent="Base">
	<method name="GetValue" description="Returns the current value of this Calculator">
		<param name="Value" type="uint64" pass="return" description="The current value of this Calculator" />
	</method>
	<method name="SetValue" description="Sets the value to be factorized">
		<param name="Value" type="uint64" pass="in" description="The value to be factorized" />
	</method>
	<method name="Calculate" description="Performs the specific calculation of this Calculator">
	</method>
</class>

The GetValue method returns the current value on which the calculator operates. It has one parameter of type uint64, which will be returned. The SetValue method sets the value on which the calculator operates. It has one input parameter of type uint64. The method Calculate performs the specific calculation of this calculator.

3.1.2 The FactorizationCalculator

Add another class FactorizationCalculator as a child class of Calculator.

<class name="FactorizationCalculator" parent="Calculator">
	<method name="GetPrimeFactors" description="Returns the prime factors of this number (without multiplicity)">
		<param name="PrimeFactors" type="structarray" class="PrimeFactor" pass="out" description="The prime factors of this number" />
	</method>
</class>

We will implement the actual prime-factor decomposition by overwriting the the Calculate method of its parent class. FactorizationCalculator introduces one additional method that outputs an array of structs.

3.1.3 The SieveCalculator

Add another class SieveCalculator as a child class of Calculator.

<class name="SieveCalculator" parent="Calculator">	
	<method name="GetPrimes" description="Returns all prime numbers lower or equal to the sieve's value">
		<param name="Primes" type="basicarray" class="uint64" pass="out" description="The primes lower or equal to the sieve's value" />
	</method>
</class>

Again, we will implement the actual calculation of primes by overwriting the the Calculate method of its parent class. SieveCalculator introduces one additional method that will return the array of prime numbers of type uint64.

3.1.4 Error: no result available

We want to use the error LIBPRIMES_ERROR_NORESULTAVAILABLE when a user tries to retrieve results from a calculator without having performed a calculation before. Thus add a new error

<error name="NORESULTAVAILABLE" code="9" description="no result is available" />

3.1.5 Additions to the global section

The global section requires two more methods, that are used as entry points to the component's functionality:

<method name="CreateFactorizationCalculator" description="Creates a new FactorizationCalculator instance">
	<param name="Instance" type="class" class="FactorizationCalculator" pass="return" description="New FactorizationCalculator instance" />
</method>

CreateFactorizationCalculator specifies a global function that will create a new instance of the FactorizationCalculator.

<method name="CreateSieveCalculator" description="Creates a new SieveCalculator instance">
	<param name="Instance" type="class" class="SieveCalculator" pass="return" description="New SieveCalculator instance" />
</method>

CreateSieveCalculator specifies a global function that will create a new instance of the SieveCalculator.

This concludes the complete specification of LibPrimes's interface.

NOTE You can download the initial IDL-file for libPrimes here.

3.2. Automatic Source Code Generation

With the complete interface definition in libPrimes.xml, we can now turn on ACT:

act.exe libPrimes.xml

This generates a folder LibPrimes_component with three subfolders, Bindings, Examples and Implementation.

Let's focus on Implementation/CPP for now, which contains a folder Interfaces and Stubs.

3.2.1 Interfaces

Consider all files in the Interfaces folder read only for your development. They will be regenerated/overwritten if you run ACT again, and you should never have to modify them manually. Usually, you will not include them in the source code control system of your component.

The libprimes_interfaces.hpp-file contains all classes from the IDL as pure abstract C++ classes. E.g. have a look at the interfaces LibPrimes::ICalculator and LibPrimes::IFactorizationCalculator:

/*...*/
class ICalculator : public virtual IBase {
public:
	virtual LibPrimes_uint64 GetValue() = 0;
	virtual void SetValue(const LibPrimes_uint64 nValue) = 0;
	virtual void Calculate() = 0;
};

class IFactorizationCalculator : public virtual IBase, public virtual ICalculator {
public:
	virtual void GetPrimeFactors(LibPrimes_uint64 nPrimeFactorsBufferSize,
	LibPrimes_uint64* pPrimeFactorsNeededCount, LibPrimes::sPrimeFactor * pPrimeFactorsBuffer) = 0;
};
/*...*/

The libprimes_interfaceexception.hpp and libprimes_interfaceexception.cpp files contain the definition of the component's exception class.

The libprimes_interfacewrapper.cpp file implements the forwarding of the C89-interface functions to the classes you will implement. It also translates all exceptions into error codes.

LibPrimesResult libprimes_calculator_getvalue(LibPrimes_Calculator pCalculator, LibPrimes_uint64 * pValue)
{
	IBase* pIBaseClass = (IBase *)pCalculator;
	try {
		if (pValue == nullptr)
			throw ELibPrimesInterfaceException (LIBPRIMES_ERROR_INVALIDPARAM);
		ICalculator* pICalculator = dynamic_cast<ICalculator*>(pIBaseClass);
		if (!pICalculator)
			throw ELibPrimesInterfaceException(LIBPRIMES_ERROR_INVALIDCAST);
		*pValue = pICalculator->GetValue();
		return LIBPRIMES_SUCCESS;
	}
	catch (ELibPrimesInterfaceException & Exception) {
		return handleLibPrimesException(pIBaseClass, Exception);
	}
	catch (std::exception & StdException) {
		return handleStdException(pIBaseClass, StdException);
	}
	catch (...) {
		return handleUnhandledException(pIBaseClass);
	}
}

3.2.2 Implementation Stubs

The files in the Stubs folder are the "actual" source code, which you will modify. This will contain your domain logic.

For each class in the IDL, a pair of header and source files has been generated. They contain a concrete class definition derived from the corresponding interface in interfaces.hpp.

class CFactorizationCalculator : public virtual IFactorizationCalculator, public virtual CCalculator {
public:
	void GetPrimeFactors(LibPrimes_uint64 nPrimeFactorsBufferSize,
		LibPrimes_uint64* pPrimeFactorsNeededCount, LibPrimes::sPrimeFactor * pPrimeFactorsBuffer) override;
};

The autogenerated implementation of each of a class's methods throws a NOTIMPLEMENTED exception:

void CFactorizationCalculator::GetPrimeFactors(LibPrimes_uint64 nPrimeFactorsBufferSize, LibPrimes_uint64* pPrimeFactorsNeededCount, LibPrimes::sPrimeFactor * pPrimeFactorsBuffer)
{
	throw ELibPrimesInterfaceException(LIBPRIMES_ERROR_NOTIMPLEMENTED);
}

3.3. Implement and build the library

The Implementations/CPP folder already contains a CMakeLists.txt that allows you to build the generated sources into a shared library.

The following code snipped sets up a Visual Studio solution:

cd LibPrimes_component/Implementation/
mkdir _build
cd _build
cmake .. -G "Visual Studio 15 Win64"
cmake --build .

Adjust the CMake-Generator for your development environment, if required.

Now we can start actually implementing the library.

3.3.1. Required steps for every ACT component

GetVersion function

void CWrapper::GetVersion(LibPrimes_uint32 & nMajor, LibPrimes_uint32 & nMinor, LibPrimes_uint32 & nMicro)
{
	nMajor = LIBPRIMES_VERSION_MAJOR;
	nMinor = LIBPRIMES_VERSION_MINOR;
	nMicro = LIBPRIMES_VERSION_MICRO;
}

ACT advocates the semantic versioning scheme and all components defined by ACT provide a function that returns the major-, minor- and micro-version. LIBPRIMES_VERSION_* are defined automatically from the version attribute of the \<component> in libPrimes.xml.

In addition an ACT component CAN provide prerelease- and build-information by defining a prereleasemethod and buildinfomethod, see the documentation.

Create-FactorizationCalculator

For all methods in the IDL that are used to return a new instance of a class, code similar to this needs to be implemented.

#include "libprimes_factorizationcalculator.hpp"
/*...*/
IFactorizationCalculator * CWrapper::CreateFactorizationCalculator()
{
	return new CFactorizationCalculator();
}

Acquire- and Release Function

void CWrapper::AcquireInstance(IBase* pInstance)
{
	IBase::AcquireBaseClassInterface(pInstance);
}

AcquireBaseClassInterface increases the reference count of pInstance. This can be used to share ownership of IBase*-instances. See the Example for shared ownership and component injection to learn more about this.

void CWrapper::ReleaseInstance(IBase* pInstance)
{
	IBase::ReleaseBaseClassInterface(pInstance);
}

ReleaseBaseClassInterface decreases the reference count of pInstance and, if this was the last reference, deletes the instance.

NOTE: Obvioulsy, you can do something more clever/robust, than simply handing out "new"-ed chunks of memory at creation and deleting them if asked for it via ReleaseInstance" (e.g. store them in a set-datastructure, so that you can "free" them if the consumer does not do so). However, this is solution is fine in the context of ACT, as all automatically generated bindings (except C) handle the lifetime of all generated IBase instances.

Error Function

bool CWrapper::GetLastError(IBase* pInstance, std::string & sErrorMessage)
{
	return pInstance->GetLastErrorMessage(sErrorMessage);
}

This method queries the last error that occurred during a method of an instance. See 4.1.1 Exception/Error handling for more details.

3.3.2. Domain code implementation: Steps for LibPrimes

CreateFunctions

Implement the missing CreateSieveCalculator

#include "libprimes_sievecalculator.hpp"
/*...*/
ISieveCalculator * CWrapper::CreateSieveCalculator()
{
	return new CSieveCalculator();
}

Calculator

Add a protected member m_value to the CCalculator

class CCalculator : public virtual ICalculator {
protected:
	LibPrimes_uint64 m_value;
/*...*/
}

The GetValue/SetValue methods of the Calculator are straight-forward:

LibPrimes_uint64 CCalculator::GetValue()
{
	return m_value;
}

void CCalculator::SetValue(const LibPrimes_uint64 nValue)
{
	m_value = nValue;
}

We can safely leave the Calculate-method untouched, or alternatively, declare it pure virtual, and remove its implementation.

FactorizationCalculator

Add an array that holds the calculated prime factors as private member in CFactorizationCalculator and a public Calculate method:

class CFactorizationCalculator : public virtual IFactorizationCalculator, public virtual CCalculator {
private:
	std::vector<sPrimeFactor> primeFactors;
public:
	void Calculate() override;
/*...*/
}

A valid implementation of GetPrimes is the following

void CFactorizationCalculator::GetPrimeFactors(LibPrimes_uint64 nPrimeFactorsBufferSize, LibPrimes_uint64* pPrimeFactorsNeededCount, LibPrimes::sPrimeFactor * pPrimeFactorsBuffer)
{
	if (primeFactors.size() == 0)
		throw ELibPrimesInterfaceException(LIBPRIMES_ERROR_NORESULTAVAILABLE);

	if (pPrimeFactorsNeededCount)
		*pPrimeFactorsNeededCount = (LibPrimes_uint64)primeFactors.size();

	if (nPrimeFactorsBufferSize >= primeFactors.size() && pPrimeFactorsBuffer)
	{
		for (int i = 0; i < primeFactors.size(); i++)
		{
			pPrimeFactorsBuffer[i] = primeFactors[i];
		}
	}
}

The following snippet calculates the prime factor decomposition of the calculator's member m_value.

void CFactorizationCalculator::Calculate()
{
	primeFactors.clear();

	LibPrimes_uint64 nValue = GetValue();
	for (LibPrimes_uint64 i = 2; i <= nValue; i++) {
		sPrimeFactor primeFactor;
		primeFactor.m_Prime = i;
		primeFactor.m_Multiplicity = 0;
		while (nValue % i == 0) {
			primeFactor.m_Multiplicity++;
			nValue = nValue / i;
		}
		if (primeFactor.m_Multiplicity > 0) {
			primeFactors.push_back(primeFactor);
		}
	}
}

SieveCalculator

Add an array that holds the calculated prime numbers as private member in CSieveCalculator and a public Calculate method:

class CSieveCalculator : public virtual ISieveCalculator, public virtual CCalculator {
private:
	std::vector<LibPrimes_uint64> primes;
public:
	void Calculate() override;
/*...*/
}

The GetPrimes method is analogous to the above GetPrimeFactors

void CSieveCalculator::GetPrimes(LibPrimes_uint64 nPrimesBufferSize, LibPrimes_uint64* pPrimesNeededCount, LibPrimes_uint64 * pPrimesBuffer)
{
	if (primes.size() == 0)
		throw ELibPrimesInterfaceException(LIBPRIMES_ERROR_NORESULTAVAILABLE);
	if (pPrimesNeededCount)
		*pPrimesNeededCount = (LibPrimes_uint64)primes.size();
	if (nPrimesBufferSize >= primes.size() && pPrimesBuffer)
	{
		for (int i = 0; i < primes.size(); i++)
		{
			pPrimesBuffer[i] = primes[i];
		}
	}
}

Finally, the following calculate method implements the Sieve of Eratosthenes

#include <cmath>
/* ... */
void CSieveCalculator::Calculate()
{
	primes.clear();

	std::vector<bool> strikenOut(m_value + 1);
	for (LibPrimes_uint64 i = 0; i <= m_value; i++) {
		strikenOut[i] = i < 2;
	}
	LibPrimes_uint64 sqrtValue = (LibPrimes_uint64)(std::sqrt(m_value));
	for (LibPrimes_uint64 i = 2; i <= sqrtValue; i++) {
		if (!strikenOut[i]) {
			primes.push_back(i);
			for (LibPrimes_uint64 j = i * i; j < m_value; j += i) {
				strikenOut[j] = true;
			}
		}
	}
	for (LibPrimes_uint64 i = sqrtValue; i <= m_value; i++) {
		if (!strikenOut[i]) {
			primes.push_back(i);
		}
	}
}

This concludes the implementation of version 1.0.0 of the library.

4. The Consumers

This section will demonstrate how easy it is to consume "LibPrimes". The autogenerated Bindings folder contains multiple language bindings, i.e. APIs for the component native to different languages.

By default ACT creates source code for example command line applications in all specified export languages. Each of the autogenerated example applications will load the library, retrieve the binary's version and print it.

4.1. Cpp Dynamic

The folder Examples\CppDynamic contains a simple CMake project and a LibPrimes_Example.cpp that holds the main function of the command line application. Generate a solution and build it

cd LibPrimes_component/Examples/CppDynamic
mkdir _build
cd _build
cmake .. -G "Visual Studio 15 Win64"

You will have to modify the path to the binary library file we created in section 3. The Library's Implementation.

#include <iostream>
#include "libprimes_dynamic.hpp"

int main()
{
  try
  {
    std::string libpath = ""; // TODO: put the location of the LibPrimes-library file here.
    auto wrapper = LibPrimes::CWrapper::loadLibrary(libpath + "/libprimes."); // TODO: add correct suffix of the library
    LibPrimes_uint32 nMajor, nMinor, nMicro;
    wrapper->GetVersion(nMajor, nMinor, nMicro);
    std::cout << "LibPrimes.Version = " << nMajor << "." << nMinor << "." << nMicro;
    std::cout << std::endl;
  }
  catch (std::exception &e)
  {
    std::cout << e.what() << std::endl;
    return 1;
  }
  return 0;
}

Now build the solution

cmake --build .

and run the command line application. It should print out

LibPrimes.Version = 1.0.0

Using LibPrimes's functionality is simple then, e.g. to calculate the prime factor decomposition of a number, add the following at the end of the try block:

auto factorization = wrapper->CreateFactorizationCalculator();
factorization->SetValue(735);
factorization->Calculate();
std::vector<LibPrimes::sPrimeFactor> primeFactors;
factorization->GetPrimeFactors(primeFactors);

std::cout << factorization->GetValue() << " = ";
for (size_t i = 0; i < primeFactors.size(); i++) {
	auto pF = primeFactors[i];
	std::cout << pF.m_Prime << "^" << pF.m_Multiplicity << ((i < (primeFactors.size() - 1)) ? " * " : "");
}
std::cout << std::endl;

Rebuild the solution and run the compiled application. It should print out

LibPrimes.Version = 1.0.0
735 = 3^1 * 5^1 * 7^2

Have a look at the implementation of the dynamic Cpp bindings in the single header file libprimes_dynamic.hpp, e.g. a C++ function call is forwarded to the thin C-interface as follows

void CFactorizationCalculator::GetPrimeFactors(std::vector<sPrimeFactor> & PrimeFactorsBuffer)
{
	LibPrimes_uint64 elementsNeededPrimeFactors = 0;
	LibPrimes_uint64 elementsWrittenPrimeFactors = 0;
	CheckError(m_pWrapper->m_WrapperTable.m_FactorizationCalculator_GetPrimeFactors(m_pHandle, 0, &elementsNeededPrimeFactors, nullptr));
	PrimeFactorsBuffer.resize((size_t) elementsNeededPrimeFactors);
	CheckError(m_pWrapper->m_WrapperTable.m_FactorizationCalculator_GetPrimeFactors(m_pHandle, elementsNeededPrimeFactors, &elementsWrittenPrimeFactors, PrimeFactorsBuffer.data()));
}

Note For the out-array, m_FactorizationCalculator_GetPrimeFactors is called twice. First, to obtain the size of the array, secondly to actually fill the array with content. Thus, it would be very inefficient to perform the calculation during the GetPrimeFactors-method

4.1.1 Exception/Error handling

All return values of functions are translated to C++ exceptions via the CheckError-methods of the base-class and the wrapper object:

class CBase {
/* ... */
	void CheckError(LibPrimesResult nResult)
	{
		if (m_pWrapper != nullptr)
			m_pWrapper->CheckError(this, nResult);
	}
/* ... */
}
/* ... */
inline void CWrapper::CheckError(CBase * pBaseClass, LibPrimesResult nResult)
{
	if (nResult != 0) {
		std::string sErrorMessage;
		if (pBaseClass != nullptr) {
			GetLastError(pBaseClass, sErrorMessage);
		}
		throw ELibPrimesException(nResult, sErrorMessage);
	}
}

4.2. Python

The folder Examples\Python contains a python script LibPrimes_Example.py that makes use of the Python bindings LibPrimes.py.

In the autogenerated main function set the correct path to the library's binary:

import os
import sys
sys.path.append("../../Bindings/Python")
import LibPrimes

def main():
	libpath = '' # TODO add the location of the shared library binary here
	wrapper = LibPrimes.Wrapper(os.path.join(libpath, "LibPrimes"))
	
	major, minor, micro = wrapper.GetVersion()
	print("LibPrimes version: {:d}.{:d}.{:d}".format(major, minor, micro))

if __name__ == "__main__":
	try:
		main()
	except LibPrimes.ELibPrimesException as e:
		print(e)

and add the following code at the end of the main function:

factorization = wrapper.CreateFactorizationCalculator()
factorization.SetValue(735)
factorization.Calculate()
primeFactors = factorization.GetPrimeFactors()
productString = "*"
print("{:d} = ".format(factorization.GetValue()), end="")
for i in range(0, len(primeFactors)):
	pF = primeFactors[i]
	if i == len(primeFactors) - 1:
		productString = "\n"
	print(" {:d}^{:d} ".format(pF.Prime, pF.Multiplicity), end=productString)

Running this script will, again, print:

LibPrimes version: 1.0.0
735 =  3^1 * 5^1 * 7^2 

4.2.1. Debug the C++-DLL from a Python Host Application

This section explains a neat trick to debug a library that is used in a unrelated project, even in a different, interpreted language:

In our specific setup, open the Visual Studio solution from 3. The Library's Implementation, set LibPrimes as startup project and make sure the Debug configuration is activated.

Open the project's properties and select the entry Debug. Set the Command to the path to your python3 executable and the Command Arguments to the location of the LibPrimes_Example.py VSProperties

Set a breakpoint somewhere in your C++-library code (e.g. in void CFactorizationCalculator::Calculate()) and start debugging.

4.3. Pascal

The folder Examples/Pascal contains a project for the Free Pascal IDE Lazarus.

Open the project LibPrimes_Example.lpi in the IDE, adjust the library path in the TestLibPrimes procedure and build the application.

procedure TLibPrimes_Example.TestLibPrimes ();
var
  ALibPrimesWrapper: TLibPrimesWrapper;
  AMajor, AMinor, AMicro: Cardinal;
  AVersionString: string;
  ALibPath: string;
begin
  writeln ('loading DLL');
  ALibPath := ''; // TODO add the location of the shared library binary here
  ALibPrimesWrapper := TLibPrimesWrapper.Create (ALibPath + '/' + 'libprimes.'); // TODO add the 
  try
    writeln ('loading DLL Done');
    ALibPrimesWrapper.GetVersion(AMajor, AMinor, AMicro);
    AVersionString := Format('LibPrimes.version = %d.%d.%d', [AMajor, AMinor, AMicro]);
    writeln(AVersionString);
  finally
    FreeAndNil(ALibPrimesWrapper);
  end;
end;

The compiled command line application Examples\Pascal\bin\x86_64-win64\Release\LibPrimes_Example.exe will output

loading DLL
loading DLL Done
LibPrimes.version = 1.0.0

4.4 Use of an ACT component in existing code bases

The examples above started of with prepared and autogenerated projects in three different languages. We found that a simple task using dynamic language bindings.

In reality, it is more important to integrate a components functionality into existing, potentially large and complex code bases.

However, the integration of an ACT component into other code bases is as simple as integrating it into a new application. The only requirement is to include the specific language binding file (libprimes_dyanmic.hpp, LibPrimes.py, Unit_LibPrimes.pas) into a project and specifying the location of the component's binary.

This concludes the section on using an ACT component via the autogenerated language bindings.

5. Extending the Library with a Callback

This section goes through the process of adding new functionality to an ACT component. We will first modify the IDL-file, secondly regenerate the interfaces and language bindings and finally adapt one of the example applications to use the new feature of the library.

The new functionality will be a callback that reports the progress during the (potentially) time consuming calculation method of the calculators.

5.1 IDL

Open the IDL-file libPrimes.xml and add a new functiontype element within the \<component>-element.

<functiontype name="ProgressCallback" description="Callback to report calculation progress and query whether it should be aborted">
	<param name="ProgressPercentage" type="single" pass="in" description="How far has the calculation progressed?"/>
	<param name="ShouldAbort" type="bool" pass="out" description="Should the calculation be aborted?"/>
</functiontype>

Instances of this functiontype will require a single parameter as input to report the relative progress of the calculation back to the client, and have a boolean output value to read back from a client, whether LibPrimes should abort the calculation.

Then add a method to the Calculator class that sets the progress callback:

<method name="SetProgressCallback" description="Sets the progress callback function">
	<param name="ProgressCallback" type="functiontype" class="ProgressCallback" pass="in" description="The progress callback" />
</method>

We will handle aborted calculations via a new exceptions, which a client can handle. Thus, add a new \<error>:

<error name="CALCULATIONABORTED" code="10" description="a calculation has been aborted" />

In the semantic versioning scheme adding a new function to a components class requires a minor version update. Thus update the version in the IDL, too:

<component xmlns="http://schemas.autodesk.com/netfabb/automaticcomponenttoolkit/2018" 
	libraryname="Prime Numbers Interface" namespace="LibPrimes" copyright="Automatic Component Toolkit Developers" year="2019" basename="libprimes"
	version="1.1.0">

NOTE You can download the modified IDL-file for libPrimes here.

Finally, recrate interfaces, wrapper and bindings code:

act.exe libPrimes.xml

A quick look at the libprimes_types.hpp and libprimes_interfaces.hpp reveals how the ProgressCallback function type and their usage is declared:

/*
  * ProgressCallback - Callback to report calculation progress and query whether it should be aborted
  *
  * @param[in] fProgressPercentage - How far has the calculation progressed?
  * @param[out] pShouldAbort - Should the calculation be aborted?
  */
  typedef void(*ProgressCallback)(LibPrimes_single, bool *);
class ICalculator : public virtual IBase {
public:
/*...*/
/**
* ICalculator::SetProgressCallback - Sets the progress callback function
* @param[in] pProgressCallback - callback function
*/
virtual void SetProgressCallback(const LibPrimes::ProgressCallback pProgressCallback) = 0;
/*...*/
}

5.2 Library

Reopen the Visual Studio solution from 3. The Library's Implementation. You will not be able to successfully rebuild the solution, since CCalculator does not define the SetProgressCallback-function. To resolve this, add a protected member to CCalculator and define the public SetProgressCallback-function.

libprimes_calculator.hpp

class CCalculator : public virtual ICalculator, public virtual CBase {
protected:
/*...*/
	ProgressCallback m_Callback;
/*...*/
public:
/*...*/
	void SetProgressCallback(const ProgressCallback pProgressCallback) override;
/*...*/
}

libprimes_calculator.cpp

void CCalculator::SetProgressCallback (const LibPrimes::ProgressCallback pProgressCallback)
{
	m_Callback = pProgressCallback;
}

We can use the callback in the calculation function for example like this:

void CFactorizationCalculator::Calculate()
{
	primeFactors.clear();

	LibPrimes_uint64 nValue = m_value;
	for (LibPrimes_uint64 i = 2; i <= nValue; i++) {

		if (m_Callback) {
			bool shouldAbort = false;
			(*m_Callback)(1 - float(nValue) / m_value, &shouldAbort);
			if (shouldAbort) {
				throw ELibPrimesInterfaceException(LIBPRIMES_ERROR_CALCULATIONABORTED);
			}
		}

		sPrimeFactor primeFactor;
		primeFactor.m_Prime = i;
		primeFactor.m_Multiplicity = 0;
		while (nValue % i == 0) {
			primeFactor.m_Multiplicity++;
			nValue = nValue / i;
		}
		if (primeFactor.m_Multiplicity > 0) {
			primeFactors.push_back(primeFactor);
		}
	}
}

Note C function pointers have to be handled carefully. It's good practice to wrap them in more convenient function objects that make their usage safe (i.e. check that the function pointer itself as well as their out-parameters are assigned). Later versions of ACT might do that.

Now, recompile the solution.

5.3 Consumer

The usage of the callback functionality from the client applications is straightforward, as their respective language bindings have already been updated at the end of the previous step.

5.3.1 Cpp Dynamic Application with Callback

Open the solution from Section 4.1, and add a concrete implementation of the "ProgressCallback" function-type:

#include <cmath>
/* ... */
void progressCallback(LibPrimes_single progress, bool* shouldAbort)
{
	std::cout << "Progress = " << std::round(progress * 100) << "%" << std::endl;
	if (shouldAbort) {
		*shouldAbort = progress > 0.5;
	}
}

Using it in main is straightforward:

int main()
{
	/*...*/
	factorization->SetValue(735);
	factorization->SetProgressCallback(progressCallback);
	factorization->Calculate();
	/*...*/
}

Running the compiled command line application will output:

LibPrimes.Version = 1.1.0
Progress = 0%
Progress = 0%
Progress = 67%
LibPrimes Error 10 (LibPrimes Error 10)

Error 10 (CALCULATIONABORTED) notifies us that a calculation has been aborted, as we expected.

5.3.2 Python Application with a callback

Open the Python example from Section 4.2, and add an implementation of the a Python function:

def progressCallback(progress, shouldAbort):
	print("Progress = {:d}%".format(round(progress*100)))
	if (shouldAbort is not None):
		shouldAbort[0] = progress > 0.5

Using it in main requires one to first create a CTypes function pointer cTypesCallback to the Python function. You need to make sure that these CTypes function pointer still exist in the python host application, when it will called by the library, e.g. like this:

# ...
factorization.SetValue(735)
cTypesCallback = LibPrimes.ProgressCallback(progressCallback)
factorization.SetProgressCallback(cTypesCallback)
factorization.Calculate()
# ...

Running the python script will output:

LibPrimes version: 1.1.0
Progress = 0%
Progress = 0%
Progress = 67%
LibPrimesException 10: LibPrimes Error 10

6. Enable journaling

ACT provides automatic journaling, that can be compiled into the software component and enabled/disabled dynamically during the usage of the component.

6.1 Update the IDL file

To add the journaling, simply add a journalmethod to the IDL file libPrimes.xml:

<global baseclassname="Base" releasemethod="ReleaseInstance" versionmethod="GetVersion" errormethod="GetLastError"
		journalmethod="SetJournal" >
<!--...-->
	<method name="SetJournal" description="Handles Library Journaling">
		<param name="FileName" type="string" pass="in" description="Journal FileName" />
	</method>
</global>

Since a new function has been added, another minor version update is required.

<component xmlns="http://schemas.autodesk.com/netfabb/automaticcomponenttoolkit/2018" 
	libraryname="Prime Numbers Interface" namespace="LibPrimes" copyright="Automatic Component Toolkit Developers" year="2019" basename="libprimes"
	version="1.2.0">

NOTE You can download the modified IDL-file for libPrimes here.

Regenerate the implementation and language bindings by running

act.exe libPrimes.xml

Check the updated libprimes_interfacewrapper.cpp and its autogenerated SetJournal method

LibPrimesResult libprimes_setjournal(const char * pFileName)
{
	IBase* pIBaseClass = nullptr;
	try {
		if (pFileName == nullptr) 
			throw ELibPrimesInterfaceException (LIBPRIMES_ERROR_INVALIDPARAM);
		std::string sFileName(pFileName);
		m_GlobalJournal = nullptr;
		if (sFileName != "") {
			m_GlobalJournal = std::make_shared<CLibPrimesInterfaceJournal> (sFileName);
		}
		return LIBPRIMES_SUCCESS;
	}
	catch (ELibPrimesInterfaceException & Exception) {
		return handleLibPrimesException(pIBaseClass, Exception);
	}
	catch (std::exception & StdException) {
		return handleStdException(pIBaseClass, StdException);
	}
	catch (...) {
		return handleUnhandledException(pIBaseClass);
	}
}

Note The journalmethod is not forwarded to a C++ class but completely handled in the wrapper itself.

This is how the journal is filled by other methods:

LibPrimesResult libprimes_calculator_getvalue(LibPrimes_Calculator pCalculator, LibPrimes_uint64 * pValue)
{
	IBase* pIBaseClass = (IBase *)pCalculator;
	PLibPrimesInterfaceJournalEntry pJournalEntry;
	try {
		if (m_GlobalJournal.get() != nullptr)  {
			pJournalEntry = m_GlobalJournal->beginClassMethod(pCalculator, "Calculator", "GetValue");
		}
		if (pValue == nullptr)
			throw ELibPrimesInterfaceException (LIBPRIMES_ERROR_INVALIDPARAM);
		ICalculator* pICalculator = dynamic_cast<ICalculator*>(pIBaseClass);
		if (!pICalculator)
			throw ELibPrimesInterfaceException(LIBPRIMES_ERROR_INVALIDCAST);
		*pValue = pICalculator->GetValue();
		if (pJournalEntry.get() != nullptr) {
			pJournalEntry->addUInt64Result("Value", *pValue);
			pJournalEntry->writeSuccess();
		}
		return LIBPRIMES_SUCCESS;
	}
	catch (ELibPrimesInterfaceException & Exception) {
		return handleLibPrimesException(pIBaseClass, Exception, pJournalEntry.get());
	}
	catch (std::exception & StdException) {
		return handleStdException(pIBaseClass, StdException, pJournalEntry.get());
	}
	catch (...) {
		return handleUnhandledException(pIBaseClass, pJournalEntry.get());
	}
}

Before you can rebuild the LibPrimes shared library, add the journals implementation-file to the ${LIBPRIMES_SRC} in the CMakeLists.txt-file:

set(LIBPRIMES_SRC ${LIBPRIMES_SRC}
	${CMAKE_CURRENT_AUTOGENERATED_DIR}/libprimes_interfaceexception.cpp
	${CMAKE_CURRENT_AUTOGENERATED_DIR}/libprimes_interfacewrapper.cpp
	${CMAKE_CURRENT_AUTOGENERATED_DIR}/libprimes_interfacejournal.cpp
)

Journaling can be a great way to help resolving bugs in software components once they are used by a other people.

6.3 Use journaling in a client

Simply add a call of the SetJournal-method in the clients' code:

6.3.1 CppDynamic Test application

int main()
{
	try
	{
		std::string libpath = (""); // TODO: put the location of the LibPrimes-library file here.
		auto wrapper = LibPrimes::CWrapper::loadLibrary(libpath + "/libprimes."); // TODO: add correct suffix of the library
		wrapper->SetJournal("journal_cppdynamic.xml");
		LibPrimes_uint32 nMajor, nMinor, nMicro;
		std::string sPreReleaseInfo, sBuildInfo;
		wrapper->GetVersion(nMajor, nMinor, nMicro, sPreReleaseInfo, sBuildInfo);
		/*..*/
	}
	/*..*/
}

The generated xml-file is a detailed journal of all calls, parameters, return values and exceptions that go through the interface:

<?xml version="1.0" encoding="UTF-8" ?>
<journal library="LibPrimes" version="1.2.0" xmlns="http://schemas.autodesk.com/components/LibPrimes/1.2.0">
    <entry method="GetVersion" timestamp="0" duration="0">
        <result name="Major" type="uint32" value="1" />
        <result name="Minor" type="uint32" value="2" />
        <result name="Micro" type="uint32" value="0" />
    </entry>
    <entry method="CreateFactorizationCalculator" timestamp="1" duration="0">
        <result name="Instance" type="class" value="000001d259663858" />
    </entry>
    <entry class="Calculator" method="SetValue" timestamp="1" duration="0">
        <instance handle="000001d259663858" />
        <parameter name="Value" type="uint64" value="735" />
    </entry>
    <entry class="Calculator" method="SetProgressCallback" timestamp="1" duration="0">
        <instance handle="000001d259663858" />
    </entry>
    <entry class="Calculator" method="Calculate" errorcode="10" timestamp="1" duration="2">
        <instance handle="000001d259663858" />
    </entry>
    <entry method="GetLastError" timestamp="1115" duration="0">
        <parameter name="Instance" type="class" value="000001d259663858" />
        <result name="ErrorMessage" type="string" value="LibPrimes Error 10" />
        <result name="HasError" type="bool" value="1" />
    </entry>
    <entry method="GetLastError" timestamp="1115" duration="0">
        <parameter name="Instance" type="class" value="000001d259663858" />
        <result name="ErrorMessage" type="string" value="LibPrimes Error 10" />
        <result name="HasError" type="bool" value="1" />
    </entry>
    <entry method="ReleaseInstance" timestamp="1116" duration="0">
        <parameter name="Instance" type="class" value="000001d259663858" />
    </entry>
</journal>

6.3.2 Python Test application

Again, simply call the SetJournal-method and run the script:

def main():
	libpath = '' # TODO add the location of the shared library binary here
	wrapper = LibPrimes.LibPrimesWrapper(os.path.join(libpath, "libprimes"))
	wrapper.SetJournal('journal_python.xml')
	major, minor, micro = wrapper.GetVersion()
	print("LibPrimes version: {:d}.{:d}.{:d}".format(major, minor, micro), end="")
	# ...

The generated xml-journal is similar to the one shown above.

7. Conclusion

This tutorial has walked you through a basic development cycle using ACT and conveyed the power of ACT to simplify and automate the development of software components. Moreover, it has shown how easy ACT components can be integrated in standalone or existing code bases with ease.

Other important aspects of software componentization, like packaging and distribution, source code control, stable interfaces, versioning, releases, ... can also be supported and simplified by ACT.

Tutorials, articles and examples about these topics will follow.