Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: [ ci-setup, dev-local, main ]
branches: [ ci-setup, dev-local, dev_local, main ]
pull_request:
branches: [ main ]

Expand Down Expand Up @@ -102,6 +102,25 @@ jobs:
run: |
make -C gen build

- name: Test C++ compatibility (extern C guards)
run: |
# Test both prefixed and compatibility shim headers
echo '#include "gen/include/sc_utils.h"' > test_cpp_compat.cpp
echo 'int main() { return 0; }' >> test_cpp_compat.cpp
g++ -std=c++11 -I. test_cpp_compat.cpp -o test_cpp_compat
echo "✓ C++ compilation with prefixed headers successful"

# Test compatibility shims if they exist
if [ -f "gen/include/utils.h" ] && [ -f "gen/include/registry.h" ]; then
echo '#include "gen/include/utils.h"' > test_cpp_shim.cpp
echo '#include "gen/include/registry.h"' >> test_cpp_shim.cpp
echo 'int main() { return 0; }' >> test_cpp_shim.cpp
g++ -std=c++11 -I. test_cpp_shim.cpp -o test_cpp_shim
echo "✓ C++ compilation with compatibility shims successful"
else
echo "⚠ Compatibility shims not found (expected with current config)"
fi

- name: Smoke test runner
run: |
./gen/build/test_runner test_fixed_suite_roundtrip
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

## [Unreleased]

## [0.2.0] - TBD

### Added
- **C++ compatibility**: All generated headers now include `extern "C"` guards for seamless C++ integration.
- **Enhanced CLI**: Improved help messages with examples and proper exit codes.
- **Config validation**: Added validation for YAML config values with warnings for invalid options.
- **Compatibility shims**: Auto-generated `utils.h` and `registry.h` headers for backward compatibility.

### Changed
- **Prefix system**: Stabilized file prefix handling to prevent symbol conflicts and linker errors.
- **CLI help**: Enhanced `--help` output with version info, examples, and clearer option descriptions.

### Fixed
- **Header naming**: Resolved include path mismatches between prefixed and non-prefixed utilities.
- **Duplicate symbols**: Generator now cleans conflicting prefix variants to prevent linker errors.
- **Build stability**: Improved C build reliability with proper symbol management.

## [0.1.0] - 2025-08-25

### Added
Expand Down
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,25 @@ Configurable prefix
- Common files use a prefix (default `sc_`), yielding `sc_utils.{h,c}` and `sc_registry.{h,c}`.
- Change it via `file_prefix` in your YAML config if needed.

### C++ compatibility

All generated headers include `extern "C"` guards for seamless C++ integration:

```cpp
// Your C++ firmware can include generated headers directly
#include "gen/include/sc_utils.h" // prefixed header
#include "gen/include/utils.h" // compatibility shim
#include "gen/include/MESSAGE_1.h" // message-specific header

extern "C" {
// C++ code can call generated C functions directly
MESSAGE_1_t msg = {0};
bool success = MESSAGE_1_decode(&msg, can_data, dlc);
}
```

The generated code remains pure C99, ensuring compatibility with both C and C++ projects without requiring changes to build systems or toolchains.

## Platforms, compilers, and test environments

