Removing sources from generated code
MPS bundles sources with generated models by default. Users of the models are thus able to navigate to definitions of the concepts that they are using in their code. It is a matter of a singleControl + click for them to see the implementation of a method that they are calling or a class they are instantiating, for example. This is very convenient for the users, since they can grasp many of the ideas of the language/library authors simply by peeking at the implementation side.
There may be situations in life when hiding the implementation could be desired, though. Especially closed-source projects need to protect carefully their intellectual property contained in the implementation. Their users should still be able to call the code, but after pressing Control + B they only get to see the class and method signatures:
Combine that with obfuscated class files and chances for someone reverse-engineering the fruits of your hard work are pretty low. Please read on to find out how to do this.
The BuildLanguage offers a strip implementation flag to indicate that a particular artifact should have the sources removed.
Setting this flag to true on one of the build layout commands (module, sources of, plugin) will instruct the build process to remove the sources of implementation from the generated artifacts. This flag will ensure that BaseLanguage methods have their bodies replaced with an empty StatementList and classes have static and instance initializers removed. May you wish to hide implementation of a Language, the flag will ensure the behavior methods have their bodies also replaced with an empty StatementList.
To summarize, MPS supports out of the box:
Hiding your implementations written in BaseLanguage
Hiding aspects of your language definitions
You only need to set the strip implementation flag in your build scripts for the desired solution or a language.
The ability to Invalidate caches in MPS may come handy when switching between projects containing stripped and not-stripped versions of your language.
Sometimes it may be necessary to manually introduce changes in your language so that the build script reflects the changes in the generated artifacts.
Just as BaseLanguage code allows for hiding implementation, your languages can allow for implementation hiding, as well.
MPS gives you three marker interfaces to demarcate the intended behavior of the concepts of your language with respect to implementation stripping:
InterfacePart - concepts that are fully visible in the generated models. Users will be able to navigate to them and keep references to them in their code.
ImplementationPart - concepts that get removed from the generated models. Users will not be able to navigate to them or keep references to them in their code.
ImplementationWithStubPart - concepts that get replaced with empty stubs. It behaves like ImplementationPart except that a stub replacement will be used to represent the nodes in the code. Think of /* compiled code */ marks for empty method bodies, for example.
If, for example, we wanted to the Robot Kaja sample language (bundled with MPS) to allow its usages to hide their implementation, we can achieve that in a few steps. Let's assume the following scenario:
An end user is writing scripts as part of a SampleRobotScripts project in the Robot Kaja language. He wants to download and reuse a library called RobotRoutines (MPS solution) of robot routines.
The author of RobotRoutines wants to hide the implementation of her library.
- The Robot Kaja language needs to be modified to support hiding implementation of RobotRoutines. It needs to declare, which of its concepts form the public interface (contract) of applications/libraries written in that language and which hold the implementation. We picked a simple language, so it is pretty straightforward to identify that only three concepts need really to be made part of the interface. The others can have their sources removed during packaging.
The Robot Kaja language allows Libraries of routines to be created. Library is a root concept and holds a collection or RoutineDefinitions. The RobotRoutines library that uses the Robot Kaja language may create several Library root nodes and implement a few handy routines in them.
When used in the SampleRobotScripts code, the developer can always navigate to definition and see the full implementation of the RobotRoutines library.
We want to offer the authors of libraries such as RobotRoutines the ability to hide the implementation. Once implemented properly, the developers of SampleRobotScripts would only see the routines signatures and their empty bodies:
The Robot Kaja language needs to have its Script, Library and RoutineDefinition concepts marked with the InterfacePart interface, because user code can refer to them.
To hide the implementation of routines we need to hide their bodies. The routine body is a CommandList concept. To hide it we have to mark it with either the ImplementationPart interface or the ImplementationWithStubPart interface. They both result in hiding the implementation, however, the latter lets you provide a replacement "stub" concept, that will be inserted instead of the removed implementation. The stub gives you the chance to provide a nicer look of the code with removed implementation and should always be considered for hidden implementation that gets occasionally seen by the end user.
The overhead of ImplementationWithStubPart over ImplementationPart is in creating the stub concept. For our CommandList we'll need a StubCommandList concept and mark it with IDontSubstituteByDefault and IStubForAnotherConcept. The IStubForAnotherConcept is needed since StubCommandList inherits the ImplementationWithStubPart interface from CommandList, and thus MPS needs to be told explicitly that StubCommandList is a stub itself and does not need to be stubbed by yet another stub.
If StubCommandList was extending AbstractCommand instead of CommandList, it would not have to be marked as IStubForAnotherConcept as AbstractCommand is not an ImplementationWithStubPart.
The stub editor should make it politely obvious to the reader that the implementation has been removed and is not to be seen here.
Upon rebuild and packaging the language is ready to help the author of RobotRoutines to hide the implementation of her library.
Once the Robot Kaja language supports implementation stripping, the author of the RobotRoutines library can set the strip implementation flag in her build script and thus have the sources of the implementation of her library removed from the generated plugin.
The marker interfaces are inherited from super-concepts and super-concept-interfaces in a traditional way. In case multiple marker interfaces are applicable to a concept (directly or through inheritance), InterfacePart wins over others and ImplementationWithStubPart wins over ImplementationPart.
- If none of the marker interface is specified a concept behaves as if the InterfacePart was set on it. Marking a concept with InterfacePart serves two purposes:
Documenting the fact that a concept is a necessary public element of your language
Preventing the concept from being accidentally marked as part of implementation, if, for example, it inherits any of the other flags.
Use the ImplementationPart interface to mark the concepts of your language that need not to be referred from client code nor are directly accessible from InterfacePart concepts through mandatory (cardinality 1 and 1..n) links. Sources of these concepts will be removed from the solution during build and so the user will not be able to see their definitions, for example using Go To Concept Declaration.
Use ImplementationWithStubPart interface to mark the concepts that should be removed just like with ImplementationPart, but which need to be replaced with a place-holder instead of being simply removed from the sources, because the user may get to see them as part of definition of an InterfacePart concept.
ImplementationWithStubPart is typically needed for concepts that represent children or target of references of cardinality 1 and 1..n pointing from InterfacePart concepts, because their containing links cannot remain empty and would report a validation error.
Children and targets of references with cardinality of 0..1 and 0..n can safely be marked as ImplementationPart. The containing links will remain empty.
Stubs should follow the naming convention Stub + name of the concept being replaced and must be located in the same package with the replaced concept.
Stubs concepts could implement ISuppressErrors to avoid type-system errors being reported from their child nodes.
Stubs should also implement IDontSubstituteByDefault so that they are not offered in code-completion menu.
MPS will report a warning if a concept is both InterfacePart and Implementation(WithStub)Part.
MPS will report an error if a concept declares or inherits the ImplementationWithStubPart interface and no suitable stub concept can be found in the same virtual package.
Stubs that inherit the ImplementationWithStubPart interface, perhaps by extending the stubbed concept, need to implement IStubForAnotherConcept to indicate that they are stubs and thus do not need to be stubbed themselves. It is a good strategy to have all stubs implement the IStubForAnotherConcept interface.
Stubs cannot be defined to replace abstract super-concepts. They must always replace all concrete concept individually, one stub for one concrete concept implementing ImplementationWithStubPart.
Redefine the editors of the stub concepts so that they show some clear messages, aka "compiled code" to clearly indicate to the reader that the implementation has been removed.
To avoid model validation errors that the users of stripped languages would see in stubbed concepts, consider "specializing" of all children and references with "at least 1" cardinality inherited into the stub concepts from their super-concepts.
This is needed for concepts, such as the one below - the modelAccessor child has cardinality 1 and so is mandatory in the stubbed concept.
The stub concept has to follow the naming convention of prepending Stub to the stubbed concept's name. It may or may not need to extend the stubbed concept - this depends on how the original concept is being referred to from the rest of the language. If the stub concept has to extend the stubbed concept, it also needs to somehow treat the mandatory child so that the child reference does not stay empty.
The suggested way is to specialize the child or reference relationship and change the target concept to BaseConcept.
In the behavior aspect in the constructor we then set the reference to point to some dummy node, such as an IntegerConstant. This will ensure the link does not stay empty, yet we avoid the need of creating nodes fully satisfying the original modelAccessor link in the stubbed concept.