Wednesday, July 1, 2026

Stop Using Empty Braces {} for C++ Constructors

Empty constructors can disable useful compiler optimizations.

// Avoid this
MyClass() {}

// Prefer this
MyClass() = default;

1. The Performance Secret: "Trivial" Types

With {} (the slow way), the constructor becomes user-provided, making the type non-trivially default constructible. That prevents standard library implementations from using the highly optimized code paths available for trivial types.

With "= default" (the fast way), the constructor remains compiler-generated, allowing the type to stay trivially default constructible when all its members are trivial. This enables the standard library and compiler to use optimized code paths that are unavailable for user-provided constructors.

In the following example, the compiler generates machine instructions for A but not for B. From the language's perspective "user-provided" and "does nothing" are different concepts:

// C++ code
struct A { A() {} };
struct B { B() = default;};
int main() {
  A a;
  B b;
}

//Assembly code (generated with Compiler Explorer, x86-64 clang (trunk), no optimization:
main:
  sub rsp, 16
  lea rdi, [rbp - 1]
  call A::A() [base object constructor]
  xor eax, eax
  add rsp, 16
  pop rbp
  ret

A::A() [base object constructor]:
  push rbp
  mov rbp, rsp
  mov qword ptr [rbp - 8], rdi
  pop rbp
  ret

Note that if you use the optimization flag -O1, the code reduces to its optimal form:

main:
  xor eax, eax
  ret

This is a good illustration of a broader point: at -O0, C++ codegen tends to reflect the syntax of your code fairly literally (useful for debugging, every statement maps predictably to instructions). At -O1 and above, codegen instead reflects the observable semantics and "construct two empty objects nobody uses" has no observable semantics at all.

2. Guarding Your Move Semantics

Writing an empty destructor ~MyClass() {} triggers the Rule of Five in a bad way. The compiler assumes that because you wrote a custom destructor, you are managing resources manually. It automatically deletes your implicit move constructor and move assignment operator. By trying to be clean with {}, you accidentally cripple your class's ability to be efficiently moved. Using "= default" keeps your move semantics intact.

3. Why Can't the Compiler Just Optimize {}?

It isn't a lack of compiler intelligence; it is a legal restriction. The C++ Standard explicitly states that writing {} makes a constructor user-provided. Changing this rule would break the legacy code. For example, if the standard suddenly declared that empty braces implied "trivial," code that intentionally uses {} to block unsafe memcpy operations (like cryptography classes) would instantly become vulnerable. Furthermore, if you had an empty #ifdef DEBUG block inside your constructor, your class would dangerously flip between being non-trivial in Debug mode and trivial in Release mode.

The C++ committee gave us "= default" in C++11 as an explicit toggle. {} tells the compiler, "Hands off, I'm taking control." = default tells the compiler, "I want standard behavior, optimize this as much as you can." Always give the compiler the green light.

Wednesday, June 17, 2026

Interrupt Deadlock

In embedded systems, there is a dangerous trap where standard thread safety fails completely, leaving your application permanently frozen. That trap is to use a mutex inside an interrupt handler. Consider this thread-safe function, where you protect shared state with a mutex:

void update_hardware(int data) {
    pthread_mutex_lock(&lock)
    global_hardware_buffer = data; // What if an interrupt hits right here?
    pthread_mutex_unlock(&lock);
}

If this function is called by an interrupt service routine (ISR) and a hardware interrupt fires right in the middle of that function:

  1. The Main Thread acquires the lock.
  2. The Interrupt hits. The CPU immediately freezes the main thread and jumps to your ISR.
  3. The Re-entry: The interrupt handler needs to log something, so it calls update_hardware().
  4. The Deadlock: The interrupt handler hits pthread_mutex_lock(). It sees the lock is busy, so it waits.

But who is it waiting for? It's waiting for the main thread to release the lock. Except the main thread is frozen underneath the interrupt handler, waiting for the interrupt to finish!

A common misconception is that the operating system's scheduler will see the interrupt handler is blocked, context-switch it out, let the main thread finish, and fix the mess. It can't. In embedded systems or Real-Time Operating Systems (RTOS), interrupts run at a higher execution priority than the scheduler itself. ISRs execute outside normal thread scheduling. If an ISR attempts to wait for a resource held by the interrupted thread, forward progress becomes impossible because the interrupted thread cannot run until the ISR completes. This is priority inversion taken to the extreme. The ISR (highest priority in the system) ends up waiting for a lower-priority thread that it itself has preempted.

To survive interrupts, your code cannot just be thread-safe, it must be reentrant. A reentrant function is completely self-contained. It never touches global variables, it never uses static buffers, and it never locks a mutex. It relies strictly on local variables allocated on the stack or parameters passed to it. Because it has no shared memory between calls, it can be interrupted at any instruction and safely called again without data corruption or deadlocks. Here is a reentrant function example:

// This function operates purely on local stack memory.
// It can be safely interrupted and re-entered at any microsecond.
int calculate_hardware_state(int current_state, int new_data) {
    int next_state;
    next_state = current_state + new_data;
    return next_state;
}

Also pay attention to macros because if a macro references a global variable, a static variable, or a hardcoded hardware register under the hood, any function using that macro instantly becomes non-reentrant:

int global_status = 0;
#define SET_STATUS_FLAG(mask) (global_status |= (mask))

Never use blocking synchronization primitives (like mutexes, malloc, or I/O) inside an interrupt handler or signal handler. If you must pass data between an interrupt and your main loop, stick to lock-free mechanisms like C11 atomics or volatile flags.

Wednesday, June 10, 2026

Choosing the Right CPU: Desktop vs. Industrial vs. Safety-Critical

We live in an era where a standard desktop processor has 24 cores and clock speeds past 5.5 GHz. Yet, if you walk into an automotive assembly line, you will see computers (PLCs) with processors running 100x slower, and being 10x more expensive than their desktop counterparts.

Why? Because in the world of computing, power is defined entirely by the problem you are trying to solve. We have to look past raw processing speed and examine three distinct operational philosophies: Throughput, Determinism, and Functional Safety.

1. The Desktop CPU

Desktop processors are designed to handle an unpredictable, highly dynamic workload. At any given moment, a desktop CPU might be asked to render a 3D video, compile a massive codebase, manage dozens of browser tabs, or decode high-definition audio.

To excel at this, desktop CPUs use general purpose operating systems like Windows or Linux, which rely on throughput-oriented schedulers. The OS slices up time and distributes it among running applications, trying to give everything a fair share. To squeeze out every drop of performance, the silicon itself relies on microarchitectural optimizations:

  • Out-of-Order Execution: The CPU dynamically rearranges the order of instructions to keep its execution pipelines full.
  • Speculative Execution & Branch Prediction: The processor literally guesses which path a piece of code will take before it even runs, executing the instructions ahead of time to hide latency.
  • Multi-Tiered Caches (L1/L2/L3): Large memory pools sit on the die to prevent the CPU from constantly waiting on slower system RAM.

However, this architecture is inherently non-deterministic. If a background cloud-sync app suddenly demands resources, or if a branch predictor guesses wrong, a task might take 50 milliseconds longer to execute on cycle two than it did on cycle one. In the consumer world, a dropped frame in a video game is an annoyance; in a physical system, a 50ms delay can be catastrophic.

2. The Industrial PLC CPU

Step inside a factory running a high-end programmable logic controller (PLC), like the Siemens SIMATIC S7-1500. Clock speeds range from tens to hundreds of megahertz, and memory is measured in megabytes rather than gigabytes. Yet, these processors are built for a completely opposing goal: Absolute Determinism.

An industrial CPU runs a Real-Time Operating System (RTOS). Instead of a fair share schedule, the RTOS uses strict, unyielding, priority-based cyclic execution. A PLC operates on a continuous loop:

  1. Read Inputs: Snapshot the state of every physical sensor.
  2. Execute Logic: Run the user control code sequentially.
  3. Write Outputs: Instantly update physical actuators, valves, and motors.

To guarantee that this cycle takes exactly the same amount of time down to the microsecond, industrial CPUs strip away the unpredictable optimization layers of desktop chips. There is no speculative execution, no out-of-order execution, and no virtual memory paging. Every task has a strict time budget monitored by a dedicated hardware watchdog timer. If a 1ms motion control loop fails to complete in exactly 1ms, the RTOS catches it, alerts the system, and can trigger a controlled shutdown.

Industrial CPUs are also physically engineered to survive decades in harsh environments. They are decoupled from fragile cooling fans, insulated against severe electromagnetic interference (EMI), and rated to maintain their precise timing clock cycles across extreme temperature swings (e.g., -25°C to +60°C).

3. The Safety-Critical CPU

While an industrial CPU guarantees when a command will execute, a safety-critical processor guarantees the mathematical integrity of the execution itself. Found in drive-by-wire automotive systems, avionics units complying with aerospace standards (like DO-254/DO-178C), or high-speed medical equipment, these processors are certified to standards like ISO 26262 (ASIL-D) or IEC 61508 (SIL-3).

The defining feature of a commercial safety-critical processor (such as the Texas Instruments Hercules™ or Infineon AURIX™ lines) is hardware level redundancy. Instead of using multiple cores to run different applications simultaneously, a safety-critical CPU pairs identical cores into a Dual-Core Lockstep (DCLS) configuration:

  • The Master and the Checker: Two physical hardware cores execute the exact same instruction stream, line-by-line, cycle-by-cycle.
  • Temporal Separation: To ensure that a localized physical event (like a voltage spike or a cosmic ray flipping a bit in memory) doesn't corrupt both cores simultaneously, the second core runs delayed by a micro-interval (typically 2 to 3 clock cycles).
  • Hardware Comparators: Independent hardware logic monitors the internal state and outputs of both cores. If a divergence of even a single bit is detected between the Master and the Checker, the comparator immediately strips power from the actuators or switches the system to a pre-defined, hardware-enforced "safe state."

In this realm, the code is heavily audited, features like Built-In Self-Tests (BIST) continuously sweep memory for corruption, and every single gate on the silicon must be mathematically traceable back to a design requirement.

Music: Ali Baba ve 7 Cüceler (arka jenerik)

Friday, May 8, 2026

Embed .NET runtime into C# exe

