libSBML C++ API  5.18.0
Step by step summary of the implementation of an extension

This section describes the steps necessary to implement a package extension (also known as a package plugin) for libSBML by hand. The purpose of this section is to document the process. In practice, the libSBML developers no longer write package extensions by hand, but instead, use a code generation system under development by the libSBML Team (a system called DEVISER, short for Design Explorer and Viewer for Iterative SBML Enhancement of Representations). This code generator embodies the steps below.

1. Create the directory structure

The conventional structure for a libSBML package extension involves two main directories, one for the implementation sources and one for example code. The following illustrates this structure:

package_root
│
├── examples
│   ├── c
│   ├── c++
│   ├── csharp
│   ├── java
│   ├── javascript
│   ├── perl
│   ├── python
│   ├── r
│   └── ruby
│
└── src
    ├── bindings
    │   ├── csharp
    │   ├── java
    │   ├── javascript
    │   ├── perl
    │   ├── python
    │   ├── r
    │   ├── ruby
    │   └── swig
    │
    └── sbml
        └── packages
            │
            └── package_name
                │
                ├── common
                │   └── test
                ├── extension
                │   └── test
                ├── sbml
                │   └── test
                ├── util
                │   └── test
                └── validator
                    └── test

The top-level directory, package_root, can be named anything, although by convention it's named after the SBML Level 3 package being implemented. The directory package_name should be the nickname or short-form name of the package (e.g., "comp" for the Hierarchical Model Composition package).

The directory structure for the implementation of the package (the tree root at src) is separated into two main branches: bindings for the programming language interfaces (known as the language bindings in libSBML-speak) generated with the help of SWIG, and sbml for actual package code. The subdirectories under package_name have the following purposes:

  • common: This contains header files that provide forward declarations for all the object classes defined by the package, plus any other common headers and/or code.
  • extension: This contains the code for the all of the extensions of libSBML classes defined by the package. For example, if a package extends the Model object, this is where the extension class code is placed.
  • sbml: This contains all the new SBML object classes defined by the package.
  • util: As its name implies, this contains any utility classes or other code that a package might need in its implementation.
  • validator: This contains code for SBML validation of the package constructs.

Examples of real package extensions employing the directory structure described above can be found in the libSBML source distribution, in the src/sbml/packages subdirectory.

2. Derive classes from SBasePlugin

Certain classes in an SBML Level 3 package will be subclasses of existing core SBML constructs, and other classes will be entirely ones introduced by the package. Existing SBML classes can be extended through the use of the special-purpose classes SBasePlugin and SBMLDocumentPlugin, while new classes are implemented as objects derived from the basic SBase class. We discuss SBasePlugin in this section, SBMLDocumentPlugin in the next section, and the implementation of entirely new classes in the section after that.

The SBasePlugin class is libSBML's base class for extensions of core SBML component objects. It defines basic virtual methods for reading/writing/checking additional attributes and/or subobjects; and these methods should be overridden by subclasses to implement the necessary features of an extended SBML object. The documentation for the SBasePlugin class describes the steps necessary to use it to extend existing object classes. Here we summarize the steps:

  1. Identify the SBML objects that need to be extended. Any existing SBML object that gets new attributes or subobjects falls into this category. For instance, a package may need to extend Model to add new list of entities (as the SBML Level 3 Layout package does with ListOfLayouts).

  2. Create a SBasePlugin subclass for each extended SBML object class. This involves several substeps, summarized briefly here:

    1. Define protected data members and implement their initialization as well as their handling by assignment operators, copy operators, and other necessary operators or fields.

    2. Override SBasePlugin class-related methods such as the class constructor, copy constructor, and others.

    3. Override SBasePlugin virtual methods for attributes on the SBML object.

    4. Override SBasePlugin virtual methods for subobjects, if the object class contains other classes of objects.

    5. Override SBasePlugin virtual methods for XML namespaces, if the SBML package uses extra XML namespaces beyond the default SBML namespace.

    6. Implement additional methods as needed.

