Introduction

This is a design document for creating an extension that enables the use of the open-source mesh generation library gmsh in Morpho.

The extension is available here and works with Morpho version 0.6.0. The version of Gmsh used at the time of writing is 4.12.2.

Example use

Here is an example of how the gmsh module can be used.

import gmsh

var gm = GmshLoader("t1.msh") // Load a .msh (gmsh's format) file in Gmsh
gm.launch() // Launch the Gmsh app, where the mesh can now be modified
gm.exportToMorpho("t1Modified.mesh") // Save the modified mesh in Morpho's .mesh format

Excerpt from examples/fromFile.morpho

The Morpho Gmsh API itself can be used by importing the gmshapi module:

import gmshapi
import plot

gmshInitialize(0,0,0)
var coneHeight = 1
var coneRadius = 1
var coneTag = gmshModelOccAddCone(0,0,0, 0,0,coneHeight, coneRadius,0, -1, 2*Pi)
gmshModelOccSynchronize()
gmshOptionSetNumber("Mesh.MeshSizeMax", 0.2)
gmshModelMeshGenerate(3)
var m = GmshLoader().buildMorphoMesh()
Show(plotmesh(m, grade=1))
gmshFinalize()

Excerpt from examples/occ.morpho

This allows for a different implementation of the gmsh module or other functionalities.

Organization

The installation instructions are in Chapter 2. A simple example of a Morpho wrapper extension is discussed in Chapter 3. The structure of the Gmsh API is discussed in Chapter 4. The creation of Morpho bindings for the Gmsh API is discussed in Chapter 5. In Chapter 6, we will discuss the implementation of the gmsh module in Morpho, which will use the Morpho Gmsh API to generate higher-level functionality. While this could also have been auto-generated similar to the Python API, we create this manually in a "Morphonic" object-oriented style. We document some to-do's in Chapter 7.

Acknowledgements

The design of this Morpho-specific binding was improved by inspirations from a Haskell Binding to the Gmsh API by Antero Marjamäki.

This is a pretty unusual piece of code for me, which required understanding the organization of the parent gmsh codebase. This document is rather verbose, and is mostly so for my own benefit. Apologies if it beats around the bush too much!

Installation instructions

  1. This extension requires gmsh to be installed as a dynamic library. Download the source code of the latest stable release of gmsh from https://gitlab.onelab.info/gmsh/gmsh/-/releases.

  2. Build gmsh from source with the dynamic build flag turned on:

    cd build
    cmake -DENABLE_BUILD_DYNAMIC=1 ..
    make
    make install
    
  3. In apigenerator/generate.py, set the correct version of gmsh that you have downloaded by modifying line 7:

    api_version = "gmsh_4_12_2" # For example
    
  4. Run the file:

    python generate.py
    
  5. Navigate to morpho-gmsh

  6. Build the extension by running the following:

    mkdir build
    cd build
    cmake -DCMAKE_BUILD_TYPE=Release ..
    make install
    
  7. Add the extension path to ~/.morphopackages:

    cd ..
    pwd >> ~/.morphopackages
    
  8. Try out the test example

    cd examples
    morpho6 testgmshapi.morpho
    

Anatomy of the Gmsh Extension

As is described in the devguide, a Morpho extension can either be

  1. Simply a .morpho file that we want to be able to import from anywhere, or
  2. An added functionality written in C and loaded as a shared library (imported the same way as a .morpho extension), or
  3. A combination of both.