When writing a C# program, if you want to bundle the .NET runtime with the application so that it can run on a Windows PC without a separate .NET installation:
  1. Create the project as "Windows Forms App", not "Windows Forms App (.NET Framework)"
  2. Use Release x64 configuration instead of only Any CPU
  3. In Visual Studio 2022, open the terminal and run:dotnet publish -c Release -r win-x64 -p:Platform=x64 --self-contained true -p:PublishSingleFile=true
  4. The published executable is located in:
  5. bin\x64\Release\net8.0-windows\win-x64\publish\
  6. The generated EXE includes the .NET runtime and can run on systems without .NET installed.
Depending on the NuGet packages used (e.g. S7NetPlus), additional DLLs may still appear in the publish folder and should also be distributed with the EXE.

Thursday, May 7, 2026

std::system Windows vs Linux

On Windows the C++ function std::system() is essentially a wrapper around the command processor (cmd.exe). When the process finishes, the exit code is passed directly back to you. If your program exits with 1, the integer returned by std::system is 1. On Linux, it might return 256.

On Linux, a single integer return value isn't just an exit code; it's a status word containing a wealth of information about how the process died. The OS packs different data into specific bit ranges. In most Linux implementations, the exit code is shifted into the high byte. This means a return code of 1 is stored as 1 << 8, which equals 256. To get back the exit code, you have to right shift the status code by 8 bits. The portable way is to use WEXITSTATUS macro.

To write code that works on both platforms, you cannot treat the return value as a raw number. You must use the decoding macros provided in <sys/wait.h> on Linux. You should always check if the process actually finished before asking for the code. Here is the safest pattern for Linux:

#include <sys/wait.h>
int status = std::system("./my_script.sh");
if (WIFEXITED(status)) {
  int exitCode = WEXITSTATUS(status); //
  std::cout << "Success! Code: " << exitCode;
} else if (WIFSIGNALED(status)) {
  int sig = WTERMSIG(status);
  std::cout << "Killed by signal: " << sig;
}

Monday, April 6, 2026

Make C++ memory safe by never using "new"

Every C++ developer eventually encounters a memory leak. You allocate something on the heap, write some logic, hit an early return at runtime and suddenly that heap memory is gone forever:

std::vector<int>* numbers = new std::vector<int>({1, 2, 3});
// ... what if we return early?
// ... what if an exception fires?
delete numbers; // only runs if we get here

The good news is that it is entirely possible to write professional C++ without malloc or new. The above example can be written as follows, without new and delete:

std::vector<int> numbers = {1, 2, 3}; // Clean and safe

The vector internally does roughly this:

 Stack              Heap
┌───────────────┐  ┌───────────────┐
│ numbers       │  │               │
│ _data ────────┼──┼─► [1] [2] [3] │
│ _size = 3     │  │               │
│ _capacity= 3  │  │               │
└───────────────┘  └───────────────┘

The vector object itself lives on the stack, but the actual integers are allocated on the heap via new[] / allocator, inserted by the compiler. The vector acts as a "wrapper" or "manager" for a raw block of memory on the heap. The std::vector object itself is just a small, fixed-size handle (8-byte pointers = 24 bytes). It doesn't grow or shrink, helping you preserve the precious stack. It knows exactly where the heap memory starts, how much is used, and how much is left. The actual data (your integers, strings, or custom objects) lives in the heap and its size can be gigabytes.

In C++, the destructor of a stack-allocated object is automatically inserted into the generated code by the compiler at all the places where the object goes out of scope:

int complexFunction(int x) {
    std::vector<int> numbers = {1, 2, 3};

    if (x < 0) {
        // COMPILER INSERTS: numbers.~vector();
        return -1; 
    }

    // COMPILER INSERTS: numbers.~vector();
    return x * 2;
}

In the assembly listing, you will see lines like this:

call std::vector<int, std::allocator<int>>::~vector() [base object destructor]

When the std::vector destructor runs, it automatically performs two critical tasks:

  1. Element Destruction: it calls the destructor for every individual object currently stored in the vector. If you have a vector of strings, it ensures each string cleans up its own character buffer first.
  2. Deallocation: Once the elements are destroyed, the vector calls the underlying deallocation function (typically a wrapper around operator delete[] or a custom allocator) to return the entire block of heap memory to the system.

If you were using malloc or new manually, you would have to remember to call free or delete in every possible exit path of your function (including if an error occurs):

std::vector removes this "human element" by making the cleanup a language-level guarantee.

If you have a custom MyClass with a constructor that takes runtime parameters:

// ❌ with new — obj on heap, you manage lifetime manually
MyClass* obj = new MyClass(size, name);
delete obj; // must remember this at every exit point

// ✅ without new — obj on stack, lifetime managed automatically
MyClass obj(size, name);
// no delete needed

In C++11 and beyond, smart pointers cover every legitimate use case for new and delete. Example of object that outlives its scope:

// ❌ old way
MyClass* obj = new MyClass(size, name);
return obj; // caller must remember to delete

// ✅ modern way
return std::make_unique<MyClass>(size, name); // ownership transfers automatically

When you have a class OneClass with a member myMember variable that is also a class of type MyClass and myMember constructor parameters are specified at runtime:

