Skip to main content
Skip table of contents

Creating a C# interface


Introduction

Before the introduction of the C# API in Mako 5.1, the best approach to integrating Mako into a C# application was to create a "wrapper" library, written in C++that can be called from C#. Although this approach is no longer strictly necessary, this page provides some useful resources nonetheless.

This example demonstrates the use of a C++ wrapper that can be called from a C# application. This approach is most appropriate to Windows, but can also be applied to other platforms (in this case macOS).

This example uses two IDEs (Integrated Development Environments) for the different parts of the project:

  • JetBrains CLion for the C++ wrapper
  • JetBrains Rider for the C# application

However, the approach can be used with other tools such as Microsoft's Visual Studio Code.

The Windows sample code is a Visual Studio 2017 solution.

Build tools

This example uses the C++ compiler from Xcode which is available from the App Store, and CMake to manage the build process.

Mako wrapper

The wrapper is a shared library that can be called from the C# application. On Windows this would be a DLL, but on Unix or macOS a shared object file. This library (libMakoWrapperDll.so) implements the interface that can be called from managed (C#) code. It links to a static library (libMakoWrapperLib.a) that implements the required function such as instantiating a Mako assembly object, from a file on disk.

C++ components

The CMake file shows what makes up the two components and how they are linked (line 39).

CMakeLists.txt

BASH
cmake_minimum_required(VERSION 3.12)
project(MakoWrapper)

# Basic Setup
set(COMPILER /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++)
set(SDKDIR /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk)
set(CCFLAGS "-mmacosx-version-min=10.11 -stdlib=libc++ -isysroot ${SDKDIR} -D MAKOWRAPPER_EXPORTS")
set(LINKFLAGS "-mmacosx-version-min=10.11 -fvisibility=hidden -bundle -stdlib=libc++ -isysroot ${SDKDIR} -framework Cocoa")
set(LIBDIR ${CMAKE_SOURCE_DIR}/Mako/libs/)

# Compiler setup
set(CMAKE_CXX_COMPILER ${COMPILER})
set(CMAKE_CXX_FLAGS "${CCFLAGS}")

include_directories(Mako)
include_directories(Mako/interface)

#Static library (libMakoWrapperLib.a)
add_library(MakoWrapperLib STATIC
        Mako/WrapperLib/APIHelper.cpp
        Mako/WrapperLib/APIHelper.h
        Mako/WrapperLib/CropboxTransformer.cpp
        Mako/WrapperLib/CropboxTransformer.h
        Mako/WrapperLib/IPageRenderer.cpp
        Mako/WrapperLib/IPageRenderer.h
        Mako/WrapperLib/IPageTransformer.cpp
        Mako/WrapperLib/IPageTransformer.h
        Mako/WrapperLib/MakoAssembly.cpp
        Mako/WrapperLib/MakoAssembly.h
        Mako/WrapperLib/MakoDocument.cpp
        Mako/WrapperLib/MakoDocument.h
        Mako/WrapperLib/PageRenderer.cpp
        Mako/WrapperLib/PageRenderer.h
        Mako/WrapperLib/PageRotationTransformer.cpp
        Mako/WrapperLib/PageRotationTransformer.h
        Mako/WrapperLib/Types.h)

#Shared library (libMakoWrapperDll.so)
add_library(MakoWrapperDll MODULE
        Mako/WrapperDll/MakoWrapperDll.cpp
        Mako/WrapperDll/MakoWrapperDll.h)

target_link_libraries(MakoWrapperDll
        MakoWrapperLib
        ${LIBDIR}/edljaws3.a
        ${LIBDIR}/edllib.a
        ${LIBDIR}/edlpcl5out.a
        ${LIBDIR}/edlpclxlout.a
        ${LIBDIR}/edlquartzrenderer.a
        ${LIBDIR}/edlxpsin.a
        ${LIBDIR}/jawsmako.a
        ${LIBDIR}/libjawsmakotext.a
        ${LINKFLAGS})

add_custom_command(
        TARGET MakoWrapperDll POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy
        ${CMAKE_SOURCE_DIR}/Mako/libs/edlpspdfout.so
        ${CMAKE_CURRENT_BINARY_DIR}/edlpspdfout.so)

MakoWrapperDll

MakoWrapperDll implements functions such as fnOpenAssembly() that can be called from C#. It in turn creates a MakoAssembly object, the constructor for which is implemented in the static library.

Dynamic library

CPP
MAKOWRAPPER_API MakoResult fnOpenAssembly(char* filePath, eFileFormat format, MakoHandle* assemblyHandle)
{
    U8String documentFile(filePath);
    std::cerr << "assembly file path(s):" << documentFile;
    IDocumentAssemblyPtr assembly;

    *assemblyHandle = nullptr;

    try
    {
        IInputPtr fileInput = IInput::create(gMako, format);
        assembly = fileInput->open(documentFile);
    }
    catch (IEDLError& err)
    {
        return err.getErrorCode();
    }

    MakoAssembly *makoAssembly = new MakoAssembly(gMako, assembly, documentFile);
    *assemblyHandle = reinterpret_cast<MakoHandle>(makoAssembly);

    return 0;
}


MAKOWRAPPER_API MakoHandle fnOpenDocument(MakoHandle assemblyHandle, int documentIndex)
{
MakoAssembly *assembly = reinterpret_cast<MakoAssembly*>(assemblyHandle);
MakoDocument* document = assembly->openDocument(documentIndex);

return reinterpret_cast<MakoHandle>(document);
}

MakoAssembly is implemented in the static library (that links to the Mako libraries).

Static library

