VCF Automation Blog

from Stefan Schnell


A shared object is a Linux library which contains code and/or data that can be used between multiple processes. They are binary files and similar to Dynamic Link Libraries (DLLs) in Windows. When a Linux program needs a shared object, it loads it into memory and uses its code and/or data. Shared objects are built with compilers like C or C++. This post shows how shared objects can be used in VCF Automation with the Python runtime environment.

Use Shared Object with Python


Create Shared Object with PureBasic

For my first experiment I am using the PureBasic programming language, which is available for Linux, Windows and OS X. However, any other programming language can also be used that supports the building of shared objects. The source code is very easy to understand. Four functions are exposed. The first adds two integer numbers, the second subtracts two integer numbers, the third returns a hello world string, but it only works with Windows, and the fourth returns also a hello world string, and that works with Linux and Windows.

ProcedureDLL.i Add(x.i, y.i)
  ProcedureReturn x + y
EndProcedure

ProcedureDLL.l Sub(x.l, y.l)
  ProcedureReturn x - y
EndProcedure

ProcedureDLL.s HelloWindows(name.s)
  If Trim(name) = #Empty$
    ProcedureReturn "Hello world from PureBasic"
  Else
    ProcedureReturn "Hello " + name + " from PureBasic"
  EndIf
EndProcedure

