Modernization project

ASN1C generated code, modern C++26 experience.

A safe modernization path: keep Objective Systems generated code regeneratable, then expose it through a C++26-style facade with RAII, spans, expected results, tests, and benchmarks.

RAIIstd::spanstd::expectedgolden vectors
Why not rewrite generated files?

The generated code is a boundary, not the product API.

Risk

Manual edits vanish on regeneration and can break vendor support or ASN.1 interoperability.

Reality

Generated code often uses runtime-specific ownership and context patterns that should remain stable.

Solution

Modernize around it: wrappers, adapters, tests, build flags, and diagnostics.

Target architecture

A modern facade over a vendor core.

ASN.1 schema
.asn
Objective Systems
ASN1C output
C++26 facade
app-safe API

The application depends on the facade, not directly on generated internals.

Before

Typical generated-code integration leaks details everywhere.

OSCTXT ctxt;
int ret = rtInitContext(&ctxt);
if (ret != 0) {
    return ret;
}

MyMessage msg;
asn1Init_MyMessage(&msg);

ret = xd_setp(&ctxt, buffer, bufferLen, 0, 0);
if (ret == 0) {
    ret = asn1PD_MyMessage(&ctxt, &msg);
}

if (ret != 0) {
    rtxErrPrint(&ctxt);
}

rtFreeContext(&ctxt);
return ret;

Manual cleanup, raw status codes, runtime details, and error reporting are repeated in every caller.

After

The application sees a small, modern contract.

#include <expected>
#include <span>
#include <vector>

namespace asn1c_modern {

enum class ErrorCode {
    invalid_argument,
    decode_failed,
    encode_failed,
    buffer_too_small
};

struct Error {
    ErrorCode code;
    std::string message;
};

template <class T>
using Result = std::expected<T, Error>;

class MyMessageCodec final {
public:
    [[nodiscard]] Result<MyMessage>
    decode(std::span<const std::byte> input) const;

    [[nodiscard]] Result<std::vector<std::byte>>
    encode(const MyMessage& message) const;
};

}
RAII

Context lifetime becomes automatic.

class Context final {
public:
    Context() {
        if (rtInitContext(&ctx_) != 0) {
            throw ContextInitError{};
        }
    }

    ~Context() noexcept {
        rtFreeContext(&ctx_);
    }

    Context(const Context&) = delete;
    Context& operator=(const Context&) = delete;

    [[nodiscard]] OSCTXT* get() noexcept { return &ctx_; }
    [[nodiscard]] std::string error_string() const;

private:
    OSCTXT ctx_{};
};

Every early return is safe. Cleanup is no longer a caller responsibility.

Implementation sample

Modern API, vendor calls inside.

Result<MyMessage> MyMessageCodec::decode(
    std::span<const std::byte> input) const
{
    if (input.empty()) {
        return std::unexpected(Error{
            ErrorCode::invalid_argument,
            "empty input buffer"
        });
    }

    Context ctx;
    MyMessage message{};
    asn1Init_MyMessage(&message);

    auto* bytes = reinterpret_cast<const OSOCTET*>(input.data());
    if (xd_setp(ctx.get(), bytes, input.size(), 0, 0) != 0) {
        return std::unexpected(Error{ErrorCode::decode_failed, ctx.error_string()});
    }

    if (asn1PD_MyMessage(ctx.get(), &message) != 0) {
        return std::unexpected(Error{ErrorCode::decode_failed, ctx.error_string()});
    }

    return message;
}

Exact Objective Systems function names vary by encoding rule and compiler options.

Type modernization

ASN.1 concepts map cleanly to modern C++.

Generated boundary

OPTIONAL, CHOICE, SEQUENCE OF, OCTET STRING, generated lists, runtime buffers.

Application boundary

std::optional, std::variant, std::vector, std::span<std::byte>, strong enums.

struct AppMessage {
    std::uint16_t version;
    std::array<std::byte, 16> transaction_id;
    std::variant<Request, Response, ErrorPayload> payload;
    std::optional<Extensions> extensions;
};

Result<AppMessage> from_generated(const MyMessage& generated);
Result<MyMessage> to_generated(const AppMessage& app);
Build modernization

Compile generated code conservatively, facade aggressively.

add_library(asn1c_generated STATIC
    generated/MyMessage.c
    generated/MyMessageDec.c
    generated/MyMessageEnc.c
)

# Vendor code: stable, conservative warning policy.
target_compile_options(asn1c_generated PRIVATE
    $<$<CXX_COMPILER_ID:MSVC>:/W0>
    $<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-w>
)

add_library(asn1c_cpp26_facade STATIC
    src/Context.cpp
    src/MyMessageCodec.cpp
    src/MyMessageAdapters.cpp
)

target_compile_features(asn1c_cpp26_facade PUBLIC cxx_std_26)
target_link_libraries(asn1c_cpp26_facade PRIVATE asn1c_generated)
Verification

Modernization is only done when vectors still match.

1

Golden vectors

Encoded bytes before and after must be identical for canonical cases.

2

Negative tests

Malformed length, invalid choices, and PER bitmap failures must remain rejected.

3

Benchmarks

Track decode latency, allocations, encoded size, and p95/p99 behavior.

Why use Amp?

Amp turns modernization into a repeatable engineering loop.

Understands both sides

Amp can read ASN.1 schemas, generated C/C++, build files, and app code together.

Changes the safe layer first

It can build facades, adapters, tests, and benchmarks without mutating generated vendor code.

Verifies behavior

It can run focused unit tests, vector checks, compilers, and report what remains blocked.

Documents decisions

It leaves behind reports and code samples so future regeneration stays controlled.

Deliverables

What the project produces.

modernization/
  include/asn1c_modern/Context.hpp
  include/asn1c_modern/MyMessageCodec.hpp
  src/Context.cpp
  src/MyMessageCodec.cpp
  src/MyMessageAdapters.cpp
  tests/golden_vectors.cpp
  benchmarks/decode_benchmark.cpp
  cmake/asn1c_cpp26.cmake
  reports/modernization-report.html

The result is safer, easier to test, and still compatible with Objective Systems ASN1C regeneration.

← Swipe to navigate →
1 / 12