Skip to content

Commit

Permalink
Merge pull request #93 from Concordium/better-ffi-error-handling
Browse files Browse the repository at this point in the history
FFI error handling
  • Loading branch information
Søren Schwartz authored Jan 30, 2024
2 parents ac603cf + b65b160 commit c5deb35
Show file tree
Hide file tree
Showing 17 changed files with 412 additions and 83 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Unreleased changes

## 4.3.1
- Added
- Enhanced error handling for FFI calls and exposing detailed exceptions to the client, providing information about potential errors.

## 4.3.0
- Bugfix
- Switched the GitHub runners from using 'ubuntu-latest' to 'ubuntu-20.04' to ensure compatibility with the default .NET 6 Docker image for the SDK.
Expand Down
2 changes: 1 addition & 1 deletion concordium-base
Submodule concordium-base updated 187 files
41 changes: 23 additions & 18 deletions rust-bindings/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions rust-bindings/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ name="rust_bindings"
crate-type = ["cdylib"]

[dependencies]
thiserror = "1.0"
anyhow="1.0"
serde_json = "1.0"
serde = { version = "1.0" }
concordium-contracts-common = { version = "8.1.1", features = ["derive-serde"], path = "../concordium-base/smart-contracts/contracts-common/concordium-contracts-common" }
concordium-contracts-common = { version = "9.0.0", features = ["derive-serde"], path = "../concordium-base/smart-contracts/contracts-common/concordium-contracts-common" }

[dev-dependencies]
hex = "0.4"

[profile.release]
rpath = true
rpath = true
95 changes: 72 additions & 23 deletions rust-bindings/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
use anyhow::{anyhow, Result};
use anyhow::Result;
use concordium_contracts_common::{
schema::{Type, VersionedModuleSchema},
schema::{Type, VersionedModuleSchema, VersionedSchemaError},
schema_json::ToJsonError,
Cursor,
};
use serde_json::to_vec;
use std::{ffi::CStr, os::raw::c_char};
use thiserror::Error;

pub type JsonString = String;