A subclass of SBasePlugin, SBMLDocumentPlugin, is designed specifically for extending the top-level SBML container object. It should be used instead of SBasePlugin to add attributes or subobjects to the <sbml> XML element, if an SBML package calls for it. The next section discusses this further.

In the directory structure for packages (1. Create the directory structure), the code for extensions of existing libSBML classes is placed in the subdirectory named extension.

3. Derive a class from SBMLDocumentPlugin if necessary

SBML Level 3 packages typically do not make any changes to the top-level <sbml> XML element beyond adding an attribute named "required", so in most cases, the extension of SBMLDocument is very simple. Here follows a summary of the basic steps; the documentation for the SBMLDocumentPlugin class explains the steps in more detail.

  1. Identify the changes necessary to SBMLDocument.

  2. Create the SBMLDocumentPlugin subclass. This involves several substeps:

    1. Override class-related methods derived from SBasePlugin, the parent class of SBMLDocumentPlugin.

    2. Determine the necessary value of the "required" attribute and set up the class implementation to set/test/manage the value accordingly.

    3. Define protected data members, if the package definition requires any.

    4. Override virtual methods for new attributes, if any.

    5. Override virtual methods for subobjects, if the package defines any new objects under the top-level <sbml> XML element.

    6. Override virtual methods for XML namespaces.

    7. Implement additional methods as needed.

In the directory structure for packages (Section 1. Create the directory structure), the code using SBMLDocumentPlugin is placed in the subdirectory named extension, grouped together with the classes derived from SBasePlugin.

4. Derive classes from SBase

As mentioned above, some classes in SBML Level 3 package specifications typically are defined as subclasses of existing core SBML objects such as Model, Reaction, Parameter, and so forth, while others are defined as entirely new classes. If the package specification calls for new SBML objects derived from SBML's abstract SBase class, the libSBML package extension should implemented them by deriving them from the libSBML SBase class.

