Skip to main content

Pitfalls in Multi-Language Software Projects

Why do Multi-Language SW Projects Exist?

In modern software development, using multiple programming languages within a single project isn't just common — it's often essential, because it lets you

  • Choose the best tool for each part of the project, for example, Rust for its safety, Python for its dynamic nature and extensibility, Java/C# for server-side business logic, and JavaScript for the client side,
  • Integrate with existing libraries, components and systems,
  • Optimize for performance where necessary,
  • Adhere to platform constraints, such as web, mobile or embedded systems.

In this post, we explore mechanisms for enabling cross-language communication, using C, C# and Rust as examples.

How are Programming Languages Connected?

Many languages provide a foreign function interface (FFI), that handles passing of parameters and return values, as well as errors or exceptions. In C# and Rust the complexity is usually hidden behind convenient syntax.

The following code snippet shows the implementation of a function that adds two numbers in C:


// C implementation
int calc_sum(int a, int b)
{
    return a + b;
}

Note: This is meant to be a simple example to show the simplicity of FFI declarations. In an actual software project, it wouldn’t make sense to implement simple operations such as addition via FFI because the overhead of such a call is usually much larger than just performing the operation using the built-in mechanism of C# or Rust.

This function can be directly called from C# or Rust by simply declaring it in the other project via :

// C# P/Invoke import
[DllImport("library", EntryPoint = "calc_sum")]
static extern int CalcSum(int a, int b);

// Rust FFI import
unsafe extern "C" {
    fn calc_sum(a: i32, b: i32) -> i32;
}

However, it’s important to note that these declarations are independent of each other and that it’s the developer’s responsibility to ensure that the order of arguments, signatures and types used in C# and Rust match the ones in the C implementation signature.

Type Layout: Passing Data From Rust to C and Back Again

When passing primitive values from Rust to C the values are passed as-is. When using complex types such as structures and unions, Rust allows the developer to specify the representation.

The following snippet defines a structure that consists of a 2-byte, a 1-byte and a 4-byte integer, which uses a C-compatible representation via #[repr(C)]:

#[repr(C)]
struct ThreeInts {
    first: i16,
    second: i8,
    third: i32
}

Another example would be Rust reference types: They are essentially non-nullable pointers but must be passed to or returned from C functions using nullable pointers. To avoid handling the null case directly, which is unsafe and can lead to undefined behavior, Rust guarantees that nullable C pointers are compatible with the safe Rust Option<&T> type. Null values at the language boundary are then automatically converted to an  Option::None value in Rust.

Marshalling: Passing Data From C# to C and Back Again

“Marshalling” is the process of transforming the memory representation of a C# object in such a way that data is interpreted correctly across boundaries.

For example, the C# bool type, having the values true or false, is using 1 byte of memory internally where true is encoded as 0 and false is any other value. When calling Windows API functions that accept a parameter of type BOOL this would be translated to a value that is 4 bytes in size where false is encoded as 0 and true is any other value.

Via the StructLayoutAttribute C# offers a way for developers to specify how a structure should be laid out in memory to be compatible with the receiving C function.

Pitfalls with Type Layout and Marshalling

Foreign function interfaces define standard rules for type layouts and marshalling but allow for different rules to be specified (for example, using the MarshalAsAttribute in C#), if necessary. This and the necessity to repeat the declaration in each language may lead to mismatches, especially when the definition changes , but consuming declarations are not updated accordingly.

For example, given the following C function and matching C# declaration:

void *create_foo(int foo_creation_flags);

// C# P/Invoke import
[DllImport("library", EntryPoint = "create_foo")]
static extern nint CreateFoo(FooCreationFlags flags);

If, in a new version of the library, the function is then changed to

void create_foo(int foo_creation_flags, void **result_obj);

there would be no compile-time error, but a runtime failure would occur.


Depending on the kind of signature mismatch, possible errors include:

  • Crash: The application stops working and is no longer usable.
  • Loss of data: Values are truncated or misinterpreted, leading to false results.

It’s possible for such a problem to go unnoticed in development and testing, because it may just appear to work properly by accident.

Note that in the above example, FooCreationFlags is an enum that needs to be manually declared and maintained. Any changes in the C definition must be manually applied to the C# definition and failure to do so, will lead to runtime failures or bugs.

The same applies to Rust foreign function interfaces, especially when using structures. Missing or reordered fields in one structure definition will go unnoticed until runtime or cause the program to produce wrong results silently.

Can the Rust or C# Compiler Detect These Issues?

Unfortunately, no. In Rust FFI function declarations are always considered “unsafe” and the responsibility of ensuring correctness is placed on the developer. Even in C# where it is possible to declare P/Invoke imports without using “unsafe” types, i.e., pointers, there exists no built-in compile-time checking, because the compiler only has access to the information given in source code and it does not understand or check the original definition in C .

Discover More

For additional insights into our architecture verification and static code analysis toolsplease visit our website.

If you have any questions or wish to schedule a demo, please reach out to us.

To stay up to date with our latest product news and events: Sign Up For Our Newsletter.

 

Comments