Repository: mun-lang/mun (language: rust)

Issue:
Implement compatibility checks between munlib and runtime
**Problem**

Currently, the runtime tries to load a `*.munlib` with the assumption that the ABI memory layout of the `*.munlib` matches its own ABI version. This can cause undefined behaviour when it tries to load a `*.munlib` with a different ABI version.

**Solution**

By including an ABI version in the `*.munlib`, the runtime can gracefully fail - reporting an error to the end user.

In the future - once we support backwards compatibility, we can even support loading of several different ABI versions using this functionality.

**Tasks**

- [x] add an integer `ABI_VERSION` constant to the `mun_abi` crate.
- [x] [generate](https://github.com/mun-lang/mun/blob/0d6e36d64376289a8969c62cefdc080ee99c2a51/crates/mun_codegen/src/code_gen/symbols.rs#L291) a `get_version` IR function
- [x] [export](https://github.com/mun-lang/mun/blob/c17448d77f9751e74ce90ea49cd8503b35459d8e/crates/mun_codegen/src/code_gen/linker.rs#L176) the `get_version` function
- [x] in the [runtime](https://github.com/mun-lang/mun/blob/master/crates/mun_runtime/src/assembly.rs#L35), call the `get_version` function and verify that the runtime's `mun_abi::ABI_VERSION` is compatible.
- [x] (bonus) add a constant with the `get_version` function name (and other API functions) to the `mun_abi` crate and use that constant in both the `mun_codegen` and `mun_runtime` crates - to avoid discrepancies.

**Good first issue process:**

- [x] [read our contributing guidelines](https://github.com/mun-lang/mun/blob/master/CONTRIBUTING.md)
- [x] claim this issue (assign yourself or comment below)
- [x] implement the feature and start a Pull Request with in the description: closes #188. 

If this is your first PR, welcome 🎉 😄


Discussion / hints:
I would like to tackle this issue and implement the feature. This will be my first ever PR.
Welcome and thank you for working on this PR. If you have any questions, please let us know.
After implementing this,  the following tests fail:
```
tests::abi_is_fresh
tests::runtime_capi_is_fresh
```
They require me to update the abi and the runtime-capi but doing so requires me to download both the [ffi](https://github.com/mun-lang/runtime-ffi/tree/ac61e99025ece3a8105bf95a7a1ff70506f74bd3) and the [abi-c](https://github.com/mun-lang/abi-c/tree/4bccc48f4c782fa06c1ae911b2f7bf36e587280c) submodules. What should i do?
You should always download the submodules, otherwise the tests will fail. Our install instructions describe how to do it, but it's just a single command:
```bash
git submodule update --init --recursive
```

Fix patch:
diff --git a/crates/mun_abi/src/lib.rs b/crates/mun_abi/src/lib.rs
index b20c489e0..08a817170 100644
--- a/crates/mun_abi/src/lib.rs
+++ b/crates/mun_abi/src/lib.rs
@@ -25,6 +25,16 @@ pub mod prelude {
     };
 }
 
+/// Defines the current abi version
+#[allow(clippy::zero_prefixed_literal)]
+pub const ABI_VERSION: u32 = 00_02_00;
+/// Defines the name for the `get_info` function
+pub const GET_INFO_FN_NAME: &str = "get_info";
+/// Defines the name for the `get_version` function
+pub const GET_VERSION_FN_NAME: &str = "get_version";
+/// Defines the name for the `set_allocator_handle` function
+pub const SET_ALLOCATOR_HANDLE_FN_NAME: &str = "set_allocator_handle";
+
 /// Represents the kind of memory management a struct uses.
 #[repr(u8)]
 #[derive(Debug, Clone, PartialEq, Eq)]
diff --git a/crates/mun_codegen/src/code_gen/linker.rs b/crates/mun_codegen/src/code_gen/linker.rs
index 2544e3082..222c8b687 100644
--- a/crates/mun_codegen/src/code_gen/linker.rs
+++ b/crates/mun_codegen/src/code_gen/linker.rs
@@ -173,8 +173,11 @@ impl Linker for MsvcLinker {
 
         self.args.push("/DLL".to_owned());
         self.args.push("/NOENTRY".to_owned());
-        self.args.push("/EXPORT:get_info".to_owned());
-        self.args.push("/EXPORT:set_allocator_handle".to_owned());
+        self.args.push(format!("/EXPORT:{}", abi::GET_INFO_FN_NAME));
+        self.args
+            .push(format!("/EXPORT:{}", abi::GET_VERSION_FN_NAME));
+        self.args
+            .push(format!("/EXPORT:{}", abi::SET_ALLOCATOR_HANDLE_FN_NAME));
         self.args.push(format!("/IMPLIB:{}", dll_lib_path_str));
         self.args.push(format!("/OUT:{}", dll_path_str));
         Ok(())
diff --git a/crates/mun_codegen/src/code_gen/symbols.rs b/crates/mun_codegen/src/code_gen/symbols.rs
index bf578b168..5c7fb6cbd 100644
--- a/crates/mun_codegen/src/code_gen/symbols.rs
+++ b/crates/mun_codegen/src/code_gen/symbols.rs
@@ -232,6 +232,7 @@ pub(super) fn gen_reflection_ir(
     // Construct the actual `get_info` function
     gen_get_info_fn(db, context, module_info, dispatch_table);
     gen_set_allocator_handle_fn(db, context);
+    gen_get_version_fn(db, context);
 }
 
 /// Construct the actual `get_info` function.
@@ -348,3 +349,19 @@ fn gen_set_allocator_handle_fn(db: &impl IrDatabase, context: &IrValueContext) {
 
     builder.build_return(None);
 }
+
+/// Generates a `get_version` method that returns the current abi version.
+/// Specifically, it returns the abi version the function was generated in.
+fn gen_get_version_fn(db: &impl IrDatabase, context: &IrValueContext) {
+    let get_version_fn = context.module.add_function(
+        abi::GET_VERSION_FN_NAME,
+        Value::<fn() -> u32>::get_ir_type(context.type_context),
+        Some(Linkage::DLLExport),
+    );
+
+    let builder = db.context().create_builder();
+    let body_ir = db.context().append_basic_block(&get_version_fn, "body");
+    builder.position_at_end(&body_ir);
+
+    builder.build_return(Some(&abi::ABI_VERSION.as_value(context).value));
+}
diff --git a/crates/mun_runtime/src/assembly.rs b/crates/mun_runtime/src/assembly.rs
index 868eb7b3a..a4bc1774b 100644
--- a/crates/mun_runtime/src/assembly.rs
+++ b/crates/mun_runtime/src/assembly.rs
@@ -33,10 +33,25 @@ impl Assembly {
 
         // Check whether the library has a symbols function
         let get_info: Symbol<'_, extern "C" fn() -> AssemblyInfo> =
-            unsafe { library.library().get(b"get_info") }?;
-
-        let set_allocator_handle: Symbol<'_, extern "C" fn(*mut std::ffi::c_void)> =
-            unsafe { library.library().get(b"set_allocator_handle") }?;
+            unsafe { library.library().get(abi::GET_INFO_FN_NAME.as_bytes()) }?;
+
+        let get_version: Symbol<'_, extern "C" fn() -> u32> =
+            unsafe { library.library().get(abi::GET_VERSION_FN_NAME.as_bytes()) }?;
+
+        let set_allocator_handle: Symbol<'_, extern "C" fn(*mut std::ffi::c_void)> = unsafe {
+            library
+                .library()
+                .get(abi::SET_ALLOCATOR_HANDLE_FN_NAME.as_bytes())
+        }?;
+
+        let version = get_version();
+        if abi::ABI_VERSION != version {
+            return Err(anyhow::anyhow!(
+                "ABI version mismatch. munlib is `{}` but runtime is `{}`",
+                version,
+                abi::ABI_VERSION
+            ));
+        }
 
         let allocator_ptr = Arc::into_raw(gc.clone()) as *mut std::ffi::c_void;
         set_allocator_handle(allocator_ptr);


Test patch: