Skip to content
Peter Verhas edited this page Aug 21, 2020 · 2 revisions

How to embed ScriptBasic for Java using the Native API

Even though most of the features can be used using the JSR223 interface of ScriptBasic for Java, there are some features that are not available via the standard interface. The reason for this is that these features are above the functionality of what the standard can handle.

For example the standard interface does not provide any possibility to refer to other script. When you want to execute a script that contain include or import statements to include or import other scripts then there has to be some way to define where the included scripts are. It can be on the local disk on the file system, in a database or somewhere over the network. There is no such feature in the standard interface. This is only one, albeit important feature that need the native API.

To use ScriptBasic for Java using the native API you have to have the ScriptBasic for Java JAR file on the classpath during execution as well as during development. You may recall that using ScriptBasic for Java via the standard interface does not require any ScriptBasic specific in the development environment. In case of the native API you need the JAR.

Development dependencies

Using Maven

If you use Maven then you can simply define in your POM file that references the actual version of ScriptBasic for Java:

	<dependencies>
		<dependency>
	        <groupId>com.scriptbasic</groupId>
	        <artifactId>jscriptbasic</artifactId>
	        <version>${project.version}</version>
		</dependency>
	</dependencies>

The artifacts are uploaded into the central repository maintained by Sonatype.

Using ANT

Go to the web interface of the central repository of Sonatype and download the artifact

  http://central.maven.org/maven2/com/scriptbasic/jscriptbasic/${project.version}/jscriptbasic-${project.version}.jar 

place it to where the other library files are and compile your application embedding ScriptBasic.

Embedding API

Using the native API of ScriptBasic for Java you should use the methods defined in the interface com.scriptbasic.api.ScriptBasic. This interface is implemented by the class com.scriptbasic.Engine. In the following sections we will have a look at how to use this class

  • to execute a program

    • from a string

    • from a java.io.Reader

    • from a file (java.io.File)

  • execute a program that includes other files

    • specifying directories as path

    • specifying a ScriptBasic specific SourcePath, which is something similar to the java ClassPath

    • specifying a ScriptBasic specific SourceProvider, which is something similar to a Java ClassLoader

We will also have a look at how to set global variables before executing the program, and how to get values of global variables after the code was executed. We will also see how to list all the global variables and also how to invoke a subroutine after the code was executed. Calling subroutines is possible passing arguments and getting return values.

Hello world

The simplest ever use of the ScriptBasic for Java native API is to execute a script that is available in a string:

        ScriptBasic engine = ScriptBasic.engine();
        engine.eval("print \"hello world\"");

This code creates a new Engine object, then calls the method eval with the string that contains the program code. Note that ScriptBasic is an interface and Engine is the implementation. If you want to have the output of the program in a String you can create a StringWriter and redirect the standard output of the program to there:

        ScriptBasic engine = ScriptBasic.engine();
        StringWriter sw = new StringWriter(10);
        engine.setOutput(sw);
        engine.eval("print \"hello world\"");
        sw.close();
        assertEquals("hello world", sw.toString());

When you do not have the code in a string you can tell the interpreter to execute the code reading the BASIC program from a java.io.Reader:

        ScriptBasic engine = ScriptBasic.engine();
        StringWriter sw = new StringWriter(10);
        engine.setOutput(sw);
        StringReader sr = new StringReader("print \"hello world\"");
        engine.eval(sr);
        sw.close();
        assertEquals("hello world", sw.toString());

or from a java.io.File:

        ScriptBasic engine = ScriptBasic.engine();
        StringWriter sw = new StringWriter(10);
        engine.setOutput(sw);
        File file = new File(getClass().getResource("hello.bas").getFile());
        engine.eval(file);
        sw.close();
        assertEquals("hello world", sw.toString());

Note that the sample above is included from a junit test file. When you use this API you will much easier get to your file name than this.getClass().getResource("hello.bas").getFile().

Including files

When a BASIC file includes another you should use a bit complex setup. Even though the method eval(File file) could handle an include statement and include the other BASIC scripts from the same directory where the first file is, it does not. Eval is to execute simple scripts that are in one file and do not need code fragments from other files. Most of the BASIC programs are like that, simple and short.

When you want to execute more complex programs that rely on other BASIC code fragments that are included into the original BASIC code then you have to specify the source path where to look for these files. Using the native API you have three opportunities:

  • define the directories as a string array (variable arguments method)

  • define the directories supplying a SourcePath object that may calculate the list of directories on the fly.

  • define a SourceProvider object that reads the content from any source you want.

The latter are more flexibile but the same time you face more programming task embedding the BASIC interpreter into your application.