The following are the basic steps to subclassing SBase for the implementation of each new SBML object class:

  1. Identify the attributes of the class. Objects will have attributes, subobjects (usually either for a mathematical formula or a list of subobjects inside a ListOf), or a mixture of both. When an object has attributes, each attribute will have a data type; this data type will be either one of the core SBML primitive types or a new type defined in the specification for the package. The first step is to gather the requirements for these attributes.

  2. Identify the subobjects of each object class. Another necessary requirements analysis step is determining if any subobjects need to be defined and contained withing other objects. Any that are subclassed from SBase will be their own separate class, and will be contained inside another class derived from ListOf. (For example, ListOfLayouts in the SBML Level 3 Layout package.)

  3. Write the implementation of each class.

    1. Define protected data members that store identified subobjects and/or attributes. For example, the the Groups class in the Groups package needs protected data members for the object identifier ("id") attribute, object name ("name") attribute, a list of Member objects (stored in a ListOfMembers class object derived from ListOf), and more.

    2. Define the following two class constructors: (i) a constructor that accepts three arguments for the SBML Level, SBML Version, and package version; and (ii) a constructor that accepts an object derived from SBMLNamespaces. Make sure that the implementation of the constructor calls the parent's constructor, and that the default SBML Level, Version and package version are given as default arguments. The implementation also has to call the method setSBMLNamespacesAndOwn() with the package-specific SBMLExtensionNamespaces object, as well as the method connectToChild(). Here is an example from the Groups package:

      Group::Group (unsigned int level, unsigned int version, unsigned int pkgVersion)
      : SBase (level, version)
      ,mId("")
      ,mName("")
      ,mMembers(level, version, pkgVersion)
      ,mMemberConstraints (level, version, pkgVersion)
      {
      // set an SBMLNamespaces derived object of this package
      setSBMLNamespacesAndOwn(new GroupsPkgNamespaces(level, version, pkgVersion));
      // connect to child objects
      connectToChild();
      }

    3. Define a constructor that accepts an SBMLNamespaces object. This constructor needs to call three methods, as illustrated in the body of this example from the Groups package:

      Group::Group(GroupsPkgNamespaces* groupsns)
      : SBase(groupsns)
      ,mId("")
      ,mName("")
      ,mMembers(groupsns)
      ,mMemberConstraints (groupsns)
      {
      // set the element namespace of this object
      setElementNamespace(groupsns->getURI());
      // connect to child objects
      connectToChild();
      // load package extensions bound with this object (if any)
      loadPlugins(groupsns);
      }

      The call to loadPlugins(...) is necessary to allow the package to be extended by other package extensions. It is also worth noting that the constructor for SBMLNamespaces and its derived class SBMLExtensionNamespaces throw an SBMLExtensionException if the argument it is given is invalid. Callers will have to create the object using code such as the following example from the Layout package:

      try
      {
      LayputPkgNamespaces layoutns(3, 1, 1);
      Layout layout(&layoutns);
      }
      {
      cerr << "Caught " << e.what() << endl;
      }

    4. Override the copy constructor, assignment operator, and clone() methods from SBase and implement appropriate versions for the package.

    5. Override the following virtual methods if the object defines "id" and "name" attributes: setId(...), setName(...), isSetId(...), isSetName(...), unsetId(...), unsetName(...).

    6. If the SBML object defines any other attributes, then for every attribute defined by the class, implement new methods to set, get, unset, and query the attribute value. For an attribute named ATTRIB, this will lead to methods named setATTRIB, getATTRIB, isSetATTRIB, and unsetATTRIB.

    7. Override the following virtual methods if the object defines any attributes:

      • addExpectedAttributes(ExpectedAttributes& attributes): This method should add the attributes that are expected to be found on this kind of extended object in an SBML file or data stream.

      • readAttributes(XMLAttributes& attributes, ExpectedAttributes& expectedAttributes): This method should read the attributes expected to be found on this kind of extended object in an SBML file or data stream.

      • hasRequiredAttributes(): This method should return true if all of the required attribute for this extended object are present on instance of the object.

      • writeAttributes(XMLOutputStream& stream): This method should write out the attributes of an extended object. The implementation should use the different kinds of writeAttribute methods defined by XMLOutputStream to achieve this.

    8. Override the following virtual methods if the object defines one or more subobjects:

      • createObject(XMLInputStream& stream): Subclasses must override this method to create, store, and then return an SBML object corresponding to the next XMLToken in the XMLInputStream.

      • connectToParent(SBase *sbase): This creates a parent-child relationship between a given extended object and its subcomponent(s).

      • setSBMLDocument(SBMLDocument* d): This method should set the parent SBMLDocument object on the subcomponent object instances, so that the subcomponent instances know which SBMLDocument contains them.

      • enablePackageInternal(std::string& pkgURI, std::string& pkgPrefix, bool flag): This method should enable or disable the subcomponent based on whether a given XML namespace is active.

      • writeElements(XMLOutputStream& stream): This method must be overridden to provide an implementation that will write out the expected subcomponents/subelements to the XML output stream.

      • readOtherXML(SBase* parentObject, XMLInputStream& stream): This function should be overridden if XML elements containing annotations, notes, MathML content, etc., need to be directly parsed from the given XMLInputStream object.

      • hasRequiredElements(): This method should return true if a given object contains all the required subcomponents defined by the specification for that SBML Level 3 package.

    9. Override the virtual method writeXMLNS(XMLOutputStream& stream) if the package needs to add additional xmlns attributes to declare additional XML namespace URIs.

    10. Define any additional methods needed by the class, for example to add and remove subobjects.

In the directory structure for packages (Section 1. Create the directory structure), the code for new object classes is placed in the subdirectory named sbml.

Developers can take advantage of the many package implementations available for libSBML to see real-life examples of objects derived from SBase.

5. Derive a class from SBMLExtension

The SBMLExtension class is an abstract class that must be extended by each package extension implementation. The class provides methods for managing common attributes of package extensions (e.g., the SBML package name, the package version, and more), registration of instantiated SBasePluginCreator objects, and initialization/registration of package extensions when the library code for the package is loaded by libSBML.