class OneClass {
public:
    OneClass(int size, std::string name)
        : myMember(size, name) // ← myMember constructed here, with runtime args
    {
        // constructor body, myMember is already fully constructed here
    }

private:
    MyClass myMember;  // ← no "new", lives inside OneClass
};

OneClass obj(size, name) created │ ├── myMember(size, name) constructed ← initializer list │ └── OneClass constructor body runs

If myMember is not constructed in OneClass constructor but in some other method call:

#include <memory>
class OneClass {
public:
    OneClass() {} // myMember is nullptr

    void initialize(int size, std::string name) {
        myMember = std::make_unique<MyClass>(size, name);   // constructed here
    }

private:
    std::unique_ptr<MyClass> myMember; // nullptr until initialize() is called
};

unique_ptr starts as nullptr and takes ownership when assigned. ~MyClass() is called automatically when OneClass is destroyed, no manual cleanup needed.

The general term for this mechanism is called RAII (Resource Acquisition Is Initialization). In this paradigm, you use objects that manage their own memory. When the object goes out of scope, it automatically cleans up.

Music: Passenger - Let Her Go

Monday, March 23, 2026

PID Theory

PID control trades optimality for simplicity, it's sub-optimal but good enough for most real systems without needing a mathematical model of the system. For example, for a lunar lander, bang-bang control achieves faster landing but you need the model the physics. You find the control parameters with simulations and tests. General form of PID control force terms:


What would be the simplest controller for a mass to stay at a specific height from the surface of a planet with only gravity acting and no atmosphere?

Without an atmosphere, your system is:


If you use only a Proportional (P) controller, your control force is:


This effectively turns your mass into a pure spring in a vacuum. It will oscillate up and down forever, centered around the target height, because there is no way to remove the kinetic energy (no damping). To stay at a specific height, you need to "electronically" create the friction/damping that the atmosphere is missing:

The P-term (Kp) provides the "restoring force" to push the mass toward the target height. The D-term (Kd) acts as artificial friction. It resists the velocity of the mass, allowing it to slow down as it approaches the target and eventually stop. The Gravity Bias (mg): Technically, to hover perfectly with a PD controller, you need to "cancel out" the constant pull of gravity so the controller only has to worry about the displacement error. Here is a P vs PD comparison using python script:

The double integrator has two poles at the origin (s=0, 0). Without a zero to "pull" them into the Left Half Plane (LHP), the poles have nowhere to go but up and down the imaginary axis as you increase K_p. By PD controller placing a zero on the LHP, you are creating a "target" in the stable region. As you increase the gain, the two poles at the origin are "pulled" off the imaginary axis and toward the LHP zero:

While an Integral (I) term is usually used to eliminate steady-state error (the "droop" caused by gravity), in a vacuum with a double integrator, adding an "I" term without a very strong "D" term is dangerous. It introduces more phase lag, which often leads to the instability shown in the original infographic you shared. In the frequency domain, an integrator introduces a 90° phase lag. A double integrator (1/s^2) already has a 180° phase lag. Adding an integral term pushes the total phase lag toward 270°.

In control systems, if your feedback is delayed by 180° or more, your "correction" starts acting in the same direction as the error. Instead of pulling the mass back to the target, the controller begins pushing it away, leading to the "Unstable" root locus you saw in the original infographic.

Integral windup is another problem where your mass (plant) is stuck (perhaps a mechanical limit or a saturated actuator), the error remains constant because the mass isn't moving, and the integral term keeps summing that error over time. The "I" value grows (winds up) to a massive number. When the mass finally breaks free, the controller has a "memory" of a huge error that no longer exists. It applies a massive, unnecessary force, causing the mass to overshoot violently or even crash into the hardware. You can mitigate windup by stopping the integrator from growing once the actuator reaches its maximum output or by only turning the "I" term on when the mass is very close to the target height.

Thursday, March 12, 2026

Digital vs Analog Simulation

While a purely digital simulation (Model-in-the-Loop) is great for testing logic, an analog simulation (Hardware-in-the-Loop) tests the electrical reality of your system. In a digital simulation, you use values like pressure directly from your atmosphere model. In reality, that pressure goes through a sensor which outputs voltage/current. Your electronics have to read that analog signal and convert it to digital before feeding it to your controller.

A real controller output has to drive a load. Analog simulation ensures the controller's transistors don't overheat or drop voltage when trying to move a high-pressure valve.

Your internal  Analog-to-Digital Converter (ADC) might add extra error. For example, your atmosphere model says 101.325kPa, but your ADC might convert it to 101.328 kPa due to its internal tolerance. Analog simulation reveals whether your control algorithm is robust enough to handle that 0.003 kPa error without oscillating. It also verifies that your controller’s ADC is actually calibrated correctly. The signal chain:

You cannot "short a wire to ground" in a purely digital simulation and see the smoke. With hardware like NI PXI Fault Insertion Units, you can physically short an analog input to a 24V rail. This allows you to verify that your hardware's protection diodes work and that your software enters a "Safe State" immediately.

Wednesday, February 4, 2026

Embedding Visual C++ Runtime into DLL

