TNI Core Interface
The TNI Core Interface is a protocol and associated functions which allows communication between Trainz and a third-party TNI plugin.
Contents |
DLL Development
All TNI plugin DLLs must be written in C/C++. Due to the code-signing requirements imposed by the TNI developer agreement, it is not possible to create TNI plugin DLLs in other languages. At the current time, all TNI plugin DLLs must compile under Microsoft Visual Studio 2013's subset of C++11. TNI DLLs may use the following external headers and techniques:
- The TrainzNativeInterface SDK headers, which provide the TNI Core Interface and other component interfaces.
- The C standard library allocators and utility functions.
- The C++ STL (Standard Template Library) containers, allocators, and utility functions.
TNI DLLs must not use the following external headers and techniques unless a specific exception is granted.
- Windows Platform SDK.
- File, network, permissions, process, or other "system access" or "external access" headers.
- Third-party DLLs.
- Third-pary source code which does not follow these requirements.
- Assembly language.
- Bytecode machines, JIT compilers, and other forms of state machine whose runtime characteristics cannot be determined in advance; i.e. programs which effectively treat input data as source code.
- The plugin may use C++ exceptions internally, but must not allow exceptions to be propagated back to the calling code.
- The plugin must not throw or attempt to catch native exceptions (win32 exceptions, posix signals, etc.)
The TNI developer may use whatever build pipeline they like during development. When submitting to N3V Games for final build and code-signing, only a single folder of source files is accepted- a vanilla project file will be provided by N3V Games engineers which includes these files, the TNI SDK, and nothing else.
DLL Entry Point
The TNI DLL must provide a standard entry point function:
//=============================================================================
// Name: TNIEntryPoint
// Desc: DLL Entry point. Creates a TNILibrary for the specified context.
// Parm: context - The context which we are being loaded into.
// Retn: TNILibrary* - A new library which represents this plugin in the
// specified context.
//=============================================================================
extern "C" TNI_ENTRYPOINT_DECL TNILibrary* TNIEntryPoint(TNIContext* context)
{
// If there are no other libraries yet, and we maintain shared state, then we should
// initialise the shared state here.
// ..
// Create a TNILibrary that Trainz can use to call our dispatch function.
TNILibraryInitData info;
info.m_libraryVersion = 1;
info.m_libraryCall = &MyLibraryCall;
info.m_shutdownCall = &MyLibraryShutdown;
TNIString* libraryName = TNIAllocString("MyTNIPlugin");
TNILibrary* library = TNIAllocLibrary(context, libraryName, info);
TNIRelease(libraryName);
// .. perform per-context initialisation here.
return library;
}
The TNIAllocLibrary() function usage above also configured some callback functions, which should look something like this:
//=============================================================================
// Name: MyLibraryCall
// Desc: Called by Trainz to perform one of the library function calls provided
// by this library.
// Parm: context - The context in which this library resides.
// Parm: library - The library object, typically created by TNIEntryPoint().
// Parm: functionName - The function to perform.
// Parm: params - The parameters to the function.
// Retn: TNIObject* - The return value from the function.
//=============================================================================
TNIObject* MyLibraryCall(TNIContext* context, const TNILibrary* library, const TNILabel* functionName, const TNIObject* params)
{
if (functionName == ...)
{
.. do something ..
return ...;
}
return nullptr;
}
//=============================================================================
// Name: MyLibraryShutdown
// Desc: Called when the specified library is being destroyed.
// Parm: context - The context in which the library was loaded.
// Parm: library - The library which is being destroyed. You may temporarily
// reference this library within this function, however on return you
// must leave the reference count as you found it.
//=============================================================================
void MyLibraryShutdown(TNIContext* context, TNILibrary* library)
{
// Perform any necessary library shutdown.
// ..
// If this is the last TNILibrary for this plugin and we maintain shared state, we should also
// release the shared state now.
// ..
}
TrainzNativeInterface.h
The TNI core interface is designed to abstract the passing of data between Trainz and the TNI plugin DLLs. All data is represented by an opaque polymorphic object basetype, TNIObject. The objects are reference counted, with simple reference-count rules:
- Ownership of function parameters is held by the caller throughout the function call. They may be retained by the callee if they are required to persist after the function returns. The callee is then responsible for ensuring that they are released at a later time. For mutable object types such as TNIArray, the caller should not modify the array after the callee returns, since it has no way to know if the callee has kept a reference to the array- instead, it should release its own reference to the array. Exceptions are permitted where shared access is considered beneficial, but these must be clearly documented at every use site.
- Ownership of function return values is passed to the caller. For mutable object types such as TNIArray, the callee should not keep its own reference to the return value since the caller could further modify the object.
- Care must be taken to avoid reference count cycles where two objects directly or indirectly reference each other (e.g. a->b->a or a->b->c->a) as this prevents the objects from ever being released, causing a memory leak.
The core interface attempts to be as safe as possible against common programming errors. Pointer parameters are checked to ensure that they are not null, and so on. However, it is not feasible to make any native-code program completely bullet proof. Typical programming mistakes such as use-after-free, use-before-initialisation, memory leaks, and so on will cause Trainz to crash and are considered serious bugs with the TNI plugin that triggered the crash. Plugins which show repeated stability issues will be withdrawn from circulation.
The following TNIObject-derived classes are provided by the TNI Core Interface:
TNIObject
TNIObject is the polymorphic base class for all the entire TNI object hierarchy. All TNIObjects are considered opaque types, are constructed via helper functions provided in the TrainzNativeInterface DLL, and are released with TNIRelease().
// ============================================================================
// TNI OBJECT TYPES
// ============================================================================
enum
{
TNI_CLASS_NONE = 0,
TNI_CLASS_OBJECT = 1,
TNI_CLASS_STRING = 2,
TNI_CLASS_ARRAY = 3,
TNI_CLASS_CONTEXT = 4,
TNI_CLASS_LIBRARY = 5,
TNI_CLASS_STREAM = 6,
TNI_CLASS_GRAPHICS = 7,
TNI_CLASS_VECTOR = 8,
TNI_CLASS_ASSETID = 9,
TNI_CLASS_LABEL = 10,
TNI_CLASS_BUFFER = 11,
TNI_CLASS_SOUP = 12,
};
// ============================================================================
// Name: TNIGetObjectClass
// Desc: Return the type of the specified object, as per the above enumeration
// values.
// Parm: object - the object to query. It is safe to pass a NULL value here.
// ============================================================================
uint32_t TNIGetObjectClass(const TNIObject* object);
// ============================================================================
// Name: TNIReference
// Desc: Add a reference to the specified object. The object will be released
// when the reference count drops to zero.
// Parm: object - The object to which we will add a reference. It is safe to
// pass a NULL value here.
// ============================================================================
void TNIReference(const TNIObject* object);
// ============================================================================
// Name: TNIRelease
// Desc: Remove a reference to the specified object. The object will be
// released when the reference count drops to zero. TNIRelease() should
// be called exactly once for each call to TNIReference() and once for
// any function return value (for example, from TNIAllocString().)
// Parm: object - The object from which we will remove a reference. It is safe
// to pass a NULL value here.
// ============================================================================
void TNIRelease(const TNIObject* object);
TNIString
TNIString provides a simple string class. The raw string data is zero-terminated in the usual C fashion, and is encoded as UTF-8. UTF-8 is a superset of ASCII, so ASCII strings can be stored directly in a TNIString with no conversion necessary.
// ============================================================================
// Name: TNIAllocString
// Desc: Allocates a TNIString given a zero-terminated UTF8 text buffer.
// Parm: utf8text - The UTF-8 text buffer. The contents of the buffer are
// copied into the TNIString. The function results are undefined if this
// parameter does not represent a valid UTF-8 encoded string.
// Retn: TNIString - The newly allocated TNIString.
// Note: You must eventually release the allocated string using TNIRelease().
// Note: If 'utf8text' is nullptr, the returned TNIString is an empty string.
// ============================================================================
TNIString* TNIAllocString(const char* utf8text);
// ============================================================================
// Name: TNIGetStringText
// Desc: Retrieves a pointer to the internal text buffer of the TNIString
// object. This is a read-only reference, and remains valid only while
// (1) you hold a reference to the TNIString, and (2) you do not modify
// the TNIString.
// Parm: string - The TNIString object to query.
// Retn: const char* - A temporary pointer to the internal string text, in
// utf8 encoding.
// Note: If 'string' is a nullptr, the return value is a pointer to an empty
// string.
// ============================================================================
const char* TNIGetStringText(const TNIString* string);
// ============================================================================
// Name: TNIGetStringSize
// Desc: Returns the size in bytes of the specified string.
// Parm: string - A TNIString object, or nullptr.
// Retn: size_t - The size in bytes of the text string. Note that this is the
// size of the UTF-8 encoded text data, not including zero termination,
// and not including any overheads. If the string is null or empty, this
// returns zero.
// ============================================================================
size_t TNIGetStringSize(const TNIString* string);
// ============================================================================
// Name: TNISetStringText
// Desc: Modifies the TNIString to a copy of the supplied UTF-8 encoded text
// buffer.
// Parm: string - The existing TNIString object to modify. Modifying the string
// invalidates any pointer previously returned by TNIGetStringText().
// Parm: utf8text - The UTF-8 text buffer. The contents of the buffer are copied
// into the TNIString. The function results are undefined if this
// parameter does not represent a valid UTF-8 encoded string.
// Note: If 'string' is a nullptr, this function does nothing.
// Note: If 'utf8text' is a nullptr, it is considered an empty string.
// ============================================================================
void TNISetStringText(TNIString* string, const char* utf8text);
// ============================================================================
// Name: TNIAppendStringText
// Desc: Appends the supplied UTF-8 encoded text buffer to the TNIString.
// Parm: string - The existing TNIString object to modify. Modifying the string
// invalidates any pointer previously returned by TNIGetStringText().
// Parm: utf8text - The UTF-8 text buffer. The contents of the buffer are copied
// into the TNIString. The function results are undefined if this
// parameter does not represent a valid UTF-8 encoded string.
// Note: If 'string' is a nullptr, this function does nothing.
// Note: If 'utf8text' is a nullptr, this function does nothing.
// ============================================================================
void TNIAppendStringText(TNIString* string, const char* utf8text);
// ============================================================================
// HELPER FUNCTIONS
// ============================================================================
// The "cast" functions do not follow the normal semantics of returning a
// referenced object. They return the same object that you pass in, with no
// additional references taken. You should discard the cast result and release
// the original object when complete.
const TNIString* TNICastString(const TNIObject* object);
TNILabel
TNILabel provide an immutable string class which is designed to provide an efficient enumeration. Only one TNILabel object can exist for any unique string, so these objects can be rapidly compared by pointer equality. It is intended that you will create one TNILabel object for each enumeration during initialisation, and will then use these objects rather than creating new objects at runtime.
// ============================================================================
// Name: TNIAllocLabel
// Desc: Returns a label object for the specified text. Only one label object
// can exist for a given label at any time- repeated calls to this
// function with the same text string will result in the same object
// being returned (although the refcount is increased with each call.)
// It is safe to rely on this property, so you can directly compare the
// returned pointer for equality or inequality.
// This is a relatively slow function; you should call it once to
// generate the label, then store the label for as long as you need to
// use it. You should not attempt to regenerate the same label for each
// usage.
// Parm: utf8text - The text which corresponds to the label. A null or empty
// string will result in a nullptr label; this is not considered an
// error.
// ============================================================================
TNILabel* TNIAllocLabel(const char* utf8text);
// ============================================================================
// Name: TNIGetLabelDebugText
// Desc: Returns a string representation of the label, for debugging purposes.
// Note that there is no guarantee that this representation matches the
// string passed to TNIAllocLabel, and it should not be machine parsed.
// Parm: label - The label to query.
// Retn: const char* - A utf8-encoded string representing the label for the
// purposes of debug logging. The returned string is not owned by the
// caller but references a buffer internal to the label object.
// If 'label' is nullptr, the result will be the empty string.
// ============================================================================
const char* TNIGetLabelDebugText(const TNILabel* label);
TNIAssetID
TNIAssetID encodes a KUID or other Asset ID. You should avoid making any assumptions about the meaning of the data encoded in an TNIAssetID, as the interpretation is subject to change without notice.
TNIArray
TNIArray provides an indexed element array containing TNIObjects. Space for the entire array is allocated when the size is set. Random access within the array is very fast.
TNISoup
TNISoup provides a key-value mapping, similar in concept to the class Soup and the config.txt file format. Keys are TNILabel objects, and values are TNIObjects.
TNIVector
TNIVector provides an immutable indexed element array containing 32-bit floating point values. Values for the entire vector are defined during construction. Random access within the vector is very fast.
TNIContext
TNIContext provides the operating context in which the TNI plugin is operating. Trainz supports multiple concurrent TNI Contexts, and DLLs are typically shared between contexts, so the plugin must correctly track which context any given object or function call is made in. The TNI plugin should not leak data between contexts, although it may share non-context-specific resources internally in a manner which is opaque to the parent context. Each TNIContext is likely to run in a different thread of execution, although this is not guaranteed. Any internal storage maintained by the TNI plugin should be keyed to its parent context to prevent race conditions and other threading concerns. Access to private shared state must be protected by appropriate thread-safe locks.
TNIContext objects are created by the host process, and cannot be created or destroyed by TNI plugins.
// ============================================================================
// Name: TNIGetContextVersionCode
// Desc: Returns the Trainz Version code for this context.
// Parm: context - The context to query.
// Retn: uint32_t - The version code.
// ============================================================================
uint32_t TNIGetContextVersionCode(const TNIContext* context);
// ============================================================================
// Name: TNIGetContextLibrary
// Desc: Looks up a library in the specified context by name.
// Parm: context - The context to query.
// Parm: libraryName - The unique name of the library to return.
// Retn: TNILibrary* - The located library object, or NULL on failure.
// ============================================================================
TNILibrary* TNIGetContextLibrary(const TNIContext* context, const TNIString* libraryName);
All TNI libraries within the same context are available to each other by name. This allows plugins to reference each other. Care must be taken to avoid reference count cycles when accessing a library in this fashion- there should always be a clear "parent" and "child" relationship between any two libraries, with the parent referencing the child and the child never referencing the parent.
TNILibrary
The TNI plugin DLL entry point is expected to construct a TNILibrary descriptor and return it to Trainz. This serves as the public interface to the plugin, and may be accessed by Trainz native code, TrainzScript, and other TNI plugins. TNILibrary objects are context-specific, so multiple TNILibrary objects may be created for a single plugin DLL.
The plugin DLL may be unloaded at any point while there are no TNILibrary objects in existence for that plugin. The plugin should create any shared state when the first TNILibrary object is created, and release any shared state when the last TNILibrary object has is released.
// ============================================================================
// Name: TNIAllocLibrary
// Desc: Allocate a new library in the specified context, given a unique
// library name and the initialisation data.
// Parm: context - The context in which this library is being created.
// Parm: libraryName - The name of this library, which frequently matches the
// name of the plugin in which it is implemented (although it is possible
// for a single plugin to export multiple libraries). Each library
// created in a given context must have a unique name.
// Parm: libraryInitData - A TNILibraryInitData structure which describes
// the library to be created.
// Retn: TNILibrary* - A valid object on success, or NULL on failure.
// ============================================================================
TNILibrary* TNIAllocLibrary(TNIContext* context, const TNIString* libraryName, const TNILibraryInitData& libraryInitData);
// ============================================================================
// Name: TNIGetLibraryVersion
// Desc: Returns the developer-specified version for the given library.
// Parm: library - The library to query.
// Retn: uint32_t - the developer-specified version for the given library.
// Returns zero on failure.
// ============================================================================
uint32_t TNIGetLibraryVersion(const TNILibrary* library);
// ============================================================================
// Name: TNICallLibraryFunction
// Desc: Calls into the specified library's LibraryCall function.
// Parm: library - The library on which to make the call.
// Parm: functionName - The name of the function to call. Must be a non-empty
// string.
// Parm: params - NULL, or a TNIObject object.
// Retn: TNIObject* - The return value from the LibraryCall function, which may
// be NULL or a valid TNIObject. Returns NULL on failure. It is the
// caller's responsibility to release the returned object.
// ============================================================================
TNIObject* TNICallLibraryFunction(const TNILibrary* library, const TNILabel* functionName, const TNIObject* params);
TNIBuffer
TNIBuffer provides untyped binary data storage. This is used in scenarios where large amounts of complex-type data needs to be rapidly created and passed between systems. This should be considered a last-resort mechanism, used only when the cost of the other object types is prohibitive.
TNIStream
TNIStream provides a mechanism for serialising both data and TNIObjects into a "command buffer" which can be passed to another system. This is used in scenarios where large numbers of commands must be rapidly packed and later replayed. The writer and reader must agree on the stream format- the reader must make the appropriate calls to match what the writer used when constructing the stream. It is strongly recommended that some form of version negotiation is used to allow future changes to any streamed formats. TNIObjects are written to a TNIStream by reference- the TNIObjects themselves are not serialised or copied.
This should be considered a last-resort mechanism, used only where the cost of the other object types is prohibitive.
Threading
Trainz is a preemptively multithreaded environment, which means that TNI functions need to be safe against certain thread usage. There are certain guarantees and limitations which are relevant to TNI development:
- For any given TNIContext, only one thread will be making calls into TNI DLLs at a time. There is no guarantee that it will always be the same thread, only that multiple threads won't do it simultaneously. Because of this, don't use thread_local storage.
- For any given TNI Plugin, multiple threads may call into your TNI DLL functions simultaneously on behalf of different TNIContexts. If you use global variables or file-scope static variables, you must protect them with appropriate thread-safe locks.
- Never use function-scope static variables, as they are not thread-safe on Windows.
- TNIObjects may be used on any thread.
- TNIObjects may be read by multiple threads simultaneously.
- TNIObjects must not be accessed on any thread while being modified on another thread. If you are in doubt as to whether a particular function may modify the state of a TNIObject, you should assume that it does.
- TNIReference() and TNIRelease() modify the state of the TNIObject.
- You may create your own worker threads using the TNI threading API, but you may not make TNILibrary or TNIContext calls from those threads.
- You may not create threads using other APIs.