Normally, we would have used the Gmsh C API to write an extension, hand-picking the functionality we most desire (category #2 above). But we are in luck, as the original Gmsh API is auto-generated, and the code to do that renders itself well to a better approach: creating a Morpho API for Gmsh (by auto-generating Morpho bindings code in C), which we can then use to create a Gmsh module for Morpho written in Morpho itself.

We use Gmsh's own API generator code to create low-level Morpho bindings to the Gmsh functions (loaded as gmshapi). Higher-level functionality, including exporting to a Morpho mesh, is then implemented in a Morpho module (loaded as simply gmsh). Ideally, the end-user should only need to import the gmsh module, but the gmshapi module is also available for those who want to use the low-level Gmsh API directly (via import gmshapi).

Thus, our Gmsh extension is an example of category #3: a C extension that wraps the Gmsh C API, and a .morpho file that uses this extension to create the gmsh module.

Directory Structure

Here is the directory structure of the Gmsh extension:

.
├── CMakeLists.txt
├── LICENSE
├── README.md
├── apigenerator
│   ├── api
│   │   ├── GenApi.py
│   │   └── gen.py
│   └── generate.py
├── examples
│   ├── fromFile.morpho
│   ├── occ.morpho
│   ├── ...
├── lib
│   └── gmshapi.so
├── share
│   ├── help
│   │   ├── gmsh.md
│   │   └── gmshapi.md
│   └── modules
│       └── gmsh.morpho
└── src
    ├── gmshapi.c
    └── gmshapi.h

The .morpho extension file(s) go in the share/modules directory. The C source and header files go in the src directory. The shared library (which will be generated upon compiling the C source) goes in the lib directory. The documentation to be made available for inline help goes in the share/help directory. This should follow the Morpho inline documentation Markdown format (see the devguide). The documentation for the higher level gmsh module is in gmsh.md, and the documentation for the low level gmshapi module is in gmshapi.md (which is auto-generated).

The apigenerator directory contains the scripts used to generate the Morpho API from the Gmsh C API. The generate.py script is used to generate the Morpho API from the Gmsh C API. It downloads gen.py from a stable Gmsh release and runs it using our own GenApi.py file. This creates the following three files:

  1. src/gmshapi.c (the C source file for the extension)
  2. src/gmshapi.h (the C header file for the extension)
  3. share/help/gmshapi.md (the inline documentation for the low level Morpho API)

Building the project using Cmake generates the shared library lib/gmshapi.so from the C source file.

The Gmsh API

Broadly, gmsh is divided into 4 components: geometry, mesh, solver and post-processing (see the Gmsh reference manual).

  1. Geomery: A model in Gmsh is defined using its Boundary Representation (BRep). This geometry is specified through a built-in kernel (geo) and/or an OpenCASCADE kernel (occ). This module allows us to define our domain and perform various operations like Boolean combinations, extrusions, translations etc. without actually meshing the domain.

  2. Mesh: The mesh module provides various algorithms for generating conformal meshes for the domains. The basic entities used in Gmsh are lines, triangles, quadrangles, tetrahedra, prisms, hexahedra and pyramids, and the meshes are by default unstructured. This is great for Morpho since we can always generate a triangular (in 2D) or tetrahedral (in 3D) mesh from Gmsh and export to the Morpho Mesh format (this is done separately in the higher level module.)

  3. Solver: The solver module allows exchange of data with external solvers or other clients through the ONELAB interface.

  4. Post-processing: The post-processing module allows viewing scalar/tensorial field data on top of the meshes.

Gmsh is written in C++, but provides APIs for several languages like C, Python, Julia and Fortran. In the API, the components mentioned above are organized into modules and submodules as follows:

  • gmsh
    • option
    • model
      • mesh
        • field
      • geo
        • mesh
      • occ
        • mesh
    • view
      • option
    • plugin
    • graphics
    • fltk
    • parser
    • onelab
    • logger

In the C API, the function names are given using the lower camel case. For example, the addPoint function in the geo submodule of model will be called gmshModelGeoAddPoint. We maintain the same call signature for the low-level Morpho API.

The APIs are auto-generated using two scripts:

  1. gen.py: This file defines all the modules and submodules of the Gmsh library as noted above, and adds each method with its input and output (handled by special objects), along with its documentation to an API object. This object then writes the API files for all the languages through methods like write_julia(), write_c(), etc. This API object is defined in the next file:
  2. GenApi.py: This is where the bindings are generated. The Module object is defined, which holds the name, documentation and a list of submodules, and provides a method to add (sub)modules to it. The API object provides methods to add modules, write to files, and write language-specific APIs. It also defines the arg object that handles the nature of the input and output arguments, providing apparatus for various languages.

In morpho-gmsh, we obtain gen.py from the stable version we desire, and provide our own GenApi.py that writes only the Morpho API. To use gen.py without modification, our write_python() does the job of producing the Morpho API, while the rest of them (write_julia(), etc) just pass.

Now, let's take a deep dive into these two files and see how we can use them to generate the Morpho API.

gen.py

This file, under gmsh/api/gen.py, has the following structure:

// gen.py
from GenApi import * # Contains all the relevant objects and methods
...
api = API(version_major, version_minor, version_patch)

gmsh = api.add_module('gmsh', 'top-level functions')

doc = '''Initialize the Gmsh API ... ''' 
gmsh.add('initialize', doc, None, iargcargv(), ibool('readConfigFiles', 'true', 'True', 'true'), ibool('run', 'false', 'False'))

doc = '''Return 1 if the Gmsh API is initialized, and 0 if not.'''
gmsh.add('isInitialized', doc, oint)
...
option = gmsh.add_module('option', 'option handling functions')

doc = ''' ... '''
option.add(...)
...
model = gmsh.add_module('model', ...)
...
model.add('add', doc, None, istring('name'))
...
mesh = model.add_module('mesh', 'mesh functions') # Submodule of 'model'
...
api.write_c()
api.write_python()
...

Outline of gmsh/api/gen.py

The top level API object collects all the modules and submodules and writes them to various target languages. Its add_module method creates and returns a Module object, which has its own add_module method. The add method of the Module object takes in the name of the method, its documentation, the return type, and the input and output arguments. The I/O arguments are passed as objects of the arg class. Note that these are target-language agnostic (aside from providing default values for some languages), and the separate write methods of the API object handle the language-specific details. This allows gen.py to remain the same for all languages, and only the GenApi.py file needs to be modified for each language.

In light of this, we will aim to reuse the original gen.py as is, and only modify GenApi.py for our purposes.

GenApi.py

The language-agnostic nature of gen.py, while allowing itself to be clean and simple, transfers the complexity of the language-specific details to GenApi.py.

The original gmsh source code has a single GenApi.py file for all the target languages --- the arg objects have attributes for all the languages, and the API object has methods to write for each of them.

To see this, let's look at an easy example function, gmshModelSetCurrent, which sets the name of the current model --- this function has one input, a char * array, and no output. Here is how it is added to the API in gen.py:

doc = '''Set the current model to the model with name `name'. If several models have the same name, select the one that was added first.'''
model.add('setCurrent', doc, None, istring('name'))

From gmsh/api/gen.py

Here is what GenApi.py's write_c method writes in the C header file for this function:

/* Set the current model to the model with name `name'. If several models have
 * the same name, select the one that was added first. */
GMSH_API void gmshModelSetCurrent(const char * name,
                                  int * ierr);

From gmsh/api/gmshc.h

and here are the relevant snippets from GenApi.py that generate this code:

class arg:
    def __init__(self, name, value, python_value, julia_value, cpp_type,
                 c_type, out):
    ...
    # Note that self.c is generates the argument for the C function signature
    self.c = c_type + " " + name
    ...
...

def istring(name, value=None, python_value=None, julia_value=None):
    # Note that the arg is initialized with c_type = "const char *"
    a = arg(name, value, python_value, julia_value, "const std::string &",
            "const char *", False)
    a.python_arg = "c_char_p(" + name + ".encode())"
    ...
    a.texi_type = "string"
    return a

...
class API:
    ...
    def write_c(self):
        ...
        def write_module(module, c_namespace, cpp_namespace, indent):
            ...
            # Note that arg.c is used here to write the C function signature
            self.fwrite(f, 
              fnameapi + (",\n" + ' ' * len(fnameapi)).join(
                list((a.c for a in args + (oint("ierr"), )))) + ");\n")
        ...
    ...
...

Snippets from gmsh/api/GenApi.py

Let's start with the istring object. This is an object of the arg class. Note that in the initializer, the c_type passed is "const char *". The arg initializer in turn uses this to set its attribute c to c_type + " " + name. This is used in the write_c method of the API object to write the call signature in the C header file. This is how the C function signature is written to the header file.

This procedure is followed for all input and output types. For example, there is an object iint for an input int, ivectorsize for a vector of size_t's, etc. and similar for outputs, ostring for an output char * and so on. These objects have attributes that specify, among other things, the call signatures for various languages. For instance, if a function takes in an input vector of integers, then the C function needs two inputs, (int * list, size_t list_n), where list_n encodes the size of the list. The same goes for outputs.

We write our own GenApi.py that only writes the Morpho API, heavily adopting from the original. We will see how to do this in the next chapter.

API auto-generation

Now that we understand how the original Gmsh API is generated, let's work towards creating Morpho bindings for it. Since our Morpho API is going to be wrapping the C API functions around a Morpho veneer (as Morpho is written in C), our GenApi.py script will auto-generate the veneers for the C functions. (Since Morpho does not have a native Foreign Function Interface (FFI) yet, we will have to write the veneers in C. Although, the machinery we build here could be useful for building a future FFI for Morpho.) In addition, we also want inline documentation for the Morpho API, which should also be auto-generated from the original Gmsh API documentation.

Hence, our GenApi.py script will have to do the following tasks:

  1. Generate the Morpho veneers for the C functions.
  2. Create the header file with the function names and error names for the Morpho API.
  3. Generate the inline documentation for the Morpho API.

In this chapter, we will first sketch out a simple example of how a veneer extension can be written for Morpho. Then we will discuss how we can auto-generate the veneers and the inline documentation for the Morpho using our GenApi.py script.

Skeleton of a Morpho wrapper extension

To see how a simple wrapper extension can be written, let's assume we want to wrap a C function integer_add that adds two integers and returns the output as an integer. Here is how we would write the wrapper in C:

C file

// intadd.c
#include <stdio.h>
#include <morpho/morpho.h>
#include <morpho/builtin.h>
#include "intadd.h"

// The C function we wish to wrap. This could be in a saparate file, in which case we would include its header file here, but we are keeping it simple for now.
int integer_add(int a, int b) {
    return a + b;
}

// The Morpho veneer function
value MorphoIntegerAdd(vm *v, int nargs, value *args) {
    // Check the number of arguments
    if (nargs != 2) {
        morpho_runtimeerror(v, INTADD_NARGS_ERROR);
        return MORPHO_NIL;
    }
    // Check the types of the arguments
    if (!MORPHO_ISINTEGER(args[0])) {
        morpho_runtimeerror(v, INTADD_TYPE_ERROR);
        return MORPHO_NIL;
    }
    if (!MORPHO_ISINTEGER(args[1])) {
        morpho_runtimeerror(v, INTADD_TYPE_ERROR);
        return MORPHO_NIL;
    }
    // Call the C function and return the result
    int a = MORPHO_GETINTEGERVALUE(args[0]);
    int b = MORPHO_GETINTEGERVALUE(args[1]);
    return MORPHO_INTEGER(integer_add(a, b));
}

void myextension_initialize(void) {
    builtin_addfunction(MORPHO_INTEGERADD_FUNCTION, MorphoIntegerAdd, BUILTIN_FLAGSEMPTY);
    morpho_defineerror(INTADD_NARGS_ERROR, ERROR_HALT, INTADD_NARGS_ERROR_MSG);
    morpho_defineerror(INTADD_ARGS_ERROR, ERROR_HALT, INTADD_ARGS_ERROR_MSG);
}

void myextension_finalize(void) {
    // Nothing to do here
}

The value object is the basic Morpho object that can hold any type of value like Lists, integers, floats, Strings, etc., even including nil. Every Morpho veneer function returns a Morpho value object. Further, nargs is the number of arguments supplied, and args is a list of Morpho value's. We can see that we have performed type checking on the arguments and raised errors if the wrong number or type of arguments are supplied. We have also captured the arguments into C variables and called the C function. Finally, we have returned the result as a Morpho value object.

All Morpho extensions must provide an initialize function, named EXTENSIONNAME_initialize. In this function, the morpho API is used to define functions and/or classes implemented, and set up any global data as necessary. Since we will be wrapping functions, our extension does not have any classes. Here, we add a function to the runtime that will be visible to user code as MORPHO_INTEGERADD_FUNCTION. This is defined as a macro instead of hardcoding the function name to make it easier to change the function name in the future or use it elsewhere. The last argument to builtin_addfunction is a flag that tells the morpho runtime whether the function is a method or a standalone function. Since this is a standalone function, we pass BUILTIN_FLAGSEMPTY. Similarly, we define errors that will be raised in the veneer function, whose name and message are also macros.

The finalize function has a similar naming convention, and is not strictly necessary (as here). This function should deallocate or close anything created by your extension that isn’t visible to the morpho runtime: we will actually use it here to finalize gmsh itself.

The macros are defined in our extension's header file intadd.h:

Header File

// intadd.h
#include <stdio.h>
#include <morpho/morpho.h>
#include <morpho/builtin.h>

#define INTADD_NARGS_ERROR "IntAddNArgsError"
#define INTADD_NARGS_ERROR_MSG "Wrong number of arguments supplied to integer_add"

#define INTADD_TYPE_ERROR "IntAddTypeError"
#define INTADD_TYPE_ERROR_MSG "integer_add requires two integer arguments"

#define MORPHO_INTEGERADD_FUNCTION "addInts"

Note that from the last line, the function is given the name addInts, so it would be called that in Morpho:

var a = 3;
var b = 4;
var c = addInts(a, b);

However, there is nothing stopping us from calling it integer_add itself. Having

#define MORPHO_INTEGERADD_FUNCTION "integer_add"

would make the last line

var c = integer_add(a, b);

This is in fact what we will do for our Gmsh extension. We will call the functions in the C API by the same names as in the original Gmsh API. This will make it easier for users to use the Morpho APIs without having new names.

Our extension is almost ready, save for documenting the function. This is done in Morpho using the Markdown format in the share/help directory. We will see how to do this in the next chapter. For our simple extension, it would look like this:

Help file

[comment] # Path: share/help/intadd.md
[comment]: # (Intadd help)
[version]: # (0.1)

# Intadd

[tagIntadd]: # "Intadd"

The `intadd` extension provides a method to add two integers.

[showsubtopics]: # "subtopics"

## addInts

[tagaddints]: # "AddInts"

The `addInts` function adds two integers and returns the result.

    var c = addInts(1, 2)
    print c // 3

This makes it so that the user can access the help for this module in the REPL using

>help intadd // or
>help intadd.addInts // etc

This is how we will document our Gmsh extension as well. For every gmsh function, we need the veneer, the initialization, the header file declaration and the documentation. We will see how to do this in the next chapter.

The arg class

In the simple example in the last chapter, we saw the processing of the input and output arguments that we needed to do to create the veneer. In the Gmsh API generator, all arguments are initialized as the arg class or one of its subclasses. We simplify this object to strip off most of the boilerplate code for the other language APIs and add in attributes for the Morpho API. Our arg class has the following structure:

# Removing all the Python, Julia and Fortran related attributes, but
# keeping the function call signatures the same in order to use gen.py
# without editing.
class arg:
    """ Basic datatype of an argument. Every datatype inherits this constructor
    and also default behaviour from here.

    Compared to the original, this object only stores the C-related information (to actually call the C-API functions) and Morpho-specific information.

    Subclasses of this constructor will have methods for processing inputs/outputs, etc.

    """
    def __init__(self, name, value, python_value, julia_value, cpp_type,
                 c_type, out):
        self.c_type = c_type
        self.name = name
        if (name == "value"): # value is a builtin type in Morpho, so change it slightly
            self.name = "cvalue"
            name = "cvalue"
        if (name == "v"): # Similarly, v is used for the virtual machine (vm) in Morpho
            self.name = "cv"
            name = "cv"
        self.value = value
        self.out = out # Boolean to indicate if the argument is an output
        self.c = c_type + " " + name
        self.texi_type = ""
        self.texi = name + ""
        # self.morpho will be used in place of self.c while calling the C-API functions, since the inputs will be slightly different while calling from the Morpho wrappers.
        self.morpho = name
        # self.morpho_object specifies how to name the Morpho Objects corresponding to the args. For instance, a string would have morpho_object as name + "_str", so that it is declared as `objectstring * name_str = ...`
        self.morpho_object = name

As noted in the comments in the code, the self.morpho attribute will be used in place of self.c while calling the C-API functions, since the inputs will be slightly different while calling from the Morpho wrappers. The self.morpho_object attribute specifies how to name the Morpho Objects corresponding to the args. For instance, a string would have morpho_object as name + "_str", so that it is declared as objectstring * name_str = ....

The original GenApi.py file creates all the objects as instances of the arg class, like so:

# Original GenApi.py
def iint(name, value=None, python_value=None, julia_value=None):
    a = arg(name, value, python_value, julia_value, "const int", "const int",
            False)
    a.python_arg = "c_int(" + name + ")"
    a.julia_ctype = "Cint"
    a.fortran_types = ["integer, intent(in)"]
    a.fortran_c_api = ["integer(c_int), value, intent(in)"]
    a.fortran_call = f"{name}=optval_c_int({value}, {name})" if value is not None else f"{name}=int({name}, c_int)"
    a.texi_type = "integer"
    return a

In contrast, we will create all other arguments as subclasses or sub-subclasses of this arg object. We will see in the next chapters that our way allows us to write additional methods to the subclasses to help write the Morpho API.

Capturing Morpho inputs

To see how we handle inputs, let's continue with our example of the gmshModelSetCurrent function that we looked at in genApi.py. This function takes in a char array containing the desired name for the current model and sets it internally, returning nothing. Hence, this function has one String input and no outputs. Below is its declaration in the C-API, gmshc.h:

/* Set the current model to the model with name `name'. If several models have
 * the same name, select the one that was added first. */
GMSH_API void gmshModelSetCurrent(const char * name,
                                  int * ierr);

gmsh/api/gmshc.h

(Note: the last argument for all the C-API functions is a reference to an integer. This stores the error code.) Here is how it is added to the API from gen.py:

doc = '''Set the current model to the model with name `name'. If several models have the same name, select the one that was added first.'''
model.add('setCurrent', doc, None, istring('name'))

gmsh/api/gen.py

In Morpho, we want a call signature like

gmshModelSetCurrent(name)

To execute this, we need to do a few things:

  1. Check that the number of arguments passed to the Morpho gmshModelSetCurrent method is 1. Otherwise, raise an error.
  2. Then check that this argument is indeed a Morpho String. Otherwise, raise an error.
  3. If all is well, then capture the input into a C-string, or a char array.
  4. Only then can we actually call the C-function.
  5. Return nil since this function doesn't output anything.

Phew, that's a bunch! But we are going to automate this process.

Based on the list above, we want this method to be wrapped in Morpho something like this:

// myextension.c
#include <stdio.h>
#include <morpho/morpho.h>
#include <morpho/builtin.h>
// Let's not forget to include the gmsh C API header file!
#include <gmshc.h>
// Let's include our own header file
#include "gmshapi.h"

value MorphoGmshModelSetCurrent(vm *v, int nargs, value *args) {
    // Check whether only 1 argument is supplied and raise error if not
    if (nargs != 1) {
        morpho_runtimeerror(v, GMSH_NARGS_ERROR);
        return MORPHO_NIL;
    } 
    // Check whether the argument is a Morpho String and raise error if not
    if (!MORPHO_ISSTRING(MORPHO_GETARG(args, 0))) {
        morpho_runtimeerror(v, GMSH_ARGS_ERROR); 
        return MORPHO_NIL; 
    }
    // If we don't raise error up to this point, we have the right number and kind of input. Capture the input Morpho string into a char array.
    const char * name = MORPHO_GETCSTRING(MORPHO_GETARG(args, 0)); 
    // Actually call the C function
    int ierr;
    gmshModelSetCurrent(name,
                      &ierr);
    // Since there is no output, we return nil
    return MORPHO_NIL;
}

A wrapper for gmshMorphoSetCurrent

Let's see how we can automate this process. To check the number of arguments, we note that the arguments are collected by the Module object as a list:

class Module:
    def __init__(self, name, doc):
        self.name = name
        self.doc = doc
        self.fs = []
        self.submodules = []

    def add(self, name, doc, rtype, *args):
        self.fs.append((rtype, name, args, doc, []))

From gmsh/api/GenApi.py

Further, all the output arguments have the out property set to True (this is the last argument to the arg and its subclasses' initializer). We can use this to grab and sort the arguments:

def write_morpho(self):
    ...
    def write_module(module, c_namespace):
        ...
        for rtype, name, args, doc, special in module.fs:

            iargs = list(a for a in args if not a.out)
            oargs = list(a for a in args if a.out)

From gmsh/api/GenApi.py

Thus, the code for the number of arguments check is as simple as this:

nargsCheck =  INDENTL1 + f"if (nargs != {len(iargs)})"+ " {\n" \
            + INDENTL2 + "morpho_runtimeerror(v, GMSH_NARGS_ERROR);\n" \
            + INDENTL2 + "return MORPHO_NIL;\n" \
            + INDENTL1 + "} \n"

Here, the GMSH_NARGS_ERROR will be defined in the gmshapi.h header file, together with the error message GMSH_NARGS_ERROR_MSG like so:

// morpho-gmsh/src/gmshapi.h
#define GMSH_NARGS_ERROR "GmshNargsErr"
#define GMSH_NARGS_ERROR_MSG "Incorrect Number of arguments for Gmsh function. Check the help for this function."

The error itself will be initialized in the gmshapi.c file in the gmshapi_initialize function as follows:

// morpho-gmsh/src/gmshapi.c
morpho_defineerror(GMSH_NARGS_ERROR, ERROR_HALT, GMSH_NARGS_ERROR_MSG);

Now, for each input argument, we need to (a) check its type and (b) capture it into a C variable. We will do this by endowing all input arguments with a capture_input method that will return the C code to do this. To do this, we first make a subclass of arg called inputArg which defines this method:

# morpho-gmsh/api/GenApi.py
class inputArg(arg):
    """
    Basic datatype for an input argument, inherited from `arg`. Provides some extra attributes and methods to process the input.
    """
    def __init__(self, name, value, python_value, julia_value, cpp_type, c_type, out):
        super().__init__(name, value, python_value, julia_value, cpp_type, c_type, out)

        # C-code to generate specific runtime error if the arguments are not correct. To-do: Currently, all inputs return the same error. Change this so that the error is function-specific or more helpful in general.
        self.runTimeErr = " {morpho_runtimeerror(v, GMSH_ARGS_ERROR); return MORPHO_NIL; }\n"
        # morphoTypeChecker is the builtin (or otherwise) Morpho function to check whether the input value is of the correct type.
        self.morphoTypeCheker = "MORPHO_ISINTEGER" # For default, using the integer case
        # morphoToCConverter is the builtin (or otherwise) Morpho function to grab the correct C-type from the input Morpho value.
        self.morphoToCConverter = "MORPHO_GETINTEGERVALUE"


    def capture_input(self, i):
        """
        capture_input(i)

        Generates C-code to check the (i+1)th input argument to the Morpho function and convert it to an appropriate C-type.
        """
        
        # Here, we check for a single object as defualt, which can be reused for anything that's not a list: `iint`, `idouble, `istring`, etc.
        chk = INDENTL1 + f"if (!{self.morphoTypeCheker}(MORPHO_GETARG(args, {i}))) " + self.runTimeErr
        inp = INDENTL1 + f"{self.c_type} {self.name} = {self.morphoToCConverter}(MORPHO_GETARG(args, {i})); \n"
        return chk + inp

We see that in addition to the attributes of arg, we add two more: morphoTypeChecker and morphoToCConverter. These are the names of the Morpho functions that will check the type of the input and convert it to a C-type, respectively. The capture_input method then uses these to generate the C-code to do this.

While the default values for these are for an integer, we can override these in the subclasses. For example, the istring object is initialized as follows:

# morpho-gmsh/api/GenApi.py
class istring(inputArg):
    def __init__(self, name, value=None, python_value=None, julia_value=None, cpp_type="const std::string &", c_type="const char *", out=False):
        super().__init__(name, value, python_value, julia_value, cpp_type, c_type, out)
        self.texi_type = "string"
        self.morphoTypeCheker = "MORPHO_ISSTRING"
        self.morphoToCConverter = "MORPHO_GETCSTRING"

More complex objects, like ivectorint (which captures a list of integers), will have a more complex capture_input method. This allows us just process all the inputs simply as:

# morpho-gmsh/api/GenApi.py
# Inside the `write_module` method

# Capture all the inputs
for i,iarg in enumerate(iargs):
    self.fwrite(f, iarg.capture_input(i))

In the next chapter, we will look at how we handle output arguments.

Capturing and returning the C outputs

Handling outputs is going to be a little more involved than the inputs. We need to do three things:

  1. Initialize pointers to the outputs so that they can be passed to the gmsh C functions.
  2. Capture the outputs from the C functions into a Morpho value format.
  3. Return the captured outputs.

Similar to the input, we create an outputArg object, inheriting from arg, that defines methods for each of these operations:

# GenApi.py
class outputArg(arg):
    """
    Basic datatype for an output argument, inherited from `arg`.
    Defines some extra methods to process the output.
    """

    # Initialize pointers to the outputs so that they can be passed to the gmsh C functions.
    def init_output(self):
        return str("")

    # Capture the outputs from the C functions into a Morpho value format.
    def capture_output(self):
        return str("")

    # Return the captured outputs.
    def return_output(self):
        return str("")

The specific output types then inherit from outputArg and override the corresponding methods.

We will again choose a simple function to illustrate this, albeit with a composite return object: a list of integers:

/* Get the list of all fields. */
GMSH_API void gmshModelMeshFieldList(int ** tags, size_t * tags_n,
                                     int * ierr);

Here, we have no inputs and a single output tags that's a list of integers. Here is how it is added to the API from gen.py:

# gen.py
doc = '''Get the list of all fields.'''
field.add('list', doc, None, ovectorint('tags'))

The oarg class is initialized with certain handy attributes to help with the output processing. In addition to the ones we have seen before, we add a new self.cToMorphoConverter attribute to help convert the C outputs to Morpho outputs. E.g.:

# GenApi.py
class ovectorint(oint):
    def __init__(self, name, value=None, python_value=None, julia_value=None):
        arg.__init__(self, name, value, python_value, julia_value, "std::vector<int> &",
                "int **", True)
        self.c = "int ** " + self.name + ", size_t * " + self.name + "_n"
        self.morpho = "&" + self.name + ", &" + self.name + "_n"
        self.texi_type = "vector of integers"
        self.morpho_object = self.name + "_list"
        self.elementType = "int"
        self.cToMorphoConverter = "MORPHO_INTEGER"

The init_output method initializes pointers to the outputs so that they can be passed to the gmsh C functions:

# GenApi.py, class ovectorint
    def init_output(self):
        return INDENTL1 \
                + f"{self.elementType}* {self.name};\n" \
                + INDENTL1 \
                + f"size_t {self.name}_n;\n"

The above will generate the C code:

    int* tags;
    size_t tags_n;

The capture_output method captures the outputs from the C functions into a Morpho value format:

# GenApi.py, class ovectorint
    def capture_output(self):
        return INDENTL1 + f"value {self.name}_value[(int) {self.name}_n];\n" \
            + INDENTL1 + f"for (size_t j=0; j<{self.name}_n; j++) " +"{ \n" \
            + INDENTL2 + f"{self.name}_value[j] = {self.cToMorphoConverter}({self.name}[j]);\n" \
            + INDENTL1 + "}\n" \
            + INDENTL1 + f"objectlist* {self.morpho_object} = object_newlist((int) {self.name}_n, {self.name}_value);\n"

The above will generate the C code:

    value tags_value[(int) tags_n];
    for (size_t j=0; j<tags_n; j++) {
        tags_value[j] = MORPHO_INTEGER(tags[j]);
    }
    objectlist* tags_list = object_newlist((int) tags_n, tags_value);

The return_output method returns the captured outputs:

# GenApi.py, class ovectorint
    def return_output(self):
        return INDENTL1 + "value out;\n" \
            + INDENTL1 + f"if ({self.name}_list) " + "{\n" \
            + INDENTL2 + f"out = MORPHO_OBJECT({self.name}_list);\n" \
            + INDENTL2 + f"morpho_bindobjects(v, 1, &out);\n" \
            + INDENTL1 + "}\n" \
            + INDENTL1 + "return out;\n"

which generates

    value out;
    if (tags_list) {
        out = MORPHO_OBJECT(tags_list);
        morpho_bindobjects(v, 1, &out);
    }
    return out;

Note: A few Gmsh C functions have a non-void return type, and hence return >something directly instead of through pointers. For example,

/* Return 1 if the Gmsh API is initialized, and 0 if not. */
GMSH_API int gmshIsInitialized(int * ierr);

These are handled directly while writing the module.

The handling of cases where the function returns outputs both directly and via pointers is not implemented yet. But as of 4.12.2 (at the time of writing), there is only one function, gmshFltkSelectEntities, in the entire API that does this, and since it is an FLTK-related function, we will probably not use it through the API in Morpho. This should still be implemented in the future.

Here are the Morpho values types returned for the corresponding C types:

C-TypeMorpho-Type
intMORPHO_INTEGER
size_tMORPHO_INTEGER
doubleMORPHO_FLOAT
char *MORPHO_STRING

All vector outputs are returned as Morpho List's. For example, an ovectordouble would be returned as a Morpho List of Morpho Floats, ovectorvectorint would be List of List of MORPHO_INTEGER's, and so on. For the list of lists, we need a separate operation to collect all the outputs and return it. We write functions for this inside our write_morpho method:

def collect_list_of_outputs(oargs):
    l = INDENTL1 + f"value outval[{len(oargs)}];\n"
    for j, oarg in enumerate(oargs):
        l += INDENTL1 + f"outval[{j}] = MORPHO_OBJECT({oarg.morpho_object});\n"
    l += INDENTL1 + f"objectlist* outlist = object_newlist({len(oargs)}, outval);\n"
    self.fwrite(f, l)
    return

def return_list_of_outputs():
    l = INDENTL1 + "value out;\n" \
      + INDENTL1 + f"if (outlist) " + "{\n" \
      + INDENTL2 + f"out = MORPHO_OBJECT(outlist);\n" \
      + INDENTL2 + f"morpho_bindobjects(v, 1, &out);\n" \
      + INDENTL1 + "}\n" \
      + INDENTL1 + "return out;\n"
    self.fwrite(f, l)

Auto-generating inline documentation

Morpho documentation is written in the Markdown format (see the first section of this chapter). The gen.py file already provides doc-strings for all the functions. In addition to writing them to our help file, we also write the call-signature and I/O parameters to the help file. This clarifies the order of the inputs since this information is not provided in the doc-strings.

Here is a sample output:

## gmshWrite

[taggmshWrite]: # "gmshWrite"

Call signature:
gmshWrite(fileName)

Arguments:
fileName (string)

Returns:
nil

Write a file. The export format is determined by the file extension.

Writing it all to the files

Now, we will see how the write_module method of the write_morpho method (actually called write_python to use gen.py as is) writes all the functions to the .c, .h and .md files.

The full structure, although long, is pretty straightforward:

# GenApi.py
class API:
    ...
    def write_python(self):
        """
        This method is actually `write_morpho`, but is named `write_python` so that
        we can run `gen.py` directly without modification.
        """

        method_names = [] # List to collect method names

        def collect_list_of_outputs(oargs):
            """
            Collect outputs in a Morpho list
            """

        def return_list_of_outputs():
            """
            Return the list of outputs
            """
        ...
        def write_module(module, c_namespace):

            # Collect all the inputs
            iargs = ...
            oargs = ...

            # Colelct all the outputs
            #
            # Create the Morpho function name
            # Write help for this function to the file
            fnamemorpho = ...
            method_names.append(fnamemorpho)
            # Write function definition to the C-file
            # Write checks for number and type of input arguments

            # Capture all the inputs
            for i,iarg in enumerate(iargs):
                self.fwrite(f, iarg.capture_input(i))

            # Initialize the outputs
            for i,oarg in enumerate(oargs):
                self.fwrite(f, oarg.init_output())

            # Create the C function call
            # Write the function call, accounting for direct return values if any.

            # Capture and return output
            # Write the return statement
            if len(oargs)==0 and not rtype: ## If there's nothing to return, return MORPHO_NIL
                self.fwrite(f, INDENTL1 + "return MORPHO_NIL;\n")
            elif len(oargs)==0 and rtype: ## If there are no outputs, but the function returns something, return the value
                self.fwrite(f, INDENTL1 + f"return {rtype.cToMorphoConverter}(outval);\n")
            for oarg in oargs: ## If there are outputs, we need to first capture them as Morpho values.
                self.fwrite(f, oarg.capture_output())
            if (len(oargs)==1): ## If there's only one output, return it
                self.fwrite(f, oargs[0].return_output())
            elif (len(oargs)>1): ## If there are multiple outputs, collect them in a Morpho List and return it
                collect_list_of_outputs(oargs)
                return_list_of_outputs()

        # Recursively write all the submodules
        for m in module.submodules:
            write_module(m, c_namespace)


    # We will first write to the C and MD files simultaneously, and collect the function names, etc for writing the Header file
    # Simultanously open the C file and the help file as we need to add the methods one by one
    with open("../src/" + f"{EXTENSION_NAME}.c", "w") as f, \
         open("../share/help/" + f"{EXTENSION_NAME}.md", "w") as hlp:

         # Write header-material to the files

         # Write all the modules

         # Write the footer for the C file
         # Write the initialize method
         # Add all the functions
         for method in method_names:
             self.fwrite(f, INDENTL1 + f"builtin_addfunction({method.upper()}_FUNCTION, {method}, BUILTIN_FLAGSEMPTY);\n")
         # Add the error definitions.
         self.fwrite(f, cmorpho_error_def)
         self.fwrite(f, "\n}\n")
         # Write the finalize method
         self.fwrite(f, cmorpho_footer)

         # Now, write the header file
         with open("../src/" + f"{EXTENSION_NAME}.h", "w") as fh:
             # Write the header-material for the header file
             # This currently also defines the Errors.
             for method in method_names:
                 # Define all the function names

        # And that's it!

Implementation of the gmsh module in Morpho

This section is a work in progress (see the to-do's).

Some To-do's

Here are some desirable features not implemented yet.

The gmshapi module

  1. The ivoidstar (pointer) input needs to be handled. This occurs only in one function as of version 4.12.2, the gmshModelOccImportShapesNativePointer method. As this is not implemented yet, we skip this method in the API.
  2. Same as above, but for the isizefun input (a function input). This also occurs for one function, gmshModelMeshSetSizeCallback, that we skip currently.
  3. The ivoidstar and isizefun need to be implemented also as subclasses of inputArg, so that we can skip any new function that has this input by looking for the input's class name. (or just finish 1. and 2.!)
  4. Handle functions which return both directly (an int, for example, in gmshIsInitialized) and via pointers. Currently, we skip them (again, only one function in the FLTK module, so doesn't affect our use case all that much right now).
  5. Add more specific Error's. Currently, there is a single error for incorrect type of arguments, so it is not very helpful.
  6. Currently, all the methods fall under gmshapi's subtopics, whereas it would be nice if the help is also organized by the submodules. We need to add this functionality.

The gmsh module

The gmsh module needs to be made in a "Morphonic" way, in the style of Morpho's own meshgen module. This requires a considerable thought on the design, and remains to be implemented. Currently, the gmsh module simply provides one (pretty central) functionality: converting gmsh meshes to Morpho meshes!