libsgfc++ 2.0.1
A C++ library that uses SGFC to read and write SGF (Smart Game Format) data.
Loading...
Searching...
No Matches
Library design and working principles

Public API vs. private implementation

The library has been designed with a strict separation of the library's public API from the library's private implementation.

All public types are one of the following:

  • An interface, declared as an abstract class because C++ does not have an interface keyword.
  • A type with no behaviour, such as a simple struct data type, an enumeration or a typedef.
  • A static-declared factory class SgfcPlusPlusFactory.

The library-internal types that provide the implementations for the public interfaces are marked with a declaration that explicitly sets symbol visibility of these types to "hidden". The goal is that the library client never instantiates library objects on its own, but instead uses the top-level factory class SgfcPlusPlusFactory to do so.

Memory management

The library attempts to protect the library client from having to perform any sort of memory management by making use of std::shared_ptr. This smart pointer type has been introduced to the C++ standard in C++11, and if you don't know it yet then you should be quick to learn more about it.

The goal is, essentially, that the library client can pass around references to library objects without having to constantly think about pointer/reference mechanics and who is responsible for destroying those objects. For example, after the library client has read an SGF file and has obtained a reference to the resulting ISgfcDocument object, the entire object tree that represents the game trees in the SGF file can be safely passed around using the ISgfcDocument reference. Once the library client removes the last reference to the ISgfcDocument object, the entire object tree will be safely deallocated.

In a few places where it is necessary to break reference cycles the library uses std::weak_ptr instead of std::shared_ptr.

A number of interfaces contain convenience methods that help with downcasting the interface type to a more specific sub-type. These methods are named To...() and return a raw pointer without an encapsulating std::shared_ptr. The caller of any these methods is not the owner of the returned object and must never destroy the returned object. Example: ToNumberValue() and many other similar methods in ISgfcSinglePropertyValue.

Concurrency

Using the factories concurrently is safe.

A library client can also safely perform concurrent load or save operations using different document reader/writer objects and document object trees. The actual SGFC backend operations are serialized by libsgfc++, though, because SGFC uses a single global hook/callback function for sending message data back to libsgfc++. Serializing the operations makes sure that an operation on thread A does not receive messages generated by an operaton on thread B, or vice versa. Serialization is implemented in the SgfcBackendController class using a central std::mutex.

No other precautions have been taken to make libsgfc++ thread-safe. If the library client wants to operate on the same document object tree in different threads then it must make sure that access is properly synchronized.

Operation modes

The library has abstracted SGFC's functionality into 3 interfaces which can be seen to represent the library's main operation modes:

  1. Command line mode. The interface for this is ISgfcCommandLine. In this mode the library mimics SGFC's command line usage. This mode gives the library client no access to the actual SGF data. In this mode the library client can do the following:
    • The library client specifies the path to a single .sgf file, or an in-memory string buffer with SGF data. The file or in-memory string buffer can have any of the formats FF1 - FF4. The library instructs SGFC to load the SGF data from the file or in-memory string buffer. The library retains the unaltered SGFC-internal data structures.
    • The library client accesses the messages generated by SGFC during parsing.
    • The library client optionally specifies the path to a single .sgf file, or it specifies an in-memory string buffer. The library instructs SGFC to write the SGF data in the unaltered SGFC-internal data structures to the specified file or in-memory string buffer. The file or in-memory string buffer has the FF4 format.
  2. Read data mode. The interface for this is ISgfcDocumentReader. The library uses SGFC for reading SGF data from a source specified by the library client, and makes that data available to the client for further processing. In this mode the library client can do the following:
    • The library client specifies the path to a single .sgf file, or an in-memory string buffer with SGF data. The file or in-memory string buffer can have any of the formats FF1 - FF4. The library instructs SGFC to load the SGF data from the file or in-memory string buffer. The library transforms the SGFC-internal data structures into its own library-specific data structures that conform to FF4. This may include alterations that cannot be reversed, i.e. it may be impossible to transform the library-specific data structures back into SGFC-internal data structures that are exactly the same as the original SGFC-internal data structures.
    • The library client accesses the messages generated by SGFC during parsing.
    • The library client accesses the SGF data in the form of library-specific data structures (ISgfcDocument).
  3. Save data mode. The interface for this is ISgfcDocumentWriter. The library uses SGFC for writing SGF data provided by the library client to a destination also specified by the library client. In this mode the library client can do the following:
    • The client specifies the SGF data in the form of library-specific data structures (ISgfcDocument).
    • The client specifies the path to a single .sgf file, or it specifies an in-memory string buffer. The library transforms the library-specific data structures into SGFC-internal data structures. The library instructs SGFC to write the SGF data in the SGFC-internal data structures to the specified file or in-memory string buffer. The file or in-memory string buffer has the FF4 format.