Expand Down Expand Up @@ -49,7 +51,7 @@ pub unsafe extern "C" fn schema_display(
schema_size: i32,
schema_version: FFIByteOption,
callback: ResultCallback,
) -> bool {
) -> u16 {
let schema = std::slice::from_raw_parts(schema_ptr, schema_size as usize);
assign_result(callback, || {
schema_display_aux(schema, schema_version.into_option())
Expand Down Expand Up @@ -88,7 +90,7 @@ pub unsafe extern "C" fn get_receive_contract_parameter(
value_ptr: *const u8,
value_size: i32,
callback: ResultCallback,
) -> bool {
) -> u16 {
assign_result(callback, || {
let schema = std::slice::from_raw_parts(schema_ptr, schema_size as usize);
let contract_name_str = get_str_from_pointer(contract_name)?;
Expand Down Expand Up @@ -132,7 +134,7 @@ pub unsafe extern "C" fn get_event_contract(
value_ptr: *const u8,
value_size: i32,
callback: ResultCallback,
) -> bool {
) -> u16 {
assign_result(callback, || {
let schema = std::slice::from_raw_parts(schema_ptr, schema_size as usize);
let contract_name_str = get_str_from_pointer(contract_name)?;
Expand All @@ -158,20 +160,20 @@ pub unsafe extern "C" fn get_event_contract(
/// # Returns
///
/// A boolean, that indicates whether the computation was successful or not.
fn assign_result<F: FnOnce() -> Result<Vec<u8>>>(callback: ResultCallback, f: F) -> bool {
fn assign_result<F: FnOnce() -> Result<Vec<u8>, FFIError>>(callback: ResultCallback, f: F) -> u16 {
match f() {
Ok(output) => {
let out_lenght = output.len() as i32;
let ptr = output.as_ptr();
callback(ptr, out_lenght);
true
0
}
Err(e) => {
let error = format!("{}", e).into_bytes();
let error_length = error.len() as i32;
let ptr = error.as_ptr();
callback(ptr, error_length);
false
e.to_int()
}
}
}
Expand All @@ -182,45 +184,92 @@ pub fn get_receive_contract_parameter_aux(
contract_name: &str,
entrypoint: &str,
value: &[u8],
) -> Result<Vec<u8>> {
) -> Result<Vec<u8>, FFIError> {
let module_schema = VersionedModuleSchema::new(schema, &schema_version)?;
let parameter_type = module_schema.get_receive_param_schema(contract_name, entrypoint)?;
let deserialized = deserialize_type_value(value, &parameter_type, true)?;
let deserialized = deserialize_type_value(value, &parameter_type)?;
Ok(deserialized)
}

fn schema_display_aux(schema: &[u8], schema_version: Option<u8>) -> Result<Vec<u8>> {
fn schema_display_aux(schema: &[u8], schema_version: Option<u8>) -> Result<Vec<u8>, FFIError> {
let display = VersionedModuleSchema::new(schema, &schema_version)?;
Ok(display.to_string().into_bytes())
}

#[derive(Error, Debug)]
pub enum FFIError {
#[error("{0}")]
JsonError(String),
#[error("error when using serde")]
SerdeJsonError,
#[error("encountered string which wasn't utf8 encoded")]
Utf8Error,
#[error(transparent)]
VersionedSchemaError(#[from] VersionedSchemaError),
}

impl FFIError {
/// The enumeration starts a 1 since return value 0 indicating a successfull
/// FFI call.
fn to_int(&self) -> u16 {
match self {
FFIError::JsonError(_) => 1,
FFIError::SerdeJsonError => 2,
FFIError::Utf8Error => 3,
FFIError::VersionedSchemaError(schema_error) => match schema_error {
VersionedSchemaError::ParseError => 4,
VersionedSchemaError::MissingSchemaVersion => 5,
VersionedSchemaError::InvalidSchemaVersion => 6,
VersionedSchemaError::NoContractInModule => 7,
VersionedSchemaError::NoReceiveInContract => 8,
VersionedSchemaError::NoInitInContract => 9,
VersionedSchemaError::NoParamsInReceive => 10,
VersionedSchemaError::NoParamsInInit => 11,
VersionedSchemaError::NoErrorInReceive => 12,
VersionedSchemaError::NoErrorInInit => 13,
VersionedSchemaError::ErrorNotSupported => 14,
VersionedSchemaError::NoReturnValueInReceive => 15,
VersionedSchemaError::ReturnValueNotSupported => 16,
VersionedSchemaError::NoEventInContract => 17,
VersionedSchemaError::EventNotSupported => 18,
},
}
}
}

impl From<std::str::Utf8Error> for FFIError {
fn from(_: std::str::Utf8Error) -> Self { FFIError::Utf8Error }
}

impl From<serde_json::Error> for FFIError {
fn from(_: serde_json::Error) -> Self { FFIError::SerdeJsonError }
}

impl From<ToJsonError> for FFIError {
fn from(value: ToJsonError) -> Self { FFIError::JsonError(value.display(true)) }
}

fn get_event_contract_aux(
schema: &[u8],
schema_version: Option<u8>,
contract_name: &str,
value: &[u8],
) -> Result<Vec<u8>> {
) -> Result<Vec<u8>, FFIError> {
let module_schema = VersionedModuleSchema::new(schema, &schema_version)?;
let parameter_type = module_schema.get_event_schema(contract_name)?;
let deserialized = deserialize_type_value(value, &parameter_type, true)?;
let deserialized = deserialize_type_value(value, &parameter_type)?;
Ok(deserialized)
}

fn deserialize_type_value(
value: &[u8],
value_type: &Type,
verbose_error_message: bool,
) -> Result<Vec<u8>> {
fn deserialize_type_value(value: &[u8], value_type: &Type) -> Result<Vec<u8>, FFIError> {
let mut cursor = Cursor::new(value);
match value_type.to_json(&mut cursor) {
Ok(v) => Ok(to_vec(&v)?),
Err(e) => Err(anyhow!("{}", e.display(verbose_error_message))),
}
let v = value_type.to_json(&mut cursor)?;
Ok(to_vec(&v)?)
}
/// The provided raw pointer [`c_char`] must be a [`std::ffi::CString`].
/// The content of the pointer [`c_char`] must not be mutated for the duration
/// of lifetime 'a.
fn get_str_from_pointer<'a>(input: *const c_char) -> Result<&'a str> {
fn get_str_from_pointer<'a>(input: *const c_char) -> Result<&'a str, FFIError> {
let c_str: &CStr = unsafe { CStr::from_ptr(input) };
Ok(c_str.to_str()?)
}
Expand Down
2 changes: 1 addition & 1 deletion src/Concordium.Sdk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<PackageTags>concordium;concordium-net-sdk;blockchain;sdk;</PackageTags>
<Company>Concordium</Company>
<PackageId>ConcordiumNetSdk</PackageId>
<Version>4.3.0</Version>
<Version>4.3.1</Version>
<PackageIcon>icon.png</PackageIcon>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseExpression>MPL-2.0</PackageLicenseExpression>
Expand Down
20 changes: 0 additions & 20 deletions src/Exceptions/InteropBindingException.cs

This file was deleted.

23 changes: 23 additions & 0 deletions src/Exceptions/SchemaJsonException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System.Text;
using Concordium.Sdk.Interop;

namespace Concordium.Sdk.Exceptions;

/// <summary>
/// Thrown when a interop call failed with possible error as message.
/// </summary>
public sealed class SchemaJsonException : Exception
{
private const string EmptyErrorMessage = "Empty error message returned";
/// <summary>
/// Type of error
/// </summary>
public SchemaJsonResult SchemaJsonResult { get; }

internal static SchemaJsonException Create(SchemaJsonResult schemaJsonResult, byte[]? message) =>
message != null ? new SchemaJsonException(schemaJsonResult, Encoding.UTF8.GetString(message)) : Empty(schemaJsonResult);

private SchemaJsonException(SchemaJsonResult schemaJsonResult, string message) : base(message) => this.SchemaJsonResult = schemaJsonResult;

private static SchemaJsonException Empty(SchemaJsonResult schemaJsonResult) => new(schemaJsonResult, EmptyErrorMessage);
}
Loading

0 comments on commit c5deb35

Please sign in to comment.