Tested combos
Expand Down
8 changes: 8 additions & 0 deletions src/Generator/Codegen.Message.fs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ module Message =
headerLines.Add "#include <stdint.h>"
headerLines.Add "#include <stdbool.h>"
headerLines.Add ""
headerLines.Add "#ifdef __cplusplus"
headerLines.Add "extern \"C\" {"
headerLines.Add "#endif"
headerLines.Add ""
// Emit value-table enums and to_string prototypes
let vtSignals = message.Signals |> List.choose (fun s -> s.ValueTable |> Option.map (fun vt -> s, vt))
vtSignals |> List.iter (fun (s, vt) ->
Expand Down Expand Up @@ -285,6 +289,10 @@ module Message =
headerLines.Add (sprintf "bool %s_decode(%s_t* msg, const uint8_t data[], uint8_t dlc);" message.Name message.Name)
headerLines.Add (sprintf "bool %s_encode(uint8_t data[], uint8_t* out_dlc, const %s_t* msg);" message.Name message.Name)
headerLines.Add ""
headerLines.Add "#ifdef __cplusplus"
headerLines.Add "}"
headerLines.Add "#endif"
headerLines.Add ""
headerLines.Add (sprintf "#endif // %s_H" (message.Name.ToUpperInvariant()))
String.concat "\n" (List.ofSeq headerLines)

Expand Down
2 changes: 1 addition & 1 deletion src/Generator/Codegen.Registry.fs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module Registry =
|> fun arr -> new string(arr)
let banner = sprintf "/* Generated by Signal CANdy\n file_prefix=%s, phys_type=%s, phys_mode=%s, dispatch=%s, motorola_start_bit=%s */\n" config.FilePrefix config.PhysType config.PhysMode config.Dispatch config.MotorolaStartBit
let registryHContent =
banner + sprintf "#ifndef %s\n#define %s\n\n#include <stdint.h>\n#include <stdbool.h>\n\nbool decode_message(uint32_t id, const uint8_t data[], uint8_t dlc, void* msg);\n\n#endif // %s" guard guard guard
banner + sprintf "#ifndef %s\n#define %s\n\n#include <stdint.h>\n#include <stdbool.h>\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\nbool decode_message(uint32_t id, const uint8_t data[], uint8_t dlc, void* msg);\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif // %s" guard guard guard
File.WriteAllText(registryHPath, registryHContent)

let includes =
Expand Down
2 changes: 1 addition & 1 deletion src/Generator/Codegen.Utils.fs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ module Utils =
let utilsHContent (config: Generator.Config.Config) =
let banner = sprintf "/* Generated by Signal CANdy\n file_prefix=%s, phys_type=%s, phys_mode=%s, dispatch=%s, motorola_start_bit=%s */\n" config.FilePrefix config.PhysType config.PhysMode config.Dispatch config.MotorolaStartBit
let g = guard config.FilePrefix "utils_h"
banner + sprintf "#ifndef %s\n#define %s\n\n#include <stdint.h>\n#include <stdbool.h>\n\n// Little-endian bit extraction functions\nuint64_t get_bits_le(const uint8_t* data, uint16_t start_bit, uint16_t length);\n\n// Little-endian bit insertion functions\nvoid set_bits_le(uint8_t* data, uint16_t start_bit, uint16_t length, uint64_t value);\n\n// Big-endian (Motorola) bit extraction\nuint64_t get_bits_be(const uint8_t* data, uint16_t start_bit, uint16_t length);\n\n// Big-endian (Motorola) bit insertion\nvoid set_bits_be(uint8_t* data, uint16_t start_bit, uint16_t length, uint64_t value);\n\n#endif // %s" g g g
banner + (sprintf "#ifndef %s\n#define %s\n\n#include <stdint.h>\n#include <stdbool.h>\n\n#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n// Little-endian bit extraction functions\nuint64_t get_bits_le(const uint8_t* data, uint16_t start_bit, uint16_t length);\n\n// Little-endian bit insertion functions\nvoid set_bits_le(uint8_t* data, uint16_t start_bit, uint16_t length, uint64_t value);\n\n// Big-endian (Motorola) bit extraction\nuint64_t get_bits_be(const uint8_t* data, uint16_t start_bit, uint16_t length);\n\n// Big-endian (Motorola) bit insertion\nvoid set_bits_be(uint8_t* data, uint16_t start_bit, uint16_t length, uint64_t value);\n\n#ifdef __cplusplus\n}\n#endif\n\n#endif // %s" g g g)

let utilsCContent (config: Generator.Config.Config) =
let uH = utilsHeaderName config
Expand Down
43 changes: 32 additions & 11 deletions src/Generator/Codegen.fs
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,45 @@ module Codegen =
Directory.CreateDirectory (Path.Combine(outputPath, "include")) |> ignore
Directory.CreateDirectory (Path.Combine(outputPath, "src")) |> ignore

// Clean up legacy, unprefixed common files to prevent duplicate symbols in C builds
let legacyHeaders = [ "utils.h"; "registry.h" ]
let legacySources = [ "utils.c"; "registry.c" ]
for h in legacyHeaders do
let p = Path.Combine(outputPath, "include", h)
if File.Exists p then
try File.Delete p with _ -> ()
for s in legacySources do
let p = Path.Combine(outputPath, "src", s)
if File.Exists p then
try File.Delete p with _ -> ()
// Clean up stale prefixed common files to prevent duplicate symbols in C builds
// Keep only the variants matching the current FilePrefix; remove others.
let keepUtilsH = Utils.utilsHeaderName config
let keepUtilsC = Utils.utilsSourceName config
let keepRegH = sprintf "%sregistry.h" config.FilePrefix
let keepRegC = sprintf "%sregistry.c" config.FilePrefix

let includeDir = Path.Combine(outputPath, "include")
let srcDir = Path.Combine(outputPath, "src")

if Directory.Exists includeDir then
Directory.GetFiles(includeDir, "*utils.h")
|> Array.iter (fun f -> if Path.GetFileName(f) <> keepUtilsH then try File.Delete f with _ -> ())
Directory.GetFiles(includeDir, "*registry.h")
|> Array.iter (fun f -> if Path.GetFileName(f) <> keepRegH then try File.Delete f with _ -> ())

if Directory.Exists srcDir then
Directory.GetFiles(srcDir, "*utils.c")
|> Array.iter (fun f -> if Path.GetFileName(f) <> keepUtilsC then try File.Delete f with _ -> ())
Directory.GetFiles(srcDir, "*registry.c")
|> Array.iter (fun f -> if Path.GetFileName(f) <> keepRegC then try File.Delete f with _ -> ())

// Generate utils.h and utils.c with prefix
let uH = Utils.utilsHeaderName config
let uC = Utils.utilsSourceName config
File.WriteAllText(Path.Combine(outputPath, "include", uH), Utils.utilsHContent config)
File.WriteAllText(Path.Combine(outputPath, "src", uC), Utils.utilsCContent config)

// Emit compatibility shims for legacy includes (utils.h, registry.h)
let shimHeader (name: string) (target: string) =
let guard = (name.Replace('.', '_') + "_SHIM").ToUpperInvariant()
"#ifndef " + guard + "\n#define " + guard + "\n\n"
+ "#ifdef __cplusplus\nextern \"C\" {\n#endif\n\n"
+ "#include \"" + target + "\"\n\n"
+ "#ifdef __cplusplus\n}\n#endif\n\n"
+ "#endif // " + guard
File.WriteAllText(Path.Combine(outputPath, "include", "utils.h"), shimHeader "utils.h" uH)
File.WriteAllText(Path.Combine(outputPath, "include", "registry.h"), shimHeader "registry.h" keepRegH)

// Generate code for each message
for message in ir.Messages do
Message.generateMessageFiles message outputPath config
Expand Down
29 changes: 29 additions & 0 deletions src/Generator/Config.fs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ module Config =
let deserializer = DeserializerBuilder().Build()
let map = deserializer.Deserialize<IDictionary<string, obj>>(yaml)
let phys = tryGetString map [ "phys_type"; "PhysType" ] |> Option.defaultValue "float"

// Validate phys_type
let validPhysTypes = ["float"; "fixed"]
if not (List.contains (phys.ToLowerInvariant()) validPhysTypes) then
eprintfn "Warning: Invalid phys_type '%s'. Valid values: %s. Using default 'float'." phys (String.concat ", " validPhysTypes)

// New: phys_mode provides fine-grained control over internal math
let physModeRaw = tryGetString map [ "phys_mode"; "PhysMode" ]
let physMode =
Expand All @@ -57,12 +63,35 @@ module Config =
| "float" -> "double" // backward-compat: old float defaults to double intermediates
| "fixed" -> "fixed_double" // old fixed falls back to double when fast path not applicable
| _ -> "double"

// Validate phys_mode
let validPhysModes = ["double"; "float"; "fixed_double"; "fixed_float"]
if not (List.contains (physMode.ToLowerInvariant()) validPhysModes) then
eprintfn "Warning: Invalid phys_mode '%s'. Valid values: %s. Using default 'double'." physMode (String.concat ", " validPhysModes)

let range = tryGetBool map [ "range_check"; "RangeCheck" ] |> Option.defaultValue false
let disp = tryGetString map [ "dispatch"; "Dispatch" ] |> Option.defaultValue "binary_search"

// Validate dispatch
let validDispatch = ["binary_search"; "direct_map"]
if not (List.contains (disp.ToLowerInvariant()) validDispatch) then
eprintfn "Warning: Invalid dispatch '%s'. Valid values: %s. Using default 'binary_search'." disp (String.concat ", " validDispatch)

let crc = tryGetBool map [ "crc_counter_check"; "CrcCounterCheck" ] |> Option.defaultValue false
let moto = tryGetString map [ "motorola_start_bit"; "MotorolaStartBit" ] |> Option.defaultValue "msb"

// Validate motorola_start_bit
let validMoto = ["msb"; "lsb"]
if not (List.contains (moto.ToLowerInvariant()) validMoto) then
eprintfn "Warning: Invalid motorola_start_bit '%s'. Valid values: %s. Using default 'msb'." moto (String.concat ", " validMoto)

// Optional: file prefix for generated common files (utils/registry). Defaults to "sc_".
let filePrefix = tryGetString map [ "file_prefix"; "FilePrefix" ] |> Option.defaultValue "sc_"

// Validate file_prefix (basic C identifier rules)
if not (System.Text.RegularExpressions.Regex.IsMatch(filePrefix, @"^[a-zA-Z_][a-zA-Z0-9_]*$")) then
eprintfn "Warning: file_prefix '%s' may not be a valid C identifier prefix. Proceeding anyway." filePrefix

Some { PhysType = phys; PhysMode = physMode; RangeCheck = range; Dispatch = disp; CrcCounterCheck = crc; MotorolaStartBit = moto; FilePrefix = filePrefix }
with
| ex ->
Expand Down
23 changes: 21 additions & 2 deletions src/Generator/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,27 @@ module Program =
let parsedArgs = parseArgs args "" "" None None true

if parsedArgs.DbcPath = "" || parsedArgs.OutputPath = "" then
eprintfn "Usage: dotnet run --project src/Generator -- --dbc <dbc_file_path> --out <output_directory> [--config <config_file_path>] [--prefix <file_prefix>] [--emit-main <true|false>]"
1
eprintfn "Signal CANdy v0.2.0 - DBC to C Code Generator"
eprintfn "Generate C99 parser modules from DBC files with C++ compatibility"
eprintfn ""
eprintfn "USAGE:"
eprintfn " dotnet run --project src/Generator -- --dbc <dbc_file> --out <output_dir> [OPTIONS]"
eprintfn ""
eprintfn "REQUIRED:"
eprintfn " --dbc <file> DBC file to parse"
eprintfn " --out <dir> Output directory for generated C files"
eprintfn ""
eprintfn "OPTIONS:"
eprintfn " --config <file> YAML config file (default: built-in defaults)"
eprintfn " --prefix <str> File prefix for common files (overrides config)"
eprintfn " --emit-main <bool> Copy examples/main.c to output (default: true)"
eprintfn ""
eprintfn "EXAMPLES:"
eprintfn " dotnet run --project src/Generator -- --dbc sample.dbc --out gen"
eprintfn " dotnet run --project src/Generator -- --dbc sample.dbc --out gen --config config.yaml"
eprintfn " dotnet run --project src/Generator -- --dbc sample.dbc --out gen --prefix my_"
eprintfn ""
0
else
printfn "DBC Path: %s" parsedArgs.DbcPath
printfn "Output Path: %s" parsedArgs.OutputPath
Expand Down
8 changes: 8 additions & 0 deletions templates/message.h.scriban
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
#include <stdint.h>
#include <stdbool.h>

#ifdef __cplusplus
extern "C" {
#endif

// Message: {{ message_name }} (ID: {{ message_id }})
typedef struct {
{{ signal_declarations_h }}
Expand All @@ -15,4 +19,8 @@ bool {{ message_name }}_decode({{ message_name }}_t* msg, const uint8_t data[],
// Encode function
bool {{ message_name }}_encode(uint8_t data[], uint8_t* out_dlc, const {{ message_name }}_t* msg);

#ifdef __cplusplus
}
#endif

#endif // {{ message_name | string.upcase }}_H
2 changes: 2 additions & 0 deletions templates/registry.c.scriban
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Auto-generated registry source (placeholder)
// Implementations will be rendered by the generator.
15 changes: 15 additions & 0 deletions templates/registry.h.scriban
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Auto-generated registry header (placeholder)
#ifndef REGISTRY_H
#define REGISTRY_H

#ifdef __cplusplus
extern "C" {
#endif

// ... registry API declarations will be rendered here ...

#ifdef __cplusplus
}
#endif

#endif // REGISTRY_H
8 changes: 8 additions & 0 deletions templates/utils.h.scriban
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@
#include <stdint.h>
#include <stdbool.h>

#ifdef __cplusplus
extern "C" {
#endif

// Little-endian bit extraction functions
uint64_t get_bits_le(const uint8_t* data, uint16_t start_bit, uint16_t length);

// Little-endian bit insertion functions
void set_bits_le(uint8_t* data, uint16_t start_bit, uint16_t length, uint64_t value);

#ifdef __cplusplus
}
#endif

#endif // UTILS_H
Binary file added test_cpp_compat
Binary file not shown.
2 changes: 2 additions & 0 deletions test_cpp_compat.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#include "gen/include/sc_utils.h"
int main() { return 0; }