In all of these modes the library client can specify arguments that indicate how SGFC should perform the read and/or write operations. The available arguments are a subset of the SGFC command line arguments. Certain arguments (e.g. --help, -i) are not available because they do not make sense in a library context, or because the library does not support them.

Error handling

SGFC messages

When SGFC reads or writes SGF data it often generates one or more messages. Every message has an ID, and every message ID is an indicator for what the message is about. libsgfc++ defines the enumeration SgfcMessageID with all possible message IDs accompanied by sparse documentation. To see the full documentation consult the SGFC documentation where all messages are listed with their IDs and meanings.

The library processes the message data generated by SGFC and makes it available to the library client in the form of a collection of ISgfcMessage objects. These objects provide the data in a structured form so that the library client can programmatically evaluate the message content and possibly handle certain warnings or errors.

Exceptions

Yes, the library does throw exceptions! The API documentation mentions all exceptions that can be thrown. The library client is responsible to handle the documented exceptions, or face the consequences. Any exceptions that are thrown are thrown by value so the library client can catch them by reference (e.g. catch (std::invalid_argument& e)).

The library throws certain exceptions when the library client makes mistakes using the library API. Examples:

  • The library client creates an ISgfcCommandLine object with invalid arguments and calls its LoadSgfFile method without checking first whether the arguments were valid.
  • The library client specifies nullptr to SgfcPlusPlusFactory::CreateDocument.

The library may also throw std::domain_error when an unexpected SGFC interfacing problem occurs. Although this may not be the fault of the library client, the design goal here is to fail fast and hard to find out about such problems sooner rather than later. If you encounter such a problem don't hesitate to file a bug report!

Full disclosure: The library can throw std::logic_error when a library coding error occurs (e.g. an enumeration value was forgotten in a switch statement). Obviously this should never happen, unit tests should have caught such problems before they library was released, etc. etc. Again, the design goal is to find these problems and not sweep them under the rug. Apologies in advance if it does happen, and please file a bug report.

Document concept

When the library reads SGF data from a source, it makes that data available as an ISgfcDocument. ISgfcDocument objects can also be built programmatically from scratch using the library's factories.

Currently such a document is equivalent to what the SGF standard shows as a "collection of game trees" in its EBNF definition. In a future version of the library the document concept might be expanded. For instance it might be possible to add meta data to a document.

Documents can be written back as SGF data to a destination. Documents can also be validated by simulating a write operation - this can be useful for ISgfcDocument objects that were built programmatically.

Virtual inheritance

Because C++ does not have a dedicated interface feature, the library implementation is forced to employ multiple inheritance. The interface class ISgfcSinglePropertyValue is inherited multiple times in all typed property value classes (e.g. SgfcColorPropertyValue):

  • Once via the SgfcSinglePropertyValue base class
  • Once via the typed interface class (e.g. ISgfcColorPropertyValue)

To make this work all sub-classes of ISgfcSinglePropertyValue have to use virtual inheritance. This makes sure that objects contain only one shared ISgfcSinglePropertyValue instance.

Without virtual inheritance a class such as SgfcColorPropertyValue cannot be instantiated because the compiler sees it as abstract, because SgfcColorPropertyValue does not implement all pure virtual methods that it inherits via the ISgfcColorPropertyValue base class.

Other classes that also have to use virtual inheritance due to the same reasons:

  • Subclasses of ISgfcProperty (because of typed property classes such as SgfcGameTypeProperty)
  • Subclasses of ISgfcMovePropertyValue(because of SgfcGoMovePropertyValue)
  • Subclasses of ISgfcPointPropertyValue(because of SgfcGoPointPropertyValue)
  • Subclasses of ISgfcStonePropertyValue(because of SgfcGoStonePropertyValue)

IMPORTANT: Where virtual inheritance is in play downcasting must be done with dynamic_cast or std::dynamic_pointer_cast, NOT with static_cast or std::static_pointer_cast. An example:

std::shared_ptr<ISgfcPropertyValue> propertyValue = [...] // get it from somewhere
[...] // somehow determine that it's a Number value
std::shared_ptr<ISgfcNumberPropertyValue> numberValueSharedPtr =
std::dynamic_pointer_cast<ISgfcNumberPropertyValue>(propertyValue);