How to embed ScriptBasic for Java using the Native API

Even though most of the features can be used using the JSR223 ineterface 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 interfaces do not provide any possibility to define for a script to refer to other script. When you want to execute a script that 'includes' or 'imports' other scripts then there has to be some way to define where the included script is. 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.

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 using 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.

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>1.0.4-SNAPSHOT</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/1.0.4-SNAPSHOT/jscriptbasic-1.0.4-SNAPSHOT.jar 

place it to where the other library files are.

Embedding API

Using the native API of ScriptBasic for Java you should use the methods defined in the interface com.scriptbasic.interfaces.EngineApi. 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:

		EngineApi engine = new 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 EngineApi 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:

		EngineApi engine = new Engine();
		StringWriter sw = new StringWriter(10);
		engine.setOutput(sw);
		engine.eval("print \"hello world\"");
		sw.close();
		Assert.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:

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

or from a java.io.File:

		EngineApi engine = new Engine();
		StringWriter sw = new StringWriter(10);
		engine.setOutput(sw);
		File file = new File(getClass().getResource("hello.bas").getFile());
		engine.eval(file);
		sw.close();
		Assert.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 can not use the above methods. Even though the method eval(File file) could handle an include statement and include the other BASIC script from the same directory where the first file is, it does not and it does it for good reason. The reason is called security. When you use the above, simple methods you need not fear that your BASIC program gets feral.

When you want to execute more complex programs that rely on other BASIC codes that are include-ed or import-ed 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 you select the more flexibility you have, and the more programming you face.

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

		EngineApi engine = new 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();
		Assert.assertEquals("hello world", sw.toString());

Since the last parameter is variable argument, you can use there String[] array, or simply as many String parameters as you like:

		EngineApi engine = new 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:

		EngineApi engine = new 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();
		Assert.assertEquals("hello world", sw.toString());

Actually this way of use has no advantage of this method over the previous one where you provided the path values as strings. However 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.

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:

		EngineApi engine = new Engine();
		StringWriter sw = new StringWriter(10);
		engine.setOutput(sw);
		SourceProvider provider = new SourceProvider() {
			private Map<String, String> source = new HashMap<String, String>();
			{
				source.put("hello.bas", "print \"hello world\"");
				source.put("include.bas", "include \"hello.bas\"");
			}

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

			@Override
			public Reader get(String sourceName) throws IOException {
				GenericReader reader = new GenericReader();
				reader.setSourceProvider(this);
				reader.set(new StringReader(source.get(sourceName)));
				return reader;
			}
		};
		engine.eval("include.bas", provider);
		sw.close();
		Assert.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 Reader 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 global 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:

		EngineApi engine = new Engine();
		StringWriter sw = new StringWriter(10);
		engine.setOutput(sw);
		engine.setVariable("a", 13);
		engine.eval("print a + \"hello world\"");
		sw.close();
		Assert.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 Long. Float values are converted to Double. Character values are converted to String. If you happen to pass a RightValue to the method setVariable() then it will be stored without conversion. Any other objects will be wrapped into a new RightValue 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().

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

		EngineApi engine = new Engine();
		engine.eval("a = \"hello world\"");
		String a = (String) engine.getVariable("a");
		Assert.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, Character, String or Boolean.

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 or Double 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:

		EngineApi engine = new 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);
		Assert.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.

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

After the code was executed and returned 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 need not return a value.

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

		EngineApi engine = new Engine();
		engine.eval("sub applePie\nglobal a\na = \"hello world\"\nEndSub");
		String a = (String) engine.getVariable("a");
		Assert.assertNull(a);
		engine.call("applePie", (Object[]) null);
		a = (String) engine.getVariable("a");
		Assert.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:

		EngineApi engine = new Engine();
		engine.eval("sub applePie(b)\nglobal a\na = b\nreturn 6\nEndSub");
		String a = (String) engine.getVariable("a");
		Assert.assertNull(a);
		@SuppressWarnings("deprecation")
		Long ret = (Long) engine.call("applePie", "hello world");
		a = (String) engine.getVariable("a");
		Assert.assertEquals("hello world", a);
		Assert.assertEquals((Long) 6L, ret);

If the argument list is too long, you will get an exception. 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():

		EngineApi engine = new Engine();
		engine.eval("sub applePie(b)\nEndSub\nsub anotherSubroutine\nEndSub\n");
		int i = 0;
		for (@SuppressWarnings("unused")
		String subName : engine.getSubroutineNames()) {
			i++;
		}
		Assert.assertEquals(2, i);
		Assert.assertEquals(1, engine.getNumberOfArguments("applePie"));
		Assert.assertEquals(0, engine.getNumberOfArguments("anotherSubroutine"));

Calling a subroutine object oriented way

The API calling a subroutine described above is not really object oriented. It helps a lot to write short implementations but for those that are keen on programming in OO style there is another interface in the native ScriptBasic for Java API Subroutine. This interface represents a subroutine in a BASIC program and is tied to an Engine. To get an instance you have to call the Engine method getSubroutine(String name). When you have the instance you can call call(Object ... args), getName() and getNumberOfArguments() methods.

		EngineApi engine = new Engine();
		engine.eval("sub applePie\nglobal a\na = \"hello world\"\nEndSub");
		String a = (String) engine.getVariable("a");
		Assert.assertNull(a);
		Subroutine applePie = engine.getSubroutine("applePie");
		applePie.call((Object[]) null);
		a = (String) engine.getVariable("a");
		Assert.assertEquals("hello world", a);

This sample above is the object oriented version of the sample before. Also see the following sample how to call a subroutine in OO way that returns some value:

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