ProcedureDLL.i Hello(name.s)
  
  _name.s = PeekS(@name, -1, #PB_UTF8)
  
  retValue.s = #Empty$
  If Trim(_name) = #Empty$
    retValue = "Hello world from PureBasic"
  Else
    retValue = "Hello " + _name + " from PureBasic"
  EndIf
  
  *retValue = AllocateMemory(Len(retValue) + 1)
  PokeS(*retValue, retValue, Len(retValue), #PB_UTF8)
  
  ProcedureReturn *retValue
  
EndProcedure

I assume that the source code was saved with the name libPython.pb.

With the command
./pbcompiler libPython.pb -so libPython.so
the source code is compiled into a shared object.

Create Shared Object with Rust

For my second experiment I am using the Rust programming language, which is also available for Linux, Windows and OS X. The code is identical to the above, with the exception that the Windows specific function is not available.

use std::ffi::{c_char, CStr, CString};

#[no_mangle]
pub extern fn Add(x: i64, y: i64) -> i64 {
    x + y
}

#[no_mangle]
pub extern fn Sub(x: i32, y: i32) -> i32 {
    x - y
}

#[no_mangle]
pub extern fn Hello(name: *const c_char) -> *mut c_char {
    let cstr: &CStr = unsafe { CStr::from_ptr(name) };
    let _name: String = String::from_utf8_lossy(cstr.to_bytes()).to_string();
    let mut retValue: String = "Hello world from Rust".to_string();
    if !(_name.is_empty()) {
        retValue = "Hello ".to_string() + &_name + " from Rust";
    }
    CString::new(retValue).unwrap().into_raw()
}

Usage of the Shared Object

Now let's take a look at the Python source code that loads the library and executes its functions.
This code is also very easy to understand. The library is loaded, then the first function Add is executed with the numbers 10 and 1. Then the second function Sub is executed with the same numbers. The third function is only called if this program is executed in a Windows environment, which is not the case here. A name is passed to the fourth function and it returns a hello world string. The result is returned as a Python dictionary respectively as VCF Automation Properties.

"""
@module de.stschnell

@version 0.1.0

@runtime python:3.10

@memoryLimit 256000000

@timeout 360

@outputType Properties

Checked with Aria Automation 8.12.0, 8.16.2 and 8.18.0
"""
import json
import os.path
import platform
from ctypes import*

def handler(context, inputs):

    result = {}

    try:

        dllName = os.path.dirname(os.path.abspath(__file__))
        if platform.system() == "Windows":
            dllName += "\\libPython.dll"
        elif platform.system() == "Linux":
            dllName += "/libPython.so"
        libPython = cdll.LoadLibrary(dllName)

        libPython.Add.argtypes = [ c_int64, c_int64 ]
        libPython.Add.restype = c_int64
        resultAdd = libPython.Add(10,1)
        result["Addition"] = str(resultAdd)

        libPython.Sub.argtypes = [ c_long, c_long ]
        libPython.Sub.restype = c_long
        resultSub = libPython.Sub(10,1)
        result["Substraction"] = str(resultSub)

        # Delete the next 6 lines if Rust is used
        if platform.system() == "Windows":
            libPython.HelloWindows.argtypes = [ c_wchar_p ]
            libPython.HelloWindows.restype = c_wchar_p
            name = c_wchar_p("Stefan")
            resultHelloWindows = libPython.HelloWindows(name)
            result["HelloWindows"] = resultHelloWindows

        libPython.Hello.argtypes = [ c_char_p ]
        libPython.Hello.restype = c_char_p
        name = "Stefan".encode("utf-8")
        resultHello = libPython.Hello(name)
        result["Hello"] = str(resultHello, "utf-8")

        outputs = {
            "status": "done",
            "result": result
        }

    except Exception as err:

        outputs = {
            "status": "incomplete",
            "error": repr(err),
            "result": result
        }

    return outputs

The files must now be combined in a zip file.

vcf automation python zip file with handler.py and shared object files from purebasic
This zip file can now be imported as an action.

vcf automation python example which uses a shared object
After we have executed the action, we see the correct and expected results. For addition 10 plus 1 equals 11, for subtraction 10 minus 1 equals 9 and the hello world message. We get the same result with the Rust Shared Object, but here with a different response for Hello.

vcf automation python example which uses a shared object from rust

Conclusion

The use of shared libraries or shared objects is very easy with the Python runtime environment. On this way it is also possible to use more extensive functionalities and possibilities of other programming languages in the context of VCF Automation. Cross-platform development is also easily possible. In this example the library was built as a Windows Dynamic Link Library (DLL) and as a Photon OS Shared Object (SO). The source code was compiled unchanged and then used with different Python versions on the different operating systems.

Addendum

Install PureBasic in a Photon Container on Red Hat Linux

First I built Shared Objects with other Linux derivatives. Then I realized that the environmental conditions had to be identical to use the library with Photon OS, like compiler, libraries and packages etc. This is not always so easy because it is necessary to install it. This rised the idea of compiling the library directly with Photon OS. On this way there are no expected difficulties, because the compilation system is identical to the target system on which the library is used. Here is a short description how to install PureBasic in a Photon OS 4.0 container under Red Hat Linux.

  1. Load the Photon image from the repository.
    podman pull photon:4.0

  2. Runs an interactive process in a new container
    podman run -it --name photon photon:4.0

  3. From another terminal copy PureBasic to the Photon container.
    podman cp ./PureBasic_Linux2_X64_LTS_6.12.tar photon:/home

  4. Install the necessary packages in the Photon container.
    tdnf install gcc glibc-devel binutils vim phyton3
    Hint: The Vim editor and Python are not necessary, but they can be used to edit the PureBasic source code or to test the shared object, which can be very helpful.

  5. Extract the archive in the home directory of the Photon container.
    tar -xf ./PureBasic_Linux2_X64_LTS_6.12.tar

  6. Set in the Photon container the PUREBASIC_HOME environment variable.
    export PUREBASIC_HOME=/home/purebasic

  7. Check the PureBasic compiler.
    ./purebasic/compilers/pbcompiler

    terminal window of photon with purebasic compiler call
The charm of this approach is that it is not necessary to worry about whether the library can actually be executed on the target system. Of course there are other ways, but on this way it is possible to test in a system that is equivalent to the target system, namely that of VCF Automation. This approach has already been successfully tested several times via the Python execution environment in VCF Automation. This integration scenario opens up many new perspectives, but of course also challenges. But first it's good to know that it works and how it works.