A user of one of my old desktop programs (written in Java8 and C++) reported that they were getting a "[FileName].dll is not a valid Win32 application." (the Turkish version: "... geçerli bir Win32 uygulaması değil"). Due to the "Win32" in the error message, my first thought was that they were using my 64-bit app on a 32-bit setup. However, that was not the case. They compared the PC that my app was working with the PC that was showing error and found out that they were able to make it work by copying mscvr120.dll file to Windows/System32 folder. After chatting with Gemini, here are my findings:
  • In the world of Windows development, Win32 is the name of the entire programming interface (the API) used to interact with the operating system. When they later moved to 64-bit, instead of renaming it to "Win64," they kept the name Win32 for the API itself to maintain developer familiarity. Technically, 64-bit Windows programs run on the Win32 API for 64-bit systems. So, when the OS says "not a valid Win32 application," it really means "not a valid Windows DLL/exe"
  • msvcr120.dll is a Dynamic Link Library (DLL) file that is a core component of the Microsoft Visual C++ Redistributable (Runtime) for Visual Studio 2013. Since the problematic PC never had any Visual Studio installed on it, it was missing the runtime dependency of my DLL.
  • Shared runtimes are used to reduce the size of the compiled binary, but they introduce a dependency on the target operating system to provide the runtime.
  • You can check the dependencies of your DLL or EXE by using Visual Studio's dumpbin.exe. On cmd, dumpbin /dependents filename.dll shows you the DLLs filename.dll depends on.
    • If you see MSVCR....dll, you need the C Runtime.
    • If you see MSVCP....dll, you also need the C++ Standard Library.
    • If you see KERNEL32.dll or USER32.dll, don't worry, those are part of Windows itself and are always present.
  • Previously, I discussed how to embed the C runtime in Linux. You can also  embed/bake the C/C++ Runtime into your binary with Visual Studio via Project Properties > C/C++ > Code Generation > Runtime Library
    • The default of /MD (Multi-threaded DLL) or /MDd (Multi-threaded Debug DLL) uses shared runtime
    • Changing it to /MT (Multi-threaded), embeds the code into the DLL, leaving it with zero external dependencies. You can verify that your DLL has no dependencies (besides KERNEL32.dll) with dumpbin.exe.
    • The disadvanages of /MT
      • Larger file size
      • If you have five different DLLs all compiled with /MT, each one has its own copy of the runtime in RAM. If they were compiled with /MD, they would all share a single instance of the shared DLL in memory.
      • If a security flaw is found in the Microsoft C++ Runtime, Windows Update cannot fix your app. You would have to recompile your project with the latest patches and send the new DLL to your users.
      • If you use /MT, make sure that any object created inside your DLL is also destroyed inside your DLL (e.g., using a DestroyObject() function you provide).
      • For Debug, use /MDd because it is optimized for finding bugs, filling uninitialized memory with specific patterns (like 0xCCCCCCCC) etc.
  • Windows folder names can be confusing:
    • C:\Windows\System32: Contrary to the name, this folder is for 64-bit DLLs on a 64-bit version of Windows.
    • C:\Windows\SysWOW64: This folder is for 32-bit DLLs. WOW64 stands for "Windows on Windows 64-bit".

Saturday, December 27, 2025

Reading unsigned data in Java

In binary files, a single byte is often used to represent numbers from 0 to 255 (unsigned). However, in Java, a byte is signed, ranging from -128 to 127, because Java doesn't have a unsigned types (with the exception of the 2 byte char type). A raw byte with value 0xF0 in a file meant to represent 240 will become -16 when using Javas's ByteBuffer.get(). To fix this:

int unsignedByte = buffer.get() & 0xFF;

Explanation: When performing bitwise operations, Java automatically "promotes" the 8-bit byte to a 32-bit signed integer. If the byte is 0xF0 = 240 (11110000), Java sees the leading 1 and assumes it is a negative number. Through Sign Extension, it fills the new 24 bits with 1s to preserve that negative value (-16) in the larger container.

Original Byte: 11110000 (-16, see two's complement)
Promoted Int : 11111111 11111111 11111111 11110000 (Still -16)
Now, you apply the mask 0xFF (255). In binary, 0xFF as a 32-bit integer is 00000000 00000000 00000000 11111111:
  11111111 11111111 11111111 11110000 (The promoted -16)
& 00000000 00000000 00000000 11111111 (The 0xFF mask)
 -------------------------------------
  00000000 00000000 00000000 11110000 (The result: 240)

By "ANDing" the promoted integer with 0xFF, you effectively clear out all the 1s created by sign extension, leaving only the original 8 bits and giving you the correct unsigned value of 240.

Similary, reading an unsigned short (16-bit) from file:

int unsignedShort = buffer.getShort() & 0xFFFF;

Reading an unsigned int (32-bit):

long unsignedInt = buffer.getInt() & 0xFFFFFFFFL;

Note that an unsigned 32-bit integer can exceed the capacity of a Java int. You must jump up to a long and use a long literal mask (noted by the L at the end of the mask value).

When writing a value that is first multplied by a scale factor of 2^31 (1 << 31),  Java assumes the 1 << 31 is an int and shifting 1 by 31 places puts it into the sign bit position of a 32bit int, which results in the negative value of -2147483648. Correct usage:

double val = 123;
long scaleFactor = (1L << 31); // Will be positive 2147483648 because long is 64bit
                               // and shifting by 31 won't put the 1 into sign bit
                               // position
long val_scaled = (long) (val * scaleFactor);

Wednesday, October 1, 2025

Why C/C++ circular dependency causes "syntax error"

I have a headerA with methodA that uses a structB from headerB and therefore includes headerB. When I call methodA, I get a "syntax error" in headerA where methodA uses structB location. Details:
// headerB.h
#ifndef HEADER_B_H
#define HEADER_B_H
struct structB { int x; };
#endif

// headerA.h
#ifndef HEADER_A_H
#define HEADER_A_H
#include "headerB.h"
void methodA(structB param);
#endif

// main.cpp
#include "headerB.h" // headerB processed first
#include "headerA.h" // headerB already included, so HEADER_B_H is defined
                     // #include "headerB.h" does NOTHING
                     // structB is unknown! → SYNTAX ERROR
The "syntax error" occurs because the compiler doesn't know what structB is when it tries to compile methodA. If headerB includes headerA (directly or indirectly), you have a circular dependency. The C++ preprocessor just does text substitution - it doesn't understand C++ syntax. It can't detect circular dependencies because include guards prevent infinite loops, so the circular dependency becomes an incomplete type error instead.

Friday, July 11, 2025

Double-to-Int Conversion with Bit Shifting

We often need to pack a large numeric range into 32 bits. For instance, timestamps in microseconds over a 36 minute period exceed Integer.MAX_VALUE. By discarding the least significant bits (via right shift), we can fit the value and later we can recover it by the same amount of left shift. However, increasing the number of shifts decreases accuracy. What we need to optimize is to find the right amount of bit shift that covers the range and minimizes error. The following Java code investigates this:


Monday, June 16, 2025

C/C++ header mismatch bug

I encountered a problem where a field in a global struct (myStruct) held a valid value before entering a function foo, but turned into garbage after entering it. When I consulted AI tools, they suggested that foo might be allocating very large local arrays, causing a stack overflow that could corrupt the global structure. Another possibility was an out-of-bounds write elsewhere in the code.

After a week of debugging and trying various solutions—such as increasing the thread's stack size—I discovered the root cause: The function foo was defined in a C library with multiple versions. Each version resided in a different folder but had the same file names. Which folder was used depended on a #define. I was including the header from one version of the library, but linking against the implementation from another. If the struct definitions had matched, this wouldn’t have caused an issue, but they differed—evident from the differing sizeof(myStruct). As a result, myStruct was interpreted using the wrong layout, leading to corrupted values from an incorrect memory region.

Sunday, June 15, 2025

C++ pointer bug

This C++ code has a significant bug that will cause undefined behavior:
#include <iostream>
class A {
  public:
    int val;
};
void reset(A *p_a) {
  if (p_a != NULL) {
      delete p_a;
  }
  p_a = new A();
}
int main() {
  A *p_a = new A();
  p_a->val = 5;
  std::cout << "Before reset, p_a->val:" << p_a->val << "\n";
  reset(p_a);
  std::cout << "After reset, p_a->val:" << p_a->val << "\n";
  return 0;
}

The reset function receives a copy of the pointer p_a, not a reference to it. When you modify p_a inside the function (with p_a = new A()), you're only changing the local copy - the original pointer in main() remains unchanged. What actually happens:

  1. p_a in main() points to an A object with val = 5 
  2. reset() receives a copy of this pointer 
  3. reset() deletes the original object (memory is freed) 
  4. reset() creates a new object, but assigns it only to the local copy 
  5. The original p_a in main() still points to the deleted memory 
  6. Accessing p_a->val after reset() is undefined behavior (accessing freed memory) 
The Fix: Pass the pointer by reference using a pointer-to-pointer or reference-to-pointer:
//Reference to pointer
void reset(A *&p_a) {
  if (p_a != nullptr) {
    delete p_a;
  }
  p_a = new A();
  // Call with: reset(p_a);

An even better fix is to use smart pointers, which removes the necessity for the reset function:

auto p_a = std::make_unique<A>();

You can detect such problems by enabling AddressSanitizer (ASAN) in Visual Studio:

  1. Right-click your project → Properties
  2. Go to Configuration Properties → C/C++ → General
  3. Set Enable Address Sanitizer to Yes (/fsanitize=address)
  4. Go to Configuration Properties → C/C++ → Optimization
  5. Set Optimization to Disabled (/Od) for better debugging
  6. Set Whole Program Optimization to No
  7. Go to Configuration Properties → C/C++ → Debug Information Format
  8. Set to Program Database (/Zi) or Program Database for Edit & Continue (/ZI)
In Eclipse CDT:

  1. Open your C/C++ project in Eclipse CDT
  2. Right-click project → Properties
  3. Navigate to C/C++ Build → Settings
  4. Under Tool Settings:
    1. GCC C++ Compiler → Miscellaneous
    2. GCC C Compiler → Miscellaneous
    3. Add to "Other flags": -fsanitize=address -g -O1
  5. Project Properties → C/C++ Build → Settings
  6. GCC C++ Linker → Miscellaneous
  7. Add to "Other objects": -fsanitize=address

Tuesday, May 27, 2025

Fuzzy Logic and Quake III Bots

Fuzzy logic is often used in decision-making systems where a detailed mathematical model of the system is unavailable or impractical. Instead of relying on equations, fuzzy logic encodes expert intuition into human-readable rules. These rules allow systems to make decisions based on approximate or linguistic input values, such as “low health” or “enemy nearby.”

For simple systems — say, with just one input and one output — fuzzy logic may be overkill. In those cases, a 1D interpolation (similar to proportional navigation) is often enough to generate smooth behavior transitions. But as systems grow more complex, fuzzy logic scales better than maintaining large interpolation grids or rigid condition trees.

While neural networks have become dominant in many domains, fuzzy logic still offers distinct advantages, especially in embedded or control-focused systems. Fuzzy logic requires structured human insight, while neural networks thrive on raw data and pattern discovery. For complex or poorly understood systems, writing fuzzy rules is impractical. Advantages of fuzzy logic over neural networks:

  1. Interpretability: Fuzzy rules are readable and understandable by developers and domain experts.
  2. Minimal training: Rules encode prior knowledge, reducing or eliminating the need for extensive data-driven training.
  3. Lightweight tuning: At most, fuzzy systems may require optimizing rule weights — a much simpler process than full network training.

One of the most interesting uses of fuzzy logic in gaming came from Quake III Arena. The bots in the game used fuzzy logic to evaluate possible behaviors — such as attack, search for health, search for a better weapon, retreat. Each action was assigned a desirability score based on fuzzy evaluations of current game state (e.g., health, distance to enemy, ammo). At each tick, the bot would choose the highest-scoring action.

To tune the bot parameters, the developers had bots play against each other and applied genetic algorithms to evolve the best-performing rule sets. Of course, they could not make the bots perfect because then a human player would never be able to win.

Monday, May 12, 2025

CPU, Analog, FPGA, or ASIC?

Algorithms can be implemented across a wide spectrum of hardware, each with its own trade-offs in speed, power, flexibility, cost, and scalability. Let’s compare the four main approaches:

1. Software on General-Purpose CPUs

Pros:

  • Easy to develop and debug: Rich toolchains, IDEs, and profiling tools.
  • Highly flexible: Reprogram anytime; modify algorithms at will.
  • Low development cost: No custom hardware needed; ready to run on PCs, servers, or microcontrollers.
  • Ecosystem and libraries: Access to optimized math libraries (e.g., FFTW, NumPy, BLAS).

Cons:

  • Latency and real-time constraints: OS overhead and unpredictable timing make hard real-time difficult. Soft real-time is achievable.
  • Performance limitations: Limited parallelism compared to hardware solutions.
  • High power consumption per operation: Especially inefficient for repetitive, simple tasks.

Ideal for general-purpose applications.

2. Analog Circuits

Pros:

  • Ultra-low latency: Signal is processed in real-time with no sampling delay.
  • Potentially high throughput: Continuous operation with no clock constraints.
  • Minimal power: No digital switching, especially useful in low-power sensors or RF front-ends.
  • No need for ADC/DAC: Processes raw analog signals directly.

Cons:

  • Limited precision: Susceptible to noise, drift, and component tolerances.
  • Hard to scale: Each additional function requires more physical components.
  • Difficult to tune or reconfigure: Redesign often requires physical changes.
  • No programmability: Once built, behavior is fixed or only marginally tunable.

Ideal for real-time sensing, analog filters, RF circuits, ultra-low power embedded front-ends.

3. Field-Programmable Gate Arrays (FPGAs)

Pros:

  • High parallelism: True concurrent execution of multiple operations.
  • Low deterministic latency: Ideal for real-time pipelines.
  • Reconfigurable hardware: Algorithms can be updated post-deployment.
  • Power-efficient: Much better performance-per-watt than CPUs for many tasks.

Cons:

  • Steep learning curve: Requires HDL knowledge (VHDL/Verilog) or high-level synthesis.
  • Toolchain complexity: Longer compile/synthesis times, debugging can be difficult.
  • Moderate development cost: More expensive than CPUs in small volumes.
  • Not optimal for floating-point math: Often better with fixed-point arithmetic.

Ideal for real-time video/audio processing, signal processing, robotics, hardware prototyping.

4. Custom Chips (ASICs)

Pros:

  • Maximum performance: Custom datapaths, memory layouts, and logic yield unmatched throughput.
  • Lowest power consumption: Fully optimized for the task at hand.
  • Smallest footprint: No unnecessary hardware or software overhead.
  • Production cost scales well: Extremely cheap per unit at high volumes.

Cons:

  • Astronomically high NRE (non-recurring engineering) cost: Millions of dollars just to reach first silicon.
  • Long time-to-market: Can take 6–24 months from design to tapeout.
  • Zero flexibility: Bugs in logic mean hardware re-spins.
  • High risk: A single design flaw can cost months of work and millions in losses.

Ideal for high-volume commercial products (e.g., smartphones, wireless chips), aerospace, medical devices, deep learning accelerators. Example: u-blox

Monday, April 14, 2025

First Step in Safety-Critical Software Development

The first action when developing safety-critical software is to add automatic commit checks for compiler warnings and reject the commit if any warnings are present. Enable the highest warning level and treat all warnings as errors.

Common Visual C++ warnings relevant to safety critical systems:

  1. C26451: Arithmetic overflow: Using operator 'op' on a value that may overflow the result (comes from Code Analysis with /analyze). Example: uint64_t c = a + b where a and b are of type uint32_t
  2. C4244: Conversion from ‘type1’ to ‘type2’, possible loss of data. Example: int → char or double → float
  3. C4018: Signed/unsigned mismatch. Can cause logic bugs and unsafe comparisons.
  4. C4701: Potentially uninitialized variable
  5. C4715: Not all control paths return a value
  6. C4013: 'function' undefined; assuming extern returning int
Of course, there is much more that needs to be done; in this blog post, I just wanted to focus on the first step from the perspective of a tech lead.

Saturday, April 12, 2025

UDP vs TCP

When working with real-time systems, it's important to understand how data is sent and received over UDP and TCP. The main reason to use UDP is that it can be 10x faster than TCP, but you have to be aware of its limitations.

UDP (User Datagram Protocol) sends data in discrete packets (called datagrams). Each call to sendto() on the sender side corresponds to exactly one recvfrom() on the receiver side.

  • No connection setup or teardown.
  • No built-in guarantees about delivery, order, or duplication (intermediate routers may retransmit packets if they think the first one was lost).
  • If you call sock.recvfrom(4) and the incoming packet is 9 bytes, you get the first 4 bytes—and the rest are discarded, i.e. you cannot get them with another receive call.

Rough Performance Advantage:

  • In short, bursty communications or real-time streaming, UDP can be 2x to 10x faster than TCP due to its lack of handshake, retransmission, and flow control mechanisms.
  • In my test case, UDP returned a DNS response in 42 ms, while TCP took 382 ms — nearly 9x faster.

TCP (Transmission Control Protocol) provides a continuous stream of bytes. It breaks your data into segments under the hood, but applications don’t see those packet boundaries.

  • Reliable: Guarantees delivery, order, and no duplication (if a duplicate packet is received, TCP automatically discards it).
  • Stream-oriented: You send bytes, not messages.
  • If you send b"Message 1", and call sock.recv(4), you might receive b"Mess", and then get the rest (b"age 1") in another call.
If the message might get corrupted during the creation phase rather than during transmission over the network, and you want to add a CRC to detect corruption at the application layer, then UDP might be better because it delivers the entire message, including the CRC, in a single recvfrom call.

Wednesday, February 12, 2025

Longitude convention and octal values

Recently, I encountered a bug in code that handled longitude values, where 33 degrees longitude is typically written as 033. Upon investigation, I discovered that Java was calculating 033 + 1 as 28 instead of 34. This happened because, when we write longitude values like "033" in geographic notation, we mean decimal 33 degrees. However, if we write it directly in Java, C++, or similar languages, it is interpreted as octal 33 (which equals decimal 27) due to the leading zero!

This is a classic example of why it's important to be careful when working with domain-specific number formats - what makes sense in geographic notation can have unexpected behavior when directly translated to programming language syntax!

Monday, February 10, 2025

Verifying Simulink generated C code

I generated embedded C code for the plant in the following MATLAB Simulink model:

To verify that the generated code behaves the same as the Simulink model, I could save the controller outputs to a file, create a C project with a main() function to read those outputs, and feed them to the plant C code at each time step—essentially converting a closed-loop simulation into an open-loop one. This approach would work if I use a single-step numerical integrator/solver like ode1 (Euler method). However, I usually prefer ode4 (Runge-Kutta 4) due to its better accuracy with less computation. Despite RK4 having 4 intermediate computations, it has a global error of O(h^4) and Euler O(h). If RK4 uses a step size of h, Euler would need h^4 to achieve similar accuracy. So, in total Euler would require more function evaluations than RK4—specifically, (1/h^3)/4 times more. For example, if hRK4 = 0.1, Euler would need 250x more computations.

Unfortunately, RK4 is a multi-step method with minor step sizes. Since the Simulink model saves controller outputs only at major steps, reading them from a file to drive the plant might result in divergent behavior—especially if the system has high-frequency dynamics—because controller outputs could differ at minor steps, while reading from the file only accounts for major step outputs.


You can only fully verify the system by integrating the C code into your embedded setup, which generates controller outputs based on real time plant-sensor data. Since testing on an embedded system takes time, to catch issues such as uninitialized variables in the plant code, you can create a C project, build and run your plant model with constant controller inputs. If it compiles and runs without NaN values, you can proceed to the embedded testing phase.

If you want to be more rigorous, you could save the plant outputs together with the controller outputs. In your C project, you read the controller output, feed it to the plant, run it for only one time step, read the Simulink plant output for that time instance from a file, and compare it with the C code plant output. Even this comparison is not perfect and only works for the first time step because Simulink integrator block outputs depend on the system state in the previous time step. To check the whole time span, you also have to save the system state and initialize the system with the correct state at each time step. But this in effect would be equal to running the system for one time step, because you initialize the system, apply a constant control input and check the output.