CPP
MakoAssembly::MakoAssembly(IJawsMakoPtr mako, IDocumentAssemblyPtr assembly, std::string sourceFilePath) :
	m_mako(mako), m_assembly(assembly), m_pdfVersion(DEFAULT_PDF_VERSION), m_sourceFilePath(std::move(sourceFilePath))
{
    IDOMMetadataPtr metadata = m_assembly->getJobMetadata();
    if (metadata)
        m_pdfVersion = getPdfVersion(metadata);
}

MakoDocument* MakoAssembly::openDocument(int documentIndex = 0) const
{
    const IDocumentPtr document = m_assembly->getDocument(documentIndex);
    return new MakoDocument(m_mako, document, m_sourceFilePath);
}

C# wrapper

The managed code imports the wrapper methods in MakoDll.cs.

MakoDll.cs (excerpt)

C#
[DllImport("libMakoWrapperDll.so", CallingConvention = CallingConvention.Cdecl)]
public static extern int fnOpenAssembly([MarshalAs(UnmanagedType.LPStr)] string assemblyFile, eFileFormat format, ref IntPtr assemblyHandle);


[DllImport("libMakoWrapperDll.so", CallingConvention = CallingConvention.Cdecl)]
public static extern IntPtr fnOpenDocument(IntPtr assemblyHandle, int documentIndex);

These functions are called from an interface implementation, for example:

MakoAssembly.cs (excerpt)

C#
using System;

namespace Mako.Wrapper
{
    public class MakoAssembly : IMakoAssembly
    {
        private readonly IntPtr m_handle;

        public MakoAssembly(IntPtr handle)
        {
            m_handle = handle;
        }

        public IMakoDocument GetDocument(int documentIndex = 0)
        {
            return new MakoDocument(MakoDll.fnOpenDocument(m_handle, documentIndex));
        }

        public void SaveAsPdf(string path, bool incremental)
        {
            MakoDll.fnSaveAssemblyAsPdf(m_handle, path, incremental);
        }

        public void Dispose()
        {
            MakoDll.fnCloseAssembly(m_handle);
        }
    }
}

C# application

The wrapper makes developing the application in C# much easier. In this example, one of the Mako samples (makoconverter) is implemented in C#.

It is a command-line application that will convert from one format to another, and accept parameters that modify the output. With the hard work being done by the wrapper, the C# is around 60 lines of source code.

Let's look at the source:

Initialisation

C#
using System;
using System.IO;
using Mako.Wrapper;

namespace MakoConverter
{
    class Program
    {
        static int Main(string[] args)
        {
            // There should be at least two arguments; a source file and a destination file.
            if (args.Length < 2)
            {
                Console.WriteLine("Usage: <source file> <output file> [ parameter=value parameter=value ... ]");
                return 1;
            }

            var inputFilePath = args[0];
            var outputFilePath = args[1];

            // Gather parameters into a list
            var outputParameters = new string[args.Length - 2];
            var parameterCount = 0;
            for (var i = 2; i < args.Length; i++)
            {
                if (args[i].Contains("="))
                    outputParameters[parameterCount++] = args[i];
            }

			// Check input file is there
			if (!File.Exists(inputFilePath))
    			throw new FileNotFoundException("Cannot find path to ", inputFilePath);

The first half gets things started by checking that the number of arguments is correct and that the input file can be found.

Processing with Mako

C#
            // Create a Mako instance
            MakoInstance makoInstance = MakoInstance.Instance;
            if (makoInstance == null)
                throw new InvalidOperationException("Mako is not initialized.");

            // Check input file type is acceptable
            if (makoInstance.formatFromPath(inputFilePath) == eFileFormat.eFFUnknown)
                throw new FileLoadException("That type of file is not recognized ", inputFilePath);

            // Read source document into an assembly
            IMakoAssembly makoAssembly = makoInstance.Open(inputFilePath);

            // Get document (not needed for conversion, but used here to get information about the document)
            IMakoDocument makoDocument = makoAssembly.GetDocument();
            var pageCount = makoDocument.GetPageCount();
            var pageLabel = pageCount == 1 ? "page" : "pages";
            Console.WriteLine($"The document {inputFilePath} has {pageCount} {pageLabel}.");

            // Report page size. Mako's units are 1/96"
            var pageSize = makoDocument.GetPageSize(0);
            Console.WriteLine($"The first page is {pageSize.width / 96 * 25.4:0.00}mm wide by {pageSize.height / 96 * 25.4:0.00}mm high.");

            // Write assembly to output file, copying over any output parameters
            // File extension determines file type
            makoAssembly.Save(outputFilePath, outputParameters);
            return 0;
        }
    }
}

In the second half, a Mako instance is created. This gives access to the wrapper functions.

The first call (makoInstance.formatFromPath()) checks that the file extension is recognizable as a format that Mako can accept. 

The second call (makoInstance.Open()) creates a Mako assembly from the input file. An assembly in Mako is the top-level object, below which are one or more documents. (For PDF, there is only ever one document). Creating an assembly is computationally inexpensive, as Mako employs "lazy loading" (meaning it only reads from the file when it has to, and then only the parts it needs).

With the assembly loaded, it would be possible to save the assembly to a new output file. But before that, this example displays some information about the document. It does so by loading the document with makoAssembly.GetDocument(), then the number of pages it contains is obtained from makoDocument.getPageCount() and the size of the first page (pages are zero-indexed in Mako) from makoDocument.getPageSize(0).

The final call (makoAssembly.Save()) saves the output file to disk, supplying any output parameters that were specified on the command line.

Downloads

Download the example here:

Windows (Visual Studio 2019, Mako 4.8.0)

MakoWrapperExample.zip

macOS (Jetbrains Rider & CLion)

MakoWrapperExample_macOS.zip

JavaScript errors detected

Please note, these errors can depend on your browser setup.

If this problem persists, please contact our support.