Getting low – implementing C

One of the most interesting developments in language design in the past decade or two is the desire to implement lower-level languages and language features via API. Java lets you do this purely externally, and Python provides a C library for interaction between the languages. It warrants mentioning that the reasons for doing this vary—among them applying Go's concurrency features as a wrapper for legacy C code—and you will likely have to deal with some of the memory management associated with introducing unmanaged code to garbage-collected applications.

Go takes a hybrid approach, allowing you to call a C interface through an import, which requires a frontend compiler such as GCC:

import "C"

So why would we want to do this?

There are some good and bad reasons to implement C directly in your project. An example of a good reason might be to have direct access to the inline assembly, which you can do in C but not directly in Go. A bad reason could be any that has a solution inherent in Golang itself.

To be fair, even a bad reason is not bad if you build your application reliably, but it does impose an additional level of complexity to anyone else who might use your code. If Go can satisfy the technical and performance requirements, it's always better to use a single language in a single project.

There's a famous quote from C++ creator Bjarne Stroustrup on C and C++:

C makes it easy to shoot yourself in the foot; C++ makes it harder, but when you do, it blows your whole leg off.

Jokes aside (Stroustrup has a vast collection of such quips and quotes), the fundamental reasoning is that the complexity of C often prevents people from accidentally doing something catastrophic.

As Stroustrup says, C makes it easy to make big mistakes, but the repercussions are often smaller due to language design than higher-level languages. Issues dealing with security and stability are easy to be introduced in any low-level language.

By simplifying the language, C++ provides abstractions that make low-level operations easier to carry out. You can see how this might apply to using C directly in Go, given the latter language's syntactical sweetness and programmer friendliness.

That said, working with C can highlight some of the potential pitfalls with regard to memory, pointers, deadlocks, and consistency, so we'll touch upon a simple example as follows:

package main

