Skip to content

Getting started with developing a native NPL Contributor Library

New feature (added in 2024.1.6)

Imagine needing a functionality that isn't natively available in NPL, for example encryption. Such a use-case could be satisfied using native NPL contributor library! A native NPL contributor library is very similar to an NPL contributor library. You can provide additional NPL constructs, and more importantly, define native NPL functions that should use JVM-based implementations instead of a normal NPL body.

Referencing and declaring protocols is currently unsupported.

Writing a native NPL Contributor Library and general guidelines

To get you started, we recommend perusing Platform's npl-contrib-native-sample.

The general components/layout of such a native NPL contributor library project are:

  1. JVM sources src/main/{kotlin, java, etc.}/
  2. An empty NPL src/npl/.gitkeep directory and .gitkeep file
  3. A src/main/resources directory containing:
  4. a META-INF/services/com.noumenadigital.npl.contrib.NativeImplWithoutRuntimePlugin file
  5. a npl-contrib-native directory containing your native NPL source files
  6. JVM test sources src/test/{kotlin, java, etc.}/,
  7. NPL test sources src/test/npl/ directory
  8. A Maven pom.xml

Your JVM sources should have 1 class that implements the NativeImplWithoutRuntimePlugin abstract class, and provides an implementation for public abstract void initialize(). In this function, you should use the NativeImplWithoutRuntimePlugin registry to register your native JVM implementations:

class NativeConcatenate : NativeImplWithoutRuntimePlugin() {
   private fun nativeConcatenate(values: List<Value>): TextValue {
      val (first, second) = values
      return TextValue(first.toString() + second.toString())
   }

   override fun initialize() {
      registry["/nativesample/v1/concatenate"] = NativeWithoutRuntime.create(::nativeConcatenate)
   }
}

Great care must be taken when inserting your native implementations in the registry. If a name collision occurs, you will break other native NPL contributor libraries. You are responsible for safely inserting your native implementations and error handling. Your native JVM implementations must be pure; they must not perform any file/network I/O operations. This may be subject to change over time. The Platform team will ensure of these requirements when reviewing your pull requests.

The NPL sources src/npl should remain completely empty but the directory structure should be present, hence the .gitkeep file. This is due to the nature of native NPL contrib library development, testing and packaging. Instead, your native NPL sources must live inside a resource folder called npl-contrib-native inside the main resources directory. Failure to do so will prevent you from testing your library and from making your native NPL source files discoverable; nothing will work. The first directory inside npl-contrib-native should be the main namespace of your library, i.e. nativesample.

To define an NPL function as having a native JVM-based implementation, you declare the function without a body, starting with the reserved keyword native:

   package nativesample.v1;
   native function concatenate(first: Text, second: Text) returns Text;

Its associated NativeImplWithoutRuntimePlugin registry key would then be: "/nativesample/v1/concatenate".

As with Kotlin's extension function, you can do something similar in NPL for standard library types:

   package nativesample.v1;
   native function Text.concatenate(other: Text) returns Text;

Its associated NativeImplWithoutRuntimePlugin registry key would then be: "/Text::nativesample/v1/concatenate".

Similarly, you can also create an extension function using your own NPL constructs:

   package nativesample.v1;
   struct MyStruct { myField: Text };
   native function MyStruct.concatenate(other: Text) returns Text;

Its associated NativeImplWithoutRuntimePlugin registry key would then be: "//nativesample/v1/MyStruct::nativesample/v1/concatenate".

Function arguments are passed in-order in the list of values in NativeWithoutRuntime.create.

When using the NPL "extension function" notation Text.myFunction, the self reference value is always passed as the first argument in the list of values in NativeWithoutRuntime.create.

In addition to the npl-contrib-native resource you must also provide a mandatory plugin file resource: META-INF/services/com.noumenadigital.npl.contrib.NativeImplWithoutRuntimePlugin. This file must exist inside a resources/META-INF/services directory structure and must be named exactly com.noumenadigital.npl.contrib.NativeImplWithoutRuntimePlugin. The content of this file must be 1 plain-text line of the fully qualified class name implementing NativeImplWithoutRuntimePlugin, i.e.:

com.noumenadigital.nativesample.NativeConcatenate

Your JVM test sources should assert that your JVM implementations behave as expected, while your NPL test sources should verify that NPL code can correctly call into the native JVM implementations. It is advisable to also test any additional NPL constructs that you may have provided with your native NPL library such as enum, struct, etc.

Your Maven project pom.xml should depend on Platform's language-builtins-native using a provided scope.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>

   <groupId>com.noumenadigital.platform</groupId>
   <artifactId>npl-contrib-native-sample</artifactId>
   <version>0.0.1-SNAPSHOT</version>

   <properties>
      <!-- the latest platform version whenever possible -->
      <platform.version>2024.1.7</platform.version>
   </properties>

   <dependencies>
      <dependency>
         <groupId>com.noumenadigital.platform</groupId>
         <artifactId>language-builtins-native</artifactId>
         <version>${platform.version}</version>
         <!-- Since Platform will include this dependency, it should be provided -->
         <scope>provided</scope>
      </dependency>
   </dependencies>

   <build>
      <plugins>
         <!-- Build Plugins to build your native NPL contributor library -->
         <plugin>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-plugin</artifactId>
            <version>1.9.10</version>
         </plugin>
         <!-- ... -->

         <!-- Platform's npl-maven-plugin for NPL testing of your library -->
         <plugin>
            <groupId>com.noumenadigital.platform</groupId>
            <artifactId>npl-maven-plugin</artifactId>
            <version>${platform.version}</version>
            <executions>
               <execution>
                  <goals>
                     <goal>npl-compile</goal>
                     <goal>npl-test-compile</goal>
                     <goal>npl-test</goal>
                  </goals>
               </execution>
            </executions>
         </plugin>
      </plugins>
   </build>
</project>

Versioning

You are free to choose how to version your native NPL contributor library source files and your produced jar file. However, we recommend to version your native NPL source files using a major version namespace & directories such as v1 and v2. This should make your life easier for patch & minor releases.

All versions of your native NPL contributor library source files should be present in a single jar file for every release. Clients (Platform) using your native NPL contributor library defined NPL constructs must continue to use it once they use it in production, otherwise their environment will break.

Packaging

Your project should produce a library jar file, containing both META-INF and npl-contrib-native as resources at the root of the jar file.

Deployment

You are free to choose where & how to publish your jar library artefact as long as it supports Maven. It must be publicly available on maven-central or similar public artefact repository.

Integrating a native NPL Contributor Library into an NPL project and in IntelliJ

The Platform team reserves the right to decide which native NPL contributor libraries to include as part of its release. This is initially the only way to have access to your native NPL contrib library available for production and development using our IntelliJ NPL Plugin. In other words, you will need the latest platform release version supporting your native NPL contrib library. Once Platform includes your library as a dependency, it will become automatically available in the IntelliJ NPL plugin and for production usage.