The documentation for SBMLExtension explains in detail the process of extending the class as part of the implementation of a package extension. The following is a summary of the steps:

  1. Define a getPackageName() method that returns the name of the package as a string.

  2. Define a set of methods that return the default SBML Level, SBML Version and version of the package.

  3. Define methods returning the package namespace URIs

  4. Override basic pure virtual methods on SBMLExtension.

  5. Create definitions derived from SBMLExtensionNamespaces.

  6. Override the SBMLExtension method getSBMLExtensionNamespaces().

  7. Define an enumeration for the package object type codes.

  8. Override the SBMLExtension method getStringFromTypeCode().

  9. Implement an init() method.

  10. Instantiate a global SBMLExtensionRegister object.

6. Implement a forward declaration file

Create a file that provides forward declarations for all classes defined by the package extension. In the directory structure for packages (Section 1. Create the directory structure), this file should be placed in the subdirectory named common. Here is an example from the Layout package; this can be found in the file src/packages/layout/common/layoutfwd.h. The definition of CLASS_OR_STRUCT is to permit this code to work for both C and C++:

7. Implement a package header file

Create a single header file that includes all other package header files necessary to declare the types defined by the package extension. In the directory structure for packages (Section 1. Create the directory structure), this file should be placed in the subdirectory named common. Here is an example from the Layout package; this can be found in the file src/packages/layout/common/LayoutExtensionTypes.h:

8. Define files for package registration

Two files need to be created in src/sbml/packages as part of the libSBML scheme for including packages at compilation time. The first file should be named name-register.h, where name is the name of the package extension. Here is an example from the Groups package, in the file src/sbml/packages/groups-register.h:

#ifdef USE_GROUPS
#endif

Note the contents in the file above: it includes the extension file in the package source directory (GroupExtension.h), and it uses a ifdef condition on a variable named USE_NAME, where NAME is the package name.

The second file should be named name-register.cxx. It will be used as an inclusion by another part of libSBML to invoke the init() method of the class created in step 4 above (5. Derive a class from SBMLExtension). This file should have content as shown in the next example taken from src/sbml/packages/groups-register.cxx:

#ifdef USE_GROUPS
GroupsExtension::init();
#endif

9. Hook into the language bindings

This part of the process is unfortunately one of the most complex to perform and to describe. The simplest approach is to copy the files from an existing package implementation and modify them to work with the names and objects defined in the new package. The following are the relevant language binding files for C#, Java, Perl, Python, R and Ruby for a package such as Groups:

src/bindings/csharp/local-downcast-extension-groups.i
src/bindings/csharp/local-downcast-namespaces-groups.i
src/bindings/csharp/local-packages-groups.i

src/bindings/java/local-downcast-extension-groups.i
src/bindings/java/local-downcast-namespaces-groups.i
src/bindings/java/local-packages-groups.i

src/bindings/perl/local-downcast-extension-groups.cpp
src/bindings/perl/local-downcast-namespaces-groups.cpp
src/bindings/perl/local-downcast-packages-groups.cpp
src/bindings/perl/local-downcast-plugins-groups.cpp
src/bindings/perl/local-groups.i

src/bindings/python/local-downcast-extension-groups.cpp
src/bindings/python/local-downcast-namespaces-groups.cpp
src/bindings/python/local-downcast-packages-groups.cpp
src/bindings/python/local-downcast-plugins-groups.cpp
src/bindings/python/local-groups.i

src/bindings/r/local-downcast-extension-groups.cpp
src/bindings/r/local-downcast-namespaces-groups.cpp
src/bindings/r/local-downcast-packages-groups.cpp
src/bindings/r/local-downcast-plugins-groups.cpp
src/bindings/r/local-groups.i

src/bindings/ruby/local-downcast-extension-groups.cpp
src/bindings/ruby/local-downcast-namespaces-groups.cpp
src/bindings/ruby/local-downcast-packages-groups.cpp
src/bindings/ruby/local-downcast-plugins-groups.cpp
src/bindings/ruby/local-groups.i