To specify the directories where the files are use the following piece of code:

        ScriptBasic engine = ScriptBasic.engine();
        StringWriter sw = new StringWriter(10);
        engine.setOutput(sw);
        String path = new File(getClass().getResource("hello.bas").getFile())
                .getParent();
        engine.eval("include.bas", path);
        sw.close();
        assertEquals("hello world", sw.toString());

In this example the sample code gets the directory where the "hello.bas" Java resource is and then sets this directory as a one element source path. Since the second parameter to eval(String, String ...) in this case is variable argument, you can use there String[] array, or simply as many String parameters as you like:

        ScriptBasic engine = ScriptBasic.engine();
        engine.eval("include.bas", ".", "..", "/usr/include/scriptbasic");

The second possibility is to provide a SourcePath object. The following sample shows you a very simple use of this approach:

        ScriptBasic engine = ScriptBasic.engine();
        StringWriter sw = new StringWriter(10);
        engine.setOutput(sw);
        String path = new File(getClass().getResource("hello.bas").getFile())
                .getParent();
        SourcePath sourcePath = new BasicSourcePath();
        sourcePath.add(path);
        engine.eval("include.bas", sourcePath);
        sw.close();
        assertEquals("hello world", sw.toString());

The example does the same as the previous one and there is no advantage to use the BasicSourceProvider implemented in ScriptBasic, but there is nothing to stop you to create your own class implementing the interface SourcePath. In that case this method can be used to deliver the different locations via your class. Also note that the class BasicSourcePath is implemented in a package that is not exported by ScriptBasic module and thus it is not usable from outside. It is here only as an example and the source code is available to consult.

The third and most powerful approach is to provide your own implementation of the SourceProvider. This is the only approach when your BASIC code is not in the file system, and you can not simply provide a java.io.Reader to the source code because the file may include other BASIC files. The sample code that does this is the following:

        ScriptBasic engine = ScriptBasic.engine();
        StringWriter sw = new StringWriter(10);
        engine.setOutput(sw);
        SourceProvider provider = new SourceProvider() {
            private Map<String, String> source = new HashMap<>();

            {
                source.put("hello.bas", "print \"hello world\"");
                source.put("include.bas", "include \"hello.bas\"");
            }

            @Override
            public SourceReader get(String sourceName, String referencingSource)
                    throws IOException {
                return get(sourceName);
            }

            @Override
            public SourceReader get(String sourceName) throws IOException {
                final SourceReader reader = new GenericSourceReader(new StringReader(source.get(sourceName)), this, sourceName);
                return new GenericHierarchicalSourceReader(reader);
            }
        };
        engine.eval("include.bas", provider);
        sw.close();
        assertEquals("hello world", sw.toString());

This sample code implements an anonymous class of the interface SourceProvider implementing both of the get methods. Note that these methods return a BASIC SourceReader and not a java.io.Reader. The actual implementation contains a mini file system in itself containing two files: include.bas and hello.bas stored in a hash map. The method get(String sourceName) reads the one that is named and creates a GenericReader implemented in ScriptBasic for Java.

Note that your implementation of a SourceProvider will, probably be more complex than this one. The algorithm implemented in the methods get(String sourceName) and in get(String sourceName,String referencingSource) may search the script content in file, in database or in any other type of repository using the sourceName as an absolute identifying string for the source or as a string that identifies the script together with the identifying string of the script that includes the other string (referencingSource).

When you design the store for your scripts you need not stick to the syntax of the file names and directories. If you have a good reason to use different naming convention for your script entities in your script store you are free.

Setting variables

Before executing the program you can set the values of certain global variables. These variables will be available for the script exactly as it was set by some BASIC code. To do so the following sample can be used:

        ScriptBasic engine = ScriptBasic.engine();
        StringWriter sw = new StringWriter(10);
        engine.setOutput(sw);
        engine.setVariable("a", 13);
        engine.eval("print a + \"hello world\"");
        sw.close();
        assertEquals("13hello world", sw.toString());

To set the variable you should pass a Java Object to the method. Whenever you pass a primitive it will be autoboxed to an Object and ScriptBasic for Java will convert it to a BASIC object. The conversion is automatic. Byte, Short and Integer values are converted to BASIC integer numerical values, which is essentially Long. Float values are converted to BASIC floating point numbers, which are Double. Character and String values are converted to BASIC strings, which are String. The BASIC code works embedding these Long, Double and String objects into RightValue objects which is the value holding object in BASIC.

If you happen to pass a RightValue, which is the already converted value to the method setVariable() then it will be stored without conversion.

Any other objects, which can not be converted to RightValue wrapped Long, Double or String will be wrapped into a new RightValue as a general Java object and will be accessible from the BASIC code as a Java Object: you can access fields X.field or you can call methods of the object X.method(), so be careful what you pass to the BASIC program.

If you want to pass an object to the BASIC program but you do not want any BASIC program to poke it (access fields and call methods on it) then either

  1. make sure that the object's class implements the interface NoAccess (exported as the SPI of the BASIC interpreter) or
  2. if it is not possible because you have no access over the class (e.g. third party and you do not control the source) then you can put the object first into a NoAccessProxy object (also available in the PSI package) and pass that to the BASIC program. The class NoAccessProxy contains a public field target that you can set and read any time and it also implements the NoAccess interface so the BASIC program will not be able to access the field and through the field your object.

However, generally it is better not ot pass any object to the BASIC program that it has nothing to do with. The above methodologies are there in case you want to pass an object to the BASIC program that it can pass on as argument to a subroutine call implemented in Java.

After the code was executed you are able to query the values of the global variables:

        ScriptBasic engine = ScriptBasic.engine();
        engine.eval("a = \"hello world\"");
        String a = (String) engine.getVariable("a");
        assertEquals("hello world", a);

In this way the BASIC object represented by the ScriptBasic for Java internal class RightValue is converted to a plain Java object. This plain java object can be Long, Double, Boolean or String.

ScriptBasic for Java does not use Byte, Short, Integer, Character and Float values internally therefore a number that you may get as a return value of a subroutine (see that later) or as the value of a global variable can only be Long, Double, Boolean and character variable can only be String.

A BASIC object however can also contain arbitrary objects as well. In that case you will get the object itself without conversion. It may be an object you set before the execution of the code but it can also be an object returned by a Java method that was called as BASIC function or as a method on an object injected into the code before execution calling the method setVariable().

If you do not know the name of the global variable beforehand you can list the names of the global variables after the execution of the code programmatically:

        ScriptBasic engine = ScriptBasic.engine();
        engine.eval("a = \"hello world\"\nb=13");
        String varnames = "";
        for (String varname : engine.getVariablesIterator()) {
            varnames += varname;
        }
        Assert.assertTrue(varnames.indexOf('a') != -1);
        Assert.assertTrue(varnames.indexOf('b') != -1);
        assertEquals(2, varnames.length());

The value returned by the method engine.getVariablesIterator() is an iterator that you can use in for loops or in any other Java way to iterate through the names of the global variables.

Loading code

If you want to load a BASIC program ready for execution, but do not want to execute yet, you can call the method load(). For every method eval() there is a method names load() handling the same type of arguments. The method load() only loads the code, performs all analysis that are needed to execute the code, but does not execute it. To execute the code you can call the method execute(). It is possible to call this method multiple times and execute the same program multiple times.

Using the methods load() is also possible when you do not want to execute the main code of a BASIC script, you only want to call subroutines without the execution of the main code.

Calling a subroutine from Java

After the code was loaded (or even executed) you can call subroutines defined in the BASIC program. Note that ScriptBasic for Java does not distinguish between functions and procedures. There is only Sub that may but does not need to return a value.

To call a subroutine you have to know the name of the subroutine and you should call the method call():

        ScriptBasic engine = ScriptBasic.engine();
        engine.eval("sub applePie\nglobal a\na = \"hello world\"\nEndSub");
        String a = (String) engine.getVariable("a");
        assertNull(a);
        engine.getSubroutine("applePie").call();
        a = (String) engine.getVariable("a");
        assertEquals("hello world", a);

This sample shows you how to call a subroutine that does not need any parameter and does not return any value. The next example shows how to pass arguments and how to get the return value:

        ScriptBasic engine = ScriptBasic.engine();
        engine.eval("sub applePie(b)\nglobal a\na = b\nreturn 6\nEndSub");
        String a = (String) engine.getVariable("a");
        assertNull(a);
        Long ret = (Long) engine.getSubroutine("applePie").call("hello world");
        a = (String) engine.getVariable("a");
        assertEquals("hello world", a);
        assertEquals((Long) 6L, ret);

The arguments to the method call() is a varag Object[] so you can pass values directly listed on the call or as an array. If you pass more arguments than what the subroutine is capable handling then an exception will be thrown. If the argument list is too short then the final arguments not matched by the passed values will be undefined. To get the number of arguments a subroutine expects you should call the method getNumberOfArguments(String name) with the subroutine name as argument. To get all the subroutines the BASIC program defines you should call the method getSubroutineNames():

        ScriptBasic engine = ScriptBasic.engine();
        engine.eval("sub applePie(b)\nEndSub\nsub anotherSubroutine\nEndSub\n");
        int i = 0;
        for (@SuppressWarnings("unused")
                String subName : engine.getSubroutineNames()) {
            i++;
        }
        assertEquals(2, i);
        assertEquals(1, engine.getNumberOfArguments("applePie"));
        assertEquals(0, engine.getNumberOfArguments("anotherSubroutine"));
Clone this wiki locally