Chapter 9: SystemC CCI

CCI Variant Types (cci_value)

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

Listen to this lessonAudiobook mode

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.

Standard and source context

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 cleanly 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.
Lesson self-check

Can you answer these clearly?

Keep moving when you can answer each question without looking back at the lesson.

Comments and Corrections