// #include <stdio.h>
// #include <string.h>
//  int string_length (char* str) {
//    return strlen(str);
//  }
import "C"
import "fmt"
func main() {
  v := C.CString("Don't Forget My Memory Is Not Visible To Go!")
  x := C.string_length(v)
  fmt.Println("A C function has determined your string 
    is",x,"characters in length")
}

Touching memory in cgo

The most important takeaway from the preceding example is to remember that anytime you go into or out of C, you need to manage memory manually (or at least more directly than with Go alone). If you've ever worked in C (or C++), you know that there's no automatic garbage collection, so if you request memory space, you must also free it. Calling C from Go does not preclude this.

The structure of cgo

Importing C into Go will take you down a syntactical side route, as you probably noticed in the preceding code. The first thing that will appear glaringly different is the actual implementation of C code within your application.

Any code (in comments to stop Go's compiler from failing) directly above the import "C" directive will be interpreted as C code. The following is an example of a C function declared above our Go code:

/*
  int addition(int a, int b) {
    return a + b;
  }

Bear in mind that Go won't validate this, so if you make an error in your C code, it could lead to silent failure.

Another related warning is to remember your syntax. While Go and C share a lot of syntactical overlap, leave off a curly bracket or a semicolon and you could very well find yourself in one of those silent failure situations. Alternately, if you're working in the C part of your application and you go back to Go, you will undoubtedly find yourself wrapping loop expressions in parentheses and ending your lines with semicolons.

Also remember that you'll frequently have to handle type conversions between C and Go that don't have one-to-one analogs. For example, C does not have a built-in string type (you can, of course, include additional libraries for types), so you may need to convert between strings and char arrays. Similarly, int and int64 might need some nonimplicit conversion, and again, you may not get the debugging feedback that you might expect when compiling these.

The other way around

Using C within Go is obviously a potentially powerful tool for code migration, implementing lower-level code, and roping in other developers, but what about the inverse? Just as you can call C from within Go, you can call Go functions as external functions within your embedded C.

The end game here is the ability to work with and within C and Go in the same application. By far the easiest way to handle this is by using gccgo, which is a frontend for GCC. This is different than the built-in Go compiler; it is possible to go back and forth between C and Go without gccgo, but using it makes this process much simpler.

gopart.go

The following is the code for the Go part of the interaction, which the C part will call as an external function:

package main

func MyGoFunction(num C.int) int {

  squared := num * num
  fmt.Println(num,"squared is",squared)
  return squared
}

cpart.c

Now for the C part, where we make our call to our Go application's exported function MyGoFunction, as shown in the following code snippet:

#include <stdio.h>

extern int square_it(int) __asm__ ("cross.main.MyGoFunction")

int main() {
  
  int output = square_it(5)
  printf("Output: %d",output)
  return 0;
}

Makefile

Unlike using C directly in Go, at present, doing the inverse requires the use of a makefile for C compilation. Here's one that you can use to get an executable from the earlier simple example:

all: main

main: cpart.o cpart.c
    gcc cpart.o cpart.c -o main

gopart.o: gopart.go
    gccgo -c gopart.go -o gopart.o -fgo-prefix=cross

clean:
    rm -f main *.o

Running the makefile here should produce an executable file that calls the function from within C.

However, more fundamentally, cgo allows you to define your functions as external functions for C directly:

package output

import "C"

//export MyGoFunction
func MyGoFunction(num int) int {

  squared := num * num
  return squared
}

Next, you'll need to use the cgo tool directly to generate header files for C as shown in the following line of code:

go tool cgo goback.go

At this point, the Go function is available for use in your C application:

#include <stdio.h>
#include "_obj/_cgo_export.h"

extern int MyGoFunction(int num);

int main() {
  
  int result = MyGoFunction(5);
  printf("Output: %d",result);
  return 0;

}

Note that if you export a Go function that contains more than one return value, it will be available as a struct in C rather than a function, as C does not provide multiple variables returned from a function.

At this point, you may be realizing that the true power of this functionality is the ability to interface with a Go application directly from existing C (or even C++) applications.

While not necessarily a true API, you can now treat Go applications as linked libraries within C apps and vice versa.

One caveat about using //export directives: if you do this, your C code must reference these as extern-declared functions. As you may know, extern is used when a C application needs to call a function from another linked C file.

When we build our Go code in this manner, cgo generates the header file _cgo_export.h, as you saw earlier. If you want to take a look at that code, it can help you understand how Go translates compiled applications into C header files for this type of use:

/* Created by cgo - DO NOT EDIT. */
#include "_cgo_export.h"

extern void crosscall2(void (*fn)(void *, int), void *, int);

extern void _cgoexp_d133c8d0d35b_MyGoFunction(void *, int);

GoInt64 MyGoFunction(GoInt p0)
{
  struct {
    GoInt p0;
    GoInt64 r0;
  } __attribute__((packed)) a;
  a.p0 = p0;
  crosscall2(_cgoexp_d133c8d0d35b_MyGoFunction, &a, 16);
  return a.r0;
}

You may also run into a rare scenario wherein the C code is not exactly as you expect, and you're unable to cajole the compiler to produce what you expect. In that case, you're always free to modify the header file before the compilation of your C application, despite the DO NOT EDIT warning.

Getting even lower – assembly in Go

If you can shoot your foot off with C and you can blow your leg off with C++, just imagine what you can do with assembly in Go.

It isn't possible to use assembly directly in Go, but as Go provides access to C directly and C provides the ability to call inline assembly, you can indirectly use it in Go.

But again, just because something is possible doesn't mean it should be done—if you find yourself in need of assembly in Go, you should consider using assembly directly and connecting via an API.

Among the many roadblocks that you may encounter with assembly in (C and then in) Go is the lack of portability. Writing inline C is one thing—your code should be relatively transferable between processor instruction sets and operating systems—but assembly is obviously something that requires a lot of specificity.

All that said, it's certainly better to have the option to shoot yourself in the foot whether you choose to take the shot or not. Use great care when considering whether you need C or assembly directly in your Go application. If you can get away with communicating between dissonant processes through an API or interprocess conduit, always take that route first.

One very obvious drawback of using assembly in Go (or on its own or in C) is you lose the cross-compilation capabilities that Go provides, so you'd have to modify your code for every destination CPU architecture. For this reason, the only practical times to use Go in C are when there is a single platform on which your application should run.

Here's an example of what an ASM-in-C-in-Go application might look like. Keep in mind that we've included no ASM code, because it varies from one processor type to another. Experiment with some boilerplate assembly in the following __asm__ section:

package main

/*
#include <stdio.h>

void asmCall() {

__asm__( "" );
    printf("I come from a %s","C function with embedded asm
");

}
*/
import "C"

func main() {
    
    C.asmCall()

}

If nothing else, this may provide an avenue for delving deeper into ASM even if you're familiar with neither assembly nor C itself. The more high-level you consider C and Go to be, the more practical you might see this.

For most uses, Go (and certainly C) is low-level enough to be able to squeeze out any performance hiccups without landing at assembly. It's worth noting again that while you do lose some immediate control of memory and pointers in Go when you invoke C applications, that caveat applies tenfold with assembly. All of those nifty tools that Go provides may not work reliably or not work at all. If you think about the Go race detector, consider the following application:

package main

/*
int increment(int i) {
  i++;
  return i;
}
*/
import "C"
import "fmt"

var myNumber int

func main() {
  fmt.Println(myNumber)
  
  for i:=0;i<100;i++ {
    myNumber = int( C.increment(C.int(myNumber)) )
    fmt.Println(myNumber)
  }

}

You can see how tossing your pointers around between Go and C might leave you out in the dark when you don't get what you expect out of the program.

Keep in mind that here there is a somewhat unique and perhaps unexpected kicker to using goroutines with cgo; they are treated by default as blocking. This isn't to say that you can't manage concurrency within C, but it won't happen by default. Instead, Go may well launch another system thread. You can manage this to some degree by utilizing the runtime function runtime.LockOSThread(). Using LockOSThread tells Go that a particular goroutine should stay within the present thread and no other concurrent goroutine may use this thread until runtime.UnlockOSThread() is called.

The usefulness of this depends heavily on the necessity to call C or a C library directly; some libraries will play happily as new threads are created, a few others may segfault.

Note

Another useful runtime call you should find useful within your Go code is NumGcoCall(). This returns the number of cgo calls made by a current process. If you need to lock and unlock threads, you can also use this to build an internal queue report to detect and prevent deadlocks.

None of this precludes the possibility of race conditions should you choose to mix and match Go and C within goroutines.

Of course, C itself has a few race detector tools available. Go's race detector itself is based on the ThreadSanitizer library. It should go without saying that you probably do not want several tools that accomplish the same thing within a single project.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.48.204