Chapter 9: SystemC CCI

CCI Variant Types (cci_value)

Exploring the cci_value class for dynamically-typed configuration, JSON serialization, and user-defined converters.

How to Read This Lesson

CCI is about making configuration explicit and inspectable. Read every parameter as part of the platform contract, not just a convenient variable.

CCI Variant Types (cci_value)

To create a truly interoperable configuration API, SystemC CCI needs to pass configuration values between arbitrary models, external tools, and text-based parsers without knowing the specific C++ types at compile-time.

To achieve this, the IEEE 1666.1 standard relies heavily on the cci::cci_value class—a variant type capable of holding arbitrarily complex, dynamically typed configuration data.

Source and LRM Trail

For CCI, start with Docs/LRMs/SystemC_CCI_1_0_LRM.pdf. Then inspect .codex-src/cci/configuration/src/cci for cci_param, cci_broker_if, cci_value, originators, callbacks, and the consuming broker. The practical question is always: who owns this value, when may it change, and how can tools inspect it?

The cci_value_category

A cci_value object always has a category (cci::cci_value_category) defining the type of data it currently holds. The base categories act as building blocks:

  • cci::CCI_NULL_VALUE - Uninitialized or explicitly null.
  • cci::CCI_BOOL_VALUE - Boolean.
  • cci::CCI_INTEGRAL_VALUE - Integers (up to 64-bit).
  • cci::CCI_REAL_VALUE - Floating-point numbers.
  • cci::CCI_STRING_VALUE - Text strings.
  • cci::CCI_LIST_VALUE - A heterogeneous array of cci_value objects.
  • cci::CCI_DICT_VALUE (Map) - A dictionary of string keys mapping to cci_value objects.

User-Defined Types

CCI seamlessly handles basic C++ types. But what if you have a custom struct representing a coordinate, a MAC address, or an exact packet header that you want to configure as a single parameter?

To support this, you must define a template specialization of cci::cci_value_converter<T>. This converter explicitly tells the CCI library how to pack your C++ struct into a variant cci_value (usually a map/dictionary) and how to unpack it back out. Once defined, your custom type can be serialized to JSON and managed by brokers exactly like native integers or strings.

Under the Hood: The cci_value AST and JSON Engine

In the Accellera reference implementation, cci_value is effectively an Abstract Syntax Tree (AST) node for configuration data. Internally, a cci_value object contains a tagged union (or pointer-to-implementation structure) that stores the actual payload alongside its cci_value_category enum. For primitive types like integers and booleans, the value is stored inline. For complex types like lists and maps, it stores pointers to heap-allocated std::vector<cci_value> or std::map<std::string, cci_value>.

When you call cci_value::to_json() or cci_value::from_json(), the Accellera reference implementation historically embeds a lightweight, high-performance JSON library (like RapidJSON) under the hood. The to_json method recursively traverses the cci_value AST, stringifying the tags according to strict JSON syntax.

The cci_value_converter<T> works via compile-time template instantiation. When you call param.set_value(my_struct), the C++ compiler resolves to your explicit pack() template specialization, actively marshalling the struct's fields into the dynamically allocated cci_value dictionary AST before passing it to the broker. This isolates the expensive string manipulation/JSON operations exclusively to external tooling, while internal parameter accesses compile down to native C++ assignments.

Complete Example: Values, JSON, and Converters

The following end-to-end example demonstrates how to:

  1. Define a custom configuration struct.
  2. Provide a cci_value_converter to pack and unpack it.
  3. Instantiate a CCI parameter using this custom type.
  4. Interact with cci_value dictionaries and JSON serialization.
#include <systemc>
#include <cci_configuration>
#include <iostream>
#include <string>
 
// 1. A custom user-defined type
struct NetworkConfig {
    std::string ip_address;
    int port;
};
 
// 2. Specialize the converter in the cci namespace
namespace cci {
    template<>
    struct cci_value_converter<NetworkConfig> {
        typedef NetworkConfig type;
 
        // Pack the struct into a cci_value map
        static bool pack(cci_value::reference dst, const type& src) {
            dst.set_map()
               .push_entry("ip", cci_value(src.ip_address))
               .push_entry("port", cci_value(src.port));
            return true;
        }
 
        // Unpack a cci_value map back into the struct
        static bool unpack(type& dst, cci_value::const_reference src) {
            if (!src.is_map()) return false;
            
            cci_value::const_map_reference m = src.get_map();
            if (!m.has_entry("ip") || !m.has_entry("port")) return false;
            
            // Note: CCI does NOT implicitly convert strings to ints. 
            // We must explicitly extract the right types.
            if (!m.at("ip").is_string() || !m.at("port").is_int()) return false;
 
            dst.ip_address = m.at("ip").get_string();
            dst.port = m.at("port").get_int();
            return true;
        }
    };
}
 
// 3. A module using the custom parameter
class Router : public sc_core::sc_module {
public:
    cci::cci_param<NetworkConfig> net_config;
 
    SC_HAS_PROCESS(Router);
    Router(sc_core::sc_module_name name)
        : sc_core::sc_module(name)
        , net_config("net_config", {"192.168.1.1", 8080}) 
    {
        SC_METHOD(print_status);
    }
 
    void print_status() {
        NetworkConfig cfg = net_config.get_value();
        std::cout << "[Router] Started on IP: " << cfg.ip_address 
                  << " Port: " << cfg.port << "\n";
    }
};
 
int sc_main(int argc, char* argv[]) {
    // Register the global broker
    cci::cci_register_broker(new cci_utils::consuming_broker("Global_Broker"));
    cci::cci_broker_handle broker = cci::cci_get_broker();
 
    // 4. Working with cci_value natively
    // We can manually construct a cci_value dictionary (map) to use as a preset
    cci::cci_value preset_val;
    cci::cci_value::map_reference vmap = preset_val.set_map();
    vmap.push_entry("ip", cci::cci_value("10.0.0.1"));
    vmap.push_entry("port", cci::cci_value(9000));
    
    // Set the preset
    broker.set_preset_cci_value("router.net_config", preset_val);
 
    // Instantiate module
    Router router("router");
 
    // 5. JSON Serialization
    // Because we provided the converter, the CCI parameter can automatically 
    // export its custom type as a JSON string!
    cci::cci_value current_val = router.net_config.get_cci_value();
    std::string json_str = current_val.to_json();
    
    std::cout << "--- JSON Serialization ---\n";
    std::cout << "Router Configuration as JSON: " << json_str << "\n\n";
 
    // 6. JSON Deserialization
    // We can also parse JSON directly back into a cci_value, and apply it.
    std::string new_json = "{\"ip\": \"127.0.0.1\", \"port\": 443}";
    cci::cci_value parsed_val = cci::cci_value::from_json(new_json);
    
    std::cout << "--- Applying new config via JSON ---\n";
    router.net_config.set_cci_value(parsed_val);
 
    // Start simulation to see the updated output
    sc_core::sc_start(1, sc_core::SC_NS);
 
    return 0;
}

Key Takeaways from the LRM

  • Strict Type Checking: If you call .get_int() on a cci_value holding a string (e.g., "123"), the system will throw an exception. The standard explicitly prohibits implicit type coercion. Always check with .is_int(), .is_string(), etc., before extracting.
  • Heterogeneous Lists: A cci_value_list acts conceptually like an std::vector<cci_value>. A single list can legally contain an integer in index 0, a string in index 1, and a nested map in index 2.
  • Seamless Tooling: By defining a cci_value_converter, custom C++ objects instantly gain JSON serialization capabilities and can be interacted with via external tools completely natively.

Comments and Corrections