How to compile and link with other c/c++ programs

Hi Everyone,
I was trying to do a small exercise (compile a mlir function and call it from c after that).
So I used the following mlir code:

#id = affine_map<(d0) -> (d0)>
#ub = affine_map<(d0) -> (d0 + 64)>

// Map used to index the buffer while computing.
// CHECK-DAG: [[$MAP_IDENTITY:map[0-9]+]] = affine_map<(d0) -> (d0)>
// CHECK-DAG: [[$MAP_PLUS_128:map[0-9]+]] = affine_map<(d0) -> (d0 + 128)>

// CHECK-LABEL: func @matmul
// FILTER-LABEL: func @matmul
func @matmul(%A: memref<256x256xf32>, %B: memref<256x256xf32>, %C: memref<256x256xf32>) -> memref<256x256xf32> {
  affine.for %i = 0 to 256 step 64 {
    affine.for %j = 0 to 256 step 64 {
      affine.for %k = 0 to 256 step 64 {
        affine.for %ii = #id(%i) to #ub(%i) {
          affine.for %jj = #id(%j) to #ub(%j) {
            affine.for %kk = #id(%k) to #ub(%k) {
              %5 = affine.load %A[%ii, %kk] : memref<256x256xf32>
              %6 = affine.load %B[%kk, %jj] : memref<256x256xf32>
              %7 = affine.load %C[%ii, %jj] : memref<256x256xf32>
              %8 = arith.mulf %5, %6 : f32
              %9 = arith.addf %7, %8 : f32
              affine.store %9, %C[%ii, %jj] : memref<256x256xf32>
            }
          }
        }
      }
    }
  }
  return %C : memref<256x256xf32>
}

and lower it to llvm ir than compile it to generate object file.

mlir-opt --lower-affine -convert-scf-to-std --convert-arith-to-llvm --convert-std-to-llvm  --convert-memref-to-llvm matmul.mlir > matmul_llvm.mlir
mlir-opt -reconcile-unrealized-casts matmul_llvm.mlir > matmul_reconcil.mlir
mlir-translate -mlir-to-llvmir matmul_reconcil.mlir>matmul.ir

llvm-as matmul.ir -o matmul.bc
llc -filetype=obj matmul.bc -o matmul.o

But when I check the signature of matmul in matmul.o by using this command:

nm -C matmul.o

I got

0000000000000000 T matmul

am I doing something wrong ?

It does not seem wrong, what else did you expect?

I expect to have

0000000000000000 T matmul(float *, float *, float *);

if not what should be the protoype of the function to be called from my c program ??

This isn’t how object files work: the native object on Linux (same for Windows/Mac) don’t know anything about the original language the binary comes from. The only thing you have is the binary blob + a symbol table that is “name to address” resolution.
Some extra meta-data (like Dwarf debug info) could help reconstructing the higher-level signature, but that’s just debug info (so best effort, and often stripped out).

Have you tried with a C file compiled with clang or gcc to see what is in the symbol table?

Hi @mehdi_amini

Yes I tried a compiled C file with gcc, for example this code:

void matmul(float * A, float *B, float *C) {
 ....
}

so the command nm -C gives:

0000000000000000 T matmul(float*, float*, float*)

same thing with clang.

I succeed to compile. I had to declare my function as extern. But now, I am having a segmentation fault.
To debug it I simplified the mlir matmul function as follows:

func @matmul(%A: memref<512x512xf32>, %B: memref<512x512xf32>, %C: memref<512x512xf32>) -> memref<512x512xf32> {

  affine.for %i= 0 to 512 step 1 {
    affine.for %j =0 to 512 step 1 { 
      %8 = arith.constant 1.5: f32
      affine.store %8, %C[%i,%j]: memref<512x512xf32>
    }
  }
  return %C : memref<512x512xf32>
}

and my C program is :

#include <iostream>

using namespace std;

static const int N=512;
extern "C" {void matmul(float *A, float *B,float *C );}


int main(){

    float *A = new float[N*N],  *B= new float[N*N], *C= new float[N*N];
    for(int i=0;i<N*N;i++){
        A[i] = 1.;
        B[i] = 1.;
        C[i] = 0.;
    }
    matmul(A,B,C);

    cout<<C[0]<<endl;

    return 0;
}

I have a segmentation fault in matmul. I think even that I can compile the whole program the signature of matmul that I am providing on my c program is not correct

Memref is a not pointer, yet your code tries to pass a pointer instead. This segfaults.

Take a look at LLVM IR Target - MLIR and at the generated LLVM IR to understand what you are expected to pass in. There is also an attribute to emit more human-friendly ABI for memrefs and a bench of helpers in CRunnerUtils.h.

1 Like

I believe you compiled a C++ file and not a C file here. If you remove the -C option and you will see the real name for the symbol.

As Alex said, the issue is the signature. We should probably add a standalone example doing this in the examples folder though: patches welcome! :wink:

thank you @mehdi_amini and @ftynse, I found the right signature.
@mehdi_amini yes you are right it is C++.

It took me a little bit of time to figure this one out, so I’m clarifying my steps here. It might be valuable for me to add this to the docs, however on the latest version of MLIR I encountered an issue compiling (see bottom of reply for details).

However, on LLVM commit 90d1786ba I have it working. As a basic example I have the following C++ and MLIR files, as well as compilation process. The goal is for the MLIR function to change the value of one of our array elements.

I recommend anyone looking at this comment to be aware that you need to create and pass a MemRefDescriptor struct to your external function, and reading the LLVM IR Target - MLIR docs is worthwhile.

#include <iostream>

template <typename T, size_t N> struct MemRefDescriptor {
  T *allocated;
  T *aligned;
  intptr_t offset;
  intptr_t sizes[N];
  intptr_t strides[N];
};

extern "C" {
void *_mlir_ciface_change_value(MemRefDescriptor<int64_t, 1> input);
}

int main(int argc, char *argv[]) {
  int64_t *a = new int64_t[10];
  a[0] = 1;

  MemRefDescriptor<int64_t, 1> memref = {
      a,    // allocated
      a,    // aligned
      0,    // offset
      {10}, // sizes[N]
      {1},  // strides[N]
  };
  std::cout << "a[0] pre-call: " << a[0] << std::endl;
  _mlir_ciface_change_value(memref);
  std::cout << "a[0] post-call: " << a[0] << std::endl;
  return 0;
}

MLIR function

func @change_value(%0: memref<10xi64>) -> (memref<10xi64>) {
     %c1 = arith.constant 2 : i64
     affine.store %c1, %0[0] :  memref<10xi64>
     return %0 : memref<10xi64>
}

Shell commands:

cat vector_print.mlir |
	${LLVM_BUILD}/bin/mlir-opt \
	    --lower-affine \
	    --convert-arith-to-llvm \
	    --convert-scf-to-std \
	    --convert-memref-to-llvm \
    --convert-std-to-llvm='emit-c-wrappers=1' \
	--canonicalize -reconcile-unrealized-casts > value.mlir

$LLVM_BUILD/bin/mlir-translate -mlir-to-llvmir value.mlir > value.ir

llvm-as value.ir -o value.bc

llc -filetype=obj value.bc -o value.o

clang++ value.o main.cc

./a.out

When using a more recent version of MLIR (commit 5182839e), I fail with:

$ llvm-as value.ir -o value.bc
llvm-as: value.ir:4:9: error: expected type
declare ptr @malloc(i64)

My optimization passes are a little different:

cat vector_print.mlir |
	${US_LLVM}/bin/mlir-opt \
	--lower-affine \
    --convert-scf-to-cf \
	--convert-memref-to-llvm \
    --convert-func-to-llvm \
    --convert-cf-to-llvm=emit-c-wrappers=1 \
	--canonicalize \
    --reconcile-unrealized-casts > value.mlir

${US_LLVM}/bin/mlir-translate -mlir-to-llvmir value.mlir > value.ir

llvm-as value.ir -o value.bc
1 Like

There are C++ equivalents of the descriptor here: llvm-project/CRunnerUtils.h at 894c0e94f9c62413feef88fd577c430839abaea7 · llvm/llvm-project · GitHub

Note that the list of conversions to run is highly dependent on the mix of dialects originally present in the IR.

This isn’t really necessary, both llc and clang accept LLVM IR text files. Using the .ll extension instead of .ir may help.

You must use the same version of MLIR and LLVM. Compile them at the same commit instead of taking the “system” version of LLVM. Textual LLVM IR changes over time and is not backwards compatible.

Great, thanks it makes sense that clang can read LLVM IR text files directly. I avoid the issue by using the llc build in my same LLVM commit, and not using llvm-as.

However I’ve found on the newer version of LLVM I’m having trouble emitting the C-wrappers.

In the 2021 version, I used the pass --convert-std-to-llvm='emit-c-wrappers=1', but in the 2022 version I get an error, which says mlir-opt: Did you mean '--convert-cf-to-llvm=emit-c-wrappers=1'?.

Using this pass, it fails with <Pass-Options-Parser>: no such option emit-c-wrappers, which is may be a bug, at least on the mlir-opt documentation side.

Looking at mlir-opt --help for the 2022 version, I see there is a pass --llvm-request-c-wrappers - Request C wrapper emission for all functions.

However, when I use this and compile down to an object file, I see that the interface is not created:

$ nm value.o
0000000000000000 T change_value

There is no “std” anymore. There were several discussions on this forum as to the split and individual dialects if you are interested.

One shouldn’t assume the “did you mean” hint gives them the intended replacement pass. It merely gives the syntactically closest (in ±edit-distance sense) pass name in case the caller made a typo. You very likely need more than one pass instead of std-to-llvm, as mentioned above.

It does what its description says: it requests C wrapper emission by adding the attribute mentioned in the documentation LLVM IR Target - MLIR, it doesn’t actually emit the wrappers. convert-func-to-llvm does. Here’s a test llvm-project/emit-c-wrappers-for-external-functions.mlir at main · llvm/llvm-project · GitHub for exactly that. Tests are generally a good source of truth on how to run things.