src/bindings/swig/groups-package.h
src/bindings/swig/groups-package.i

10. Hook into the libSBML build system

Inclusion and compilation of packages in libSBML is supported through the use of the CMake system. To connect a new package extension to the rest of the libSBML build system, two CMake files have to be created: (1) a CMake file to compile the various parts of the package implementation, and (2) a CMake file that connects the package to the rest of the libSBML build system.

  1. CMake file to compile the package code. Create a name-package.cmake file at the top level of the libSBML source tree, /src, where name is the name of the package. Here is an example of the groups-package.cmake file from the Groups package:

    1 if(ENABLE_GROUPS)
    2  include(${LIBSBML_ROOT_SOURCE_DIR}/groups-package.cmake)
    3 
    4  # Go through all the Groups directories and build a list of files.
    5  set(GROUPS_SOURCES)
    6  foreach(dir common extension sbml validator)
    7  include_directories(${CMAKE_CURRENT_SOURCE_DIR}/sbml/packages/groups/${dir})
    8 
    9  file(GLOB current ${CMAKE_CURRENT_SOURCE_DIR}/sbml/packages/groups/${dir}/*.cpp
    10  ${CMAKE_CURRENT_SOURCE_DIR}/sbml/packages/groups/${dir}/*.c
    11  ${CMAKE_CURRENT_SOURCE_DIR}/sbml/packages/groups/${dir}/*.h)
    12 
    13  # Special handling for the validator: set the *Constraints.cpp files to be
    14  # 'header' files so they won't be compiled. They are #included directly instead.
    15 
    16  if ("${dir}" STREQUAL "validator/constraints")
    17  foreach(tempFile ${current})
    18  if ("${tempFile}" MATCHES ".*Constraints.cpp")
    19  set_source_files_properties(
    20  ${tempFile}
    21  PROPERTIES HEADER_FILE_ONLY true
    22  )
    23  endif()
    24  endforeach()
    25  endif()
    26 
    27  set(GROUPS_SOURCES ${GROUPS_SOURCES} ${current})
    28 
    29  # Mark header files for installation.
    30  file(GLOB groups_headers ${CMAKE_CURRENT_SOURCE_DIR}/sbml/packages/groups/${dir}/*.h)
    31  install(FILES ${groups_headers} DESTINATION include/sbml/packages/groups/${dir} )
    32  endforeach()
    33 
    34  # Create source group files for IDEs.
    35  source_group(groups_package FILES ${GROUPS_SOURCES})
    36 
    37  # Add the Groups package sources to libSBML sources.
    38  SET(LIBSBML_SOURCES ${LIBSBML_SOURCES} ${GROUPS_SOURCES})
    39 
    40  # Add unit test files.
    41  if(WITH_CHECK)
    42  add_subdirectory(sbml/packages/groups/extension/test)
    43  endif()
    44 
    45 endif()

  2. CMake file to connect the package to the rest of libSBML's build system. In the libSBML top-level, create another (different) file named name-package.cmake, where name is the name of the package. The file should contain code that looks like the following, where this example is drawn from the Layout package:

    option(ENABLE_GROUPS
    "Enable libSBML support for the SBML Level 3 Groups ('groups') package." OFF)
    # Provide summary status =
    list(APPEND LIBSBML_PACKAGE_SUMMARY "SBML 'groups' package = ${ENABLE_GROUPS}")
    if(ENABLE_GROUPS)
    add_definitions(-DUSE_GROUPS)
    set(LIBSBML_PACKAGE_INCLUDES ${LIBSBML_PACKAGE_INCLUDES} "LIBSBML_HAS_PACKAGE_GROUPS")
    list(APPEND SWIG_EXTRA_ARGS -DUSE_GROUPS)
    list(APPEND SWIG_SWIGDOCDEFINES --define USE_GROUPS)
    endif()

    An implementation starting with the code above could simply replace the string GROUPS with the appropriate name of the package.