question

Hao Zhou avatar image
1 Like"
Hao Zhou asked alan.zhang commented

How to create FlexScript classes that incorporate dot syntax in module SDK

In 17.1 beta, I noticed that FlexSim creates FlexScript classes in AGV and Conveyor modules. Is it possible to provide documents or tutorials to show how to create custom FlexScript classes incorporating dot syntax for module developers?

Thanks, Hao

FlexSim 17.0.3
module sdk
5 |100000

Up to 12 attachments (including images) can be used with a maximum of 23.8 MiB each and 47.7 MiB total.

1 Answer

Jordan Johnson avatar image
9 Likes"
Jordan Johnson answered alan.zhang commented

That is absolutely the plan. We'll make much better documentation in the future. For now, allow me to get you started with some basics.

FlexScript Interfaces

Each FlexScript interface is defined by a special object in the engine. That object stores the list of methods, operators, and properties for the interface. To add a new interface, all we have to do is create a new interface object, and make sure it goes in the list that the compiler and code editor read.

In order to create a new interface from a module, all you have to do is override a method called bindInterface(), and then let the engine know that it needs to call bindInterface() for that object. These two steps are slightly different for ObjectDataType objects and SimpleDataType objects, so I'll cover both. There is also a way to make an interface that wraps a pointer (like the Table interface). I'll explain a little about that as well. In all three cases, you will need to implement/overload a bindInterface() method. I'll discuss how to do that last.

ObjectDataType

In the C++ class definition, be sure to add the following method:

virtual void bindInterface() override;

You will also need to implement that method, and that will be discussed later. To make sure the engine calls your object's bindInterface() method, you need to add a node to the object's tree definition. This node should be a subnode of behavior/eventfunctions. Its name must be flexScriptInterface, and it must have text data. The text data contains the FlexScript name of the class. The engine searches the library for classes with this node, and calls their bindInterface() method.

SimpleDataType

Like the ObjectDataType, add the following method to your class's C++ definition:

virtual void bindInterface() override;

We'll talk about implementation later. Now we need to tell the engine that it should make an interface object for this class. The engine looks at the node MAIN:/project/exec/globals/sdtinterfaces for interfaces to create. Your module just needs to add a node to this list with the following structure:

  • The name of the node is the getClassFactory() name of your class.
  • The text of the node is the interface name.
  • There is one subnode:
    • The name of the subnode is isCoupling.
    • The number value of the subnode reflects whether your class inherits SimpleDataType (0) or CouplingDataType (1)

Pointer-Based Helper Classes

There are some classes which are not based on an SDT or an ODT, like the Conveyor.SlugBuilder class. This interface is defined by the following object:

class SlugBuilderProperty
{
protected:
  TreeNode* conveyorNode = nullptr;
  const SlugBuilder& getSlugBuilder();
public:
  // methods and properties
  // ...
  static void bindInterface();
}

The important thing to remember is that this class has a static void method called bindInterface(), which takes no parameters. If you class has that, you can make a FlexScript interface for that class.

To get the engine to create the FlexScript interface object, you have to call the bindClass() method within another object's bindInterface() method. The Conveyor's bindInterface() method contains this code:

bindClassByName<SlugBuilderProperty>("Conveyor.SlugBuilder");

This method tells the compiler to create a FlexScript class called Conveyor.SlugBuilder, and to give it the interface defined by the C++ SlugBuilderProperty class. But more on that in the next section.

bindInterface()

Here's the bindInterface() code from the Conveyor class:

void Conveyor::bindInterface()
{
	bindParentClass("Object");
	bindDocumentationXMLPath("modules\\Conveyor\\help\\FlexScriptAPI\\Conveyor.xml");
	bindTypedPropertyByName<Motor>("motor", "Object", force_cast<void*>(&Conveyor::__getMotor), nullptr);
	bindMethod(estimateConveyTime, Conveyor, "double estimateConveyTime(Object origin, double n_a, Object dest, double n_a, double itemLength, int flags = 0)", BIND_METHOD_STATIC);
	bindMethod(sendItem, Conveyor, "void sendItem(Object item, Object dest)", BIND_METHOD_STATIC);
	bindTypedProperty(targetSpeed, double, &Conveyor::__getTargetSpeed, &Conveyor::__setTargetSpeed);
	bindTypedProperty(currentSpeed, double, &Conveyor::__getCurrentSpeed, nullptr);
	bindTypedProperty(defaultSpeed, double, &Conveyor::__getDefaultSpeed, nullptr);
	bindTypedProperty(acceleration, double, &Conveyor::__getAcceleration, nullptr);
	bindTypedProperty(deceleration, double, &Conveyor::__getDeceleration, nullptr);
	bindTypedProperty(width, double, &Conveyor::__getWidth, nullptr);
	bindTypedProperty(length, double, &Conveyor::__getSimLength, nullptr);
	bindNodeListArrayClassByName<ConveyorItems>("Conveyor.Items", "Conveyor.Item", true);
	bindTypedPropertyByName<ConveyorItems>("itemData", "Conveyor.Items", force_cast<void*>(&Conveyor::__getItemData), nullptr);
	bindClassByName<SlugBuilderProperty>("Conveyor.SlugBuilder");
	bindTypedPropertyByName<SlugBuilderProperty>("slugBuilder", "Conveyor.SlugBuilder", force_cast<void*>(&Conveyor::__getSlugBuilder), nullptr);
}

Here's a brief description of the bind calls in this code:

  • bindParentClass - This is how FlexScript interfaces can inherit other FlexScript classes. This give the Conveyor class access to Object methods and properties.
  • bindDocumentationXMLPath - This connects a FlexScript interface to specific help documentation.
  • bindTypedPropertyByName - This method adds a property to the interface. You provide the property name, the FlexScript name of the type it returns, and pointers to a getter (and, optionally, a setter) method. For simple properties, you can use the bindTypedProperty macro.
  • bindMethodByName - This method adds a method to the interface. Unfortunately, intellisense almost always gives this a syntax error, but it still builds. If you pass the static flag, be sure that the method you pass is in fact a static method.
    • The method signature should use only type names available in FlexScript. If the engine can't parse this expression, FlexSim will crash on start up.
  • bindNodeListArrayClassByName - This method creates a new class interface, based on a node list array. If you have a custom NodeListArray class, you can use this method to create an interface for this class.
  • bindClassByName - This method creates a new class interface, based on an arbitrary class. That class must have the static void bindInterface() method.
    • By default, classes bound with this method (and bindNodeListArrayByName) cannot be declared. You can only create a local variable for these classes using the var type. You can pass a flag that makes the type user accessible, meaning you can delcare a local variable of that type directly.

Here's the code for the Conveyor.SlugBuilder's bindInterface:

void Conveyor::SlugBuilderProperty::bindInterface()
{
	XS;
	bindCopyConstructor(&SlugBuilderProperty::construct);
	bindCopyAssigner(&SlugBuilderProperty::operator=);
	bindTypedProperty(conveyor, Conveyor, &SlugBuilderProperty::__getConveyor, nullptr);
	bindTypedProperty(isEnabled, int, &SlugBuilderProperty::__getEnabled, &SlugBuilderProperty::__setEnabled);
	bindTypedProperty(isClear, int, &SlugBuilderProperty::__getIsClear, nullptr);
	bindTypedProperty(state, int, &SlugBuilderProperty::__getState, nullptr);
	bindMethodByName<decltype(&SlugBuilderProperty::makeReady)>("makeReady", &SlugBuilderProperty::makeReady, "void makeReady()");
	bindMethodByName<decltype(&SlugBuilderProperty::release)>("release", &SlugBuilderProperty::release, "void release()");
	// bindMethodByName<decltype(&SlugBuilderProperty::addItem)>("addItem", &SlugBuilderProperty::addItem, "void addItem(Object item)");
	bindDocumentationXMLPath("modules\\Conveyor\\help\\FlexScriptAPI\\Conveyor.SlugBuilder.xml");
	XE;
}

Here's a description of the new bind calls in this code:

  • bindCopyConstructor - This is only needed for non-SDT/ODT classes. It allows you to copy the pointers you need from one instance of the class to the other (conveyorNode = other.conveyorNode).
  • bindCopyAssigner - This is only needed for non-SDT/ODT classes. It allows you to assign this variable to a var type in FlexSim.

There are a couple other bind___ methods that can be used in bindInterface:

  • bindBracketOperator - allows the FlexScript interface to use [] syntax.
  • bindCastOperator - allows this object to be cast to other types (usually TreeNode*)

As a final note, you should know that the engine binds classes in two passes. The first pass creates empty type shell for all the outer types (SDT/ODT). That makes the types available for use in method calls. If you are using bindClass or bindNodeListArrayClass, then you should bind those classes before binding properties and methods that deal with that class.

That should get you started. We'll make better documentation in the future. Good luck!

· 9
5 |100000

Up to 12 attachments (including images) can be used with a maximum of 23.8 MiB each and 47.7 MiB total.

Hao Zhou avatar image Hao Zhou commented ·

Thanks for the document! I really appreciate it!

I am trying to follow the instructions and create a test interface. I would like to create a IntVector class. It inherits from SimpleDataType. And it is pretty much a wrapper for std::vector<int>. And I am trying to implement two interface methods: one is called reset() that calls reset() static function in C++, the other is called pushBack() that calls pushBack() member function in C++. The header is as follows:

#pragma once

#include "FlexsimDefs.h"

#include<vector>

class IntVector : public SimpleDataType
{
public:
	IntVector()
		: _vector()
	{}

	IntVector(treenode holderNode)
		: _vector()
	{
		nodeaddsimpledata(holderNode, this, SDT_BIND_ON_CREATE);
	}

	virtual const char* getClassFactory(void) override { return "FaradayFuture::IntVector"; }
	virtual void bind() override;
	virtual char* toString(int verbose) override;
	virtual void bindInterface() override;

	void clear() { _vector.clear(); }
	void pushBack(const int &value) { _vector.push_back(value); }
	void popBack() { _vector.pop_back(); }
	int size() const { return _vector.size(); }
	int getValueAtIndex(const int &index);
	int getIndexByValue(const int &value);
	void removeValueAtIndex(const int &index);
	bool removeValue(const int &value);

	static void reset(treenode holderNode);

protected:
	std::vector<int> _vector;
};

Part of cpp file is as follows:

void IntVector::bind()
{
	bindStlContainer(_vector);
}

void IntVector::reset(treenode holderNode)
{
	if (getdatatype(holderNode) == DATATYPE_SIMPLE
		&& holderNode->objectAs(SimpleDataType)->getClassFactory() == "FaradayFuture::IntVector")
	{
		holderNode->objectAs(IntVector)->clear();
	}
	else
	{
		new IntVector(holderNode);
	}
}

void IntVector::bindInterface()
{
	bindParentClass("treenode");

	bindMethod(reset, IntVector, "void reset(treenode holderNode)", BIND_METHOD_STATIC);

	bindMethod(pushBack, IntVector, "void pushBack(int value)");
}

I also modified the tree by following the instruction, see image below:

The problems I have are as follows:

1. The code can compile. FlexSim opens properly without crashing in the beginning. That seems the interface is bind properly. When I create a new model, drag a Source object into the model, open a tree view for the object, insert a new node inside "labels" and designate it as so(), then I call the following code in script window

IntVector.reset(so())

Compiler console pops up and says "Could not resolve correct operator for construct operation. Left side type is Variant&, right type is void". Could you point out what is the reason for this?

2. I also create an API function as the old way by creating a function as follows:

visible void intVectorReset_nodefunction(FLEXSIMINTERFACE)
{
	treenode vectorHolder = param(1);
	IntVector::reset(vectorHolder);
}

Then I can the following code in script window

intVectorReset(so());

It create a IntVector object properly in FlexSim without any problem. The problem is when I call

so().is(IntVector)

It always returns 0. Somehow, it cannot be down-casted properly. What it the reason for this?

Thank you very much for your help.

Thanks, Hao

0 Likes 0 ·
1.png (6.9 KiB)
Jordan Johnson avatar image Jordan Johnson ♦♦ Hao Zhou commented ·

Here's my implementation of the IntVector class:

using namespace FlexSim;
class IntVector : public SimpleDataType
{
protected:
	std::vector<int> vector;
public:
	static IntVector* init(TreeNode* holder)
	{
		if (holder->dataType == DATA_SIMPLE) {
			if (strcmp(holder->objectAs(SimpleDataType)->getClassFactory(), "IntVectorDemo::IntVector") == 0) {
				return holder->objectAs(IntVector);
			}
		}
		clearcontents(holder);
		nodeaddsimpledata(holder, new IntVector(), 1);
		return holder->objectAs(IntVector);
	}
	int __getLength() const { return vector.size(); }
	__declspec(property(get = __getLength)) int length;
	void clear() { vector.clear(); }
	void pushBack(int value) { vector.push_back(value); }
	void popBack() { vector.pop_back(); }
	int getValue(int index) { return vector[index]; }
	void setValue(int index, int value) { vector[index] = value; }
	int& at(int index) { return vector[index]; }
	void removeIndex(int index) { vector.erase(vector.begin() + index); }
	void removeValue(int value) 
	{  
		vector.erase(
			std::remove_if(vector.begin(), vector.end(), [value](int i) { return i == value; }), 
			vector.end());
	}
	virtual bool isClassType(const char* className) override { return strcmp(className, getClassFactory()) == 0; }
	virtual void bind() override
	{
		__super::bind();
		bindStlContainer(vector);
	}
	virtual void bindInterface() override
	{
		bindMethod(init, IntVector, "IntVector init(treenode target)", BIND_METHOD_STATIC);
		bindTypedProperty(length, int, &IntVector::__getLength, nullptr);
		bindMethod(clear, IntVector, "void clear()");
		bindMethod(pushBack, IntVector, "void pushBack(int value)");
		bindMethod(popBack, IntVector, "void popBack()");
		bindMethod(getValue, IntVector, "int getValue(int index)");
		bindMethod(setValue, IntVector, "void setValue(int index, int value)");
		bindMethod(at, IntVector, "int& at(int index)");
		bindMethod(removeIndex, IntVector, "void removeIndex(int index)");
		bindMethod(removeValue, IntVector, "void removeValue(int value)");
	}
	virtual const char* getClassFactory(void) override { return "IntVectorDemo::IntVector"; }
};

There's a couple things I forgot about:

  • For SDT types (like this one), you need to override the bools isClassType(const char*) method.
  • SDT/ODT types are always pointers in C++, but FlexScript hides that
    • For this reason, you usually don't need to worry about constructors for these kinds of types

Here's a little test script for how to use this class:

treenode testNode = model().subnodes["Tools"].subnodes.assert("test");
IntVector vector;
if (testNode.is(IntVector))
	vector = testNode;
else
	vector = IntVector.init(testNode);
vector.clear();
for (int i = 1; i <= 100; i++)
	vector.pushBack(i);
vector.setValue(9, vector.getValue(8));
vector.popBack();
vector.removeValue(9);
vector.at(0) -= 1001;
return [vector.length, vector.at(2)];

I hope that helps you get on your way!

2 Likes 2 ·
Hao Zhou avatar image Hao Zhou Jordan Johnson ♦♦ commented ·

Hi Jordan,

Thanks for your sample code and detailed explanation. These are very helpfully. By overriding function isClassType(), it works properly now.

One question for function signature. For example, the following code works perfectly

void pushBack(int value) { _vector.push_back(value); }

But if I change the signature to be

void pushBack(const int &value) { _vector.push_back(value); }

FlexSim will throw exception as follows:

exception: FlexScript exception: /0 c: /testlink_instance i: /testlink_associated

What is the reason for this? Member function cannot be bound properly with this kind of signiture?

Thanks, Hao

0 Likes 0 ·
Show more comments
Hao Zhou avatar image Hao Zhou commented ·
@jordan.johnson

Thanks for the reply. I tried "void pushBack(int &value)", it also does not work. So the only signature that works is "void pushBack(int value)".

So I guess either I should change all my signatures to be consistent with FlexScript signatures. Or, if I want to keep my original signatures, I have to create a wrapper as follows:

private:
void pushBackBind(int value) { this->pushBack(value); }

Then, bind it with

bindMethodByName<decltype(&IntVector::pushBackBind)>("pushBack", &IntVector::pushBackBind, "void pushBackBind(int value)");

Another thing I also noticed is function signature should not have underline symbol "_". If I change signature to be "pushBack_Bind()" instead of "pushBackBind()", even though I can bind it and compile properly, FlexSim will crash in the very beginning. Engine version is 17.1 beta.

Thanks, Hao

0 Likes 0 ·
alan.zhang avatar image alan.zhang commented ·

This is good documentation and open a new world to extend FlexScript functionalities with user defined classes. This is really great!

@jordan.johnson, I have a few questions:

  • You used Conveyor as an example to illustrate dot syntax. If I looked at the FlexScript Class Reference in FlexSim user manual, there is no Conveyor class there. I tried to create a Straight Conveyor to test in a script window, I could not get the dot syntax work. The code is below:
Object c1 = node("MODEL:/Conveyor1");
pf(c1.length);pr();

Error message:

exception: FlexScript exception: Label property length retrieved on /Conveyor1. Label does not exist. at /0 c: /testlink_instance i: /testlink_associated

  • I was trying to add a property in the SnowProcessor example, called myDotSyntaxVar1, following the steps for ObjectDataType, I could not make it work. The code are below.

SnowProcessor.h

class SnowProcessor : public Processor
{
protected:
...
	// variables
	double myDotSyntaxVar1;
public:
...
	virtual void bindInterface() override;
	double __getMyDotSyntaxVar1() const { return myDotSyntaxVar1; }
	void __setMyDotSyntaxVar1(double val) { myDotSyntaxVar1 = val; }
};

SnowProcessor.cpp

void SnowProcessor::bindInterface()
{
	//bindParentClass("Object"); // This line cannot be compiled!
	bindDocumentationXMLPath("modules\\testmodule\\help\\FlexScriptAPI\\SnowProcessor.xml");
	bindTypedProperty(myDotSyntaxVar1, double, &SnowProcessor::__getMyDotSyntaxVar1, &SnowProcessor::__setMyDotSyntaxVar1);
}
  • bindParentClass is giving me "identifier not found" compiling error.

Could you please let me know anything wrong?

Thanks, Alan

0 Likes 0 ·
Jordan Johnson avatar image Jordan Johnson ♦♦ alan.zhang commented ·

I wrote this documentation with 17.1 in mind. bindParentClass() is a new method in SimpleDataType.h. As for the .length property, the Conveyor class is also new in 17.1. If you update FlexSim to 17.1, the help manual will have the documentation you're looking for there. The module SDK is still not correctly updated to 17.1, but I will let you know when we fix that issue.

1 Like 1 ·
alan.zhang avatar image alan.zhang Jordan Johnson ♦♦ commented ·

@jordan.Johnson

After update to 17.1, I can use Conveyor class and access the length without any issue using the following code:

Conveyor c1 = node("MODEL:/Conveyor1");
pd(c1.length);pr();

I also manually copied the files in program/system/include and the flexsim.lib file to my module directory. Now I can compile my module with bindParentClass("Object").

I tested in a sample model using following lines:

SnowProcessor p1 = node("MODEL:/SnowProcessor1");
pd(p1.myDotSyntaxVar1);pr();
p1.myDotSyntaxVar1 = 5;
pd(p1.myDotSyntaxVar1);pr();

It works great! Thanks very much for the help!

I am looking forward the update of the module SDK.

0 Likes 0 ·
alan.zhang avatar image alan.zhang commented ·

@jordan.johnson

Could you please show me how to bind NodeListArray?

In the SnowProcessor example, how can I get individual Snowflake defined as:

NodeListArray<Snowflake>::SdtSubNodeBindingType flakes;

I am trying to use bindNodeListArrayClassByName but not successful.

Thanks,

0 Likes 0 ·