Skip to content

Commit

Permalink
fix: FFI/cgo string passing
Browse files Browse the repository at this point in the history
  • Loading branch information
Techassi committed Oct 26, 2023
1 parent f8c4573 commit dd01271
Show file tree
Hide file tree
Showing 12 changed files with 650 additions and 581 deletions.
890 changes: 451 additions & 439 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ repository = "https://github.com/stackabletech/stackable-cockpit/"
async-trait = "0.1"
axum = { version = "0.6", features = ["http2", "headers"] }
bcrypt = "0.15"
bindgen = "0.68.1"
clap = { version = "4.2.1", features = ["derive", "env"] }
clap_complete = "4.2"
comfy-table = { version = "7.0", features = ["custom_styling"] }
directories = "5.0"
dotenvy = "0.15"
futures = "0.3"
gobuild = "0.1.0-alpha.2"
indexmap = { version = "2.0", features = ["serde"] }
k8s-openapi = { version = "0.19", default-features = false, features = ["v1_27"] }
kube = { version = "0.85", default-features = false, features = ["client", "rustls-tls"] }
Expand Down
2 changes: 1 addition & 1 deletion extra/completions/stackablectl.bash

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

28 changes: 14 additions & 14 deletions extra/completions/stackablectl.fish

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion rust/helm-sys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ publish = false
links = "helm"

[build-dependencies]
gobuild.workspace = true
bindgen.workspace = true
51 changes: 28 additions & 23 deletions rust/helm-sys/build.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
use std::env;
use std::{env, path::PathBuf, process::Command};

use gobuild::BuildMode;
fn main() {
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());

const ENV_GO_HELM_WRAPPER: &str = "GO_HELM_WRAPPER";
println!("cargo:rerun-if-changed=go-helm-wrapper/main.go");

fn main() {
// cgo requires an explicit dependency on libresolv on some platforms (such as Red Hat Enterprise Linux 8 and derivatives)
println!("cargo:rustc-link-lib=resolv");
println!("cargo:rerun-if-env-changed={ENV_GO_HELM_WRAPPER}");
match env::var(ENV_GO_HELM_WRAPPER) {
Ok(go_helm_wrapper) => {
// Reuse pre-built helm wrapper if possible
eprintln!("Reusing pre-built go-helm-wrapper ({go_helm_wrapper:?})");
println!("cargo:rustc-link-lib=static:+verbatim={go_helm_wrapper}");
}
Err(env::VarError::NotPresent) => {
gobuild::Build::new()
.file("go-helm-wrapper/main.go")
.buildmode(BuildMode::CArchive)
.compile("go-helm-wrapper");
}
Err(err @ env::VarError::NotUnicode(..)) => {
panic!("{ENV_GO_HELM_WRAPPER} must be valid unicode: {err}");
}
}
let mut cmd = Command::new("go");
cmd.arg("build")
.args(["-buildmode", "c-archive"])
.arg("-o")
.arg(out_path.join("libgo-helm-wrapper.a"))
.arg("go-helm-wrapper/main.go")
.env("CGO_ENABLED", "1");
cmd.status().expect("Failed to build go-helm-wrapper");

let bindings = bindgen::builder()
.header(out_path.join("libgo-helm-wrapper.h").to_str().unwrap())
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
.generate()
.expect("Failed to generate Rust bindings from Go header file");

bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("Failed to write bindings");

println!("cargo:rustc-link-lib=static=go-helm-wrapper");
println!(
"cargo:rustc-link-search=native={}",
out_path.to_str().unwrap()
);
}
46 changes: 28 additions & 18 deletions rust/helm-sys/go-helm-wrapper/main.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package main

/*
#include <stdlib.h>
*/
import "C"

import (
"C"
"context"
"encoding/json"
"fmt"
"time"
"unsafe"

gohelm "github.com/mittwald/go-helm-client"
"helm.sh/helm/v3/pkg/action"
Expand All @@ -31,16 +36,16 @@ func main() {
}

//export go_install_helm_release
func go_install_helm_release(releaseName string, chartName string, chartVersion string, valuesYaml string, namespace string, suppressOutput bool) *C.char {
func go_install_helm_release(releaseName *C.char, chartName *C.char, chartVersion *C.char, valuesYaml *C.char, namespace *C.char, suppressOutput bool) *C.char {
helmClient := getHelmClient(namespace, suppressOutput)

timeout, _ := time.ParseDuration("10m")
chartSpec := gohelm.ChartSpec{
ReleaseName: releaseName,
ChartName: chartName,
Version: chartVersion,
ValuesYaml: valuesYaml,
Namespace: namespace,
ReleaseName: C.GoString(releaseName),
ChartName: C.GoString(chartName),
Version: C.GoString(chartVersion),
ValuesYaml: C.GoString(valuesYaml),
Namespace: C.GoString(namespace),
UpgradeCRDs: true,
Wait: true,
Timeout: timeout,
Expand All @@ -54,21 +59,21 @@ func go_install_helm_release(releaseName string, chartName string, chartVersion
}

//export go_uninstall_helm_release
func go_uninstall_helm_release(releaseName string, namespace string, suppressOutput bool) *C.char {
func go_uninstall_helm_release(releaseName *C.char, namespace *C.char, suppressOutput bool) *C.char {
helmClient := getHelmClient(namespace, suppressOutput)

if err := helmClient.UninstallReleaseByName(releaseName); err != nil {
if err := helmClient.UninstallReleaseByName(C.GoString(releaseName)); err != nil {
return C.CString(fmt.Sprintf("%s%s", HELM_ERROR_PREFIX, err))
}

return C.CString("")
}

//export go_helm_release_exists
func go_helm_release_exists(releaseName string, namespace string) bool {
func go_helm_release_exists(releaseName *C.char, namespace *C.char) bool {
helmClient := getHelmClient(namespace, true)

release, _ := helmClient.GetRelease(releaseName)
release, _ := helmClient.GetRelease(C.GoString(releaseName))
return release != nil
}

Expand All @@ -78,7 +83,7 @@ func go_helm_release_exists(releaseName string, namespace string) bool {
// by the Rust code and it will abort operations.
//
//export go_helm_list_releases
func go_helm_list_releases(namespace string) *C.char {
func go_helm_list_releases(namespace *C.char) *C.char {
helmClient := getHelmClient(namespace, true)

// List all releases, not only the deployed ones (e.g. include pending installations)
Expand Down Expand Up @@ -112,12 +117,12 @@ func go_helm_list_releases(namespace string) *C.char {
// operations.
//
//export go_add_helm_repo
func go_add_helm_repo(name string, url string) *C.char {
helmClient := getHelmClient("default", true) // Namespace doesn't matter
func go_add_helm_repo(name *C.char, url *C.char) *C.char {
helmClient := getHelmClient(C.CString("default"), true) // Namespace doesn't matter

chartRepo := repo.Entry{
Name: name,
URL: url,
Name: C.GoString(name),
URL: C.GoString(url),
}

if err := helmClient.AddOrUpdateChartRepo(chartRepo); err != nil {
Expand All @@ -127,9 +132,14 @@ func go_add_helm_repo(name string, url string) *C.char {
return C.CString("")
}

func getHelmClient(namespace string, suppressOutput bool) gohelm.Client {
//export free_go_string
func free_go_string(ptr *C.char) {
C.free(unsafe.Pointer(ptr))
}

func getHelmClient(namespace *C.char, suppressOutput bool) gohelm.Client {
options := gohelm.Options{
Namespace: namespace,
Namespace: C.GoString(namespace),
Debug: false,
}

Expand Down
132 changes: 102 additions & 30 deletions rust/helm-sys/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,109 @@
use std::{marker::PhantomData, os::raw::c_char};
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(improper_ctypes)]
#![allow(non_snake_case)]

#[repr(C)]
pub struct GoString<'a> {
p: *const u8,
n: i64,
_lifetime: PhantomData<&'a str>,
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

use std::ffi::{c_char, CStr, CString};

pub const HELM_ERROR_PREFIX: &str = "ERROR:";

pub fn install_helm_release(
release_name: &str,
chart_name: &str,
chart_version: &str,
values_yaml: &str,
namespace: &str,
suppress_output: bool,
) -> String {
let release_name = CString::new(release_name).unwrap();
let chart_name = CString::new(chart_name).unwrap();
let chart_version = CString::new(chart_version).unwrap();
let values_yaml = CString::new(values_yaml).unwrap();
let namespace = CString::new(namespace).unwrap();

unsafe {
let c = go_install_helm_release(
release_name.as_ptr() as *mut c_char,
chart_name.as_ptr() as *mut c_char,
chart_version.as_ptr() as *mut c_char,
values_yaml.as_ptr() as *mut c_char,
namespace.as_ptr() as *mut c_char,
suppress_output as u8,
);

cstr_ptr_to_string(c)
}
}

pub fn uninstall_helm_release(
release_name: &str,
namespace: &str,
suppress_output: bool,
) -> String {
let release_name = CString::new(release_name).unwrap();
let namespace = CString::new(namespace).unwrap();

unsafe {
let c = go_uninstall_helm_release(
release_name.as_ptr() as *mut c_char,
namespace.as_ptr() as *mut c_char,
suppress_output as u8,
);

cstr_ptr_to_string(c)
}
}

pub fn check_helm_release_exists(release_name: &str, namespace: &str) -> bool {
let release_name = CString::new(release_name).unwrap();
let namespace = CString::new(namespace).unwrap();

unsafe {
go_helm_release_exists(
release_name.as_ptr() as *mut c_char,
namespace.as_ptr() as *mut c_char,
) != 0
}
}

impl<'a> From<&'a str> for GoString<'a> {
fn from(str: &'a str) -> Self {
GoString {
p: str.as_ptr(),
n: str.len() as i64,
_lifetime: PhantomData,
}
pub fn list_helm_releases(namespace: &str) -> String {
let namespace = CString::new(namespace).unwrap();

unsafe {
let c = go_helm_list_releases(namespace.as_ptr() as *mut c_char);
cstr_ptr_to_string(c)
}
}

extern "C" {
pub fn go_install_helm_release(
release_name: GoString,
chart_name: GoString,
chart_version: GoString,
values_yaml: GoString,
namespace: GoString,
suppress_output: bool,
) -> *const c_char;
pub fn go_uninstall_helm_release(
release_name: GoString,
namespace: GoString,
suppress_output: bool,
) -> *const c_char;
pub fn go_helm_release_exists(release_name: GoString, namespace: GoString) -> bool;
pub fn go_helm_list_releases(namespace: GoString) -> *const c_char;
pub fn go_add_helm_repo(name: GoString, url: GoString) -> *const c_char;
pub fn add_helm_repository(repository_name: &str, repository_url: &str) -> String {
let repository_name = CString::new(repository_name).unwrap();
let repository_url = CString::new(repository_url).unwrap();

unsafe {
let c = go_add_helm_repo(
repository_name.as_ptr() as *mut c_char,
repository_url.as_ptr() as *mut c_char,
);

cstr_ptr_to_string(c)
}
}

/// Checks if the result string is an error, and if so, returns the error message as a string.
pub fn to_helm_error(result: &str) -> Option<String> {
if !result.is_empty() && result.starts_with(HELM_ERROR_PREFIX) {
return Some(result.replace(HELM_ERROR_PREFIX, ""));
}

None
}

unsafe fn cstr_ptr_to_string(c: *mut c_char) -> String {
let cstr = CStr::from_ptr(c);
let s = String::from_utf8_lossy(cstr.to_bytes()).to_string();
free_go_string(cstr.as_ptr() as *mut c_char);

s
}
1 change: 0 additions & 1 deletion rust/stackable-cockpit/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ pub const HELM_REPO_NAME_DEV: &str = "stackable-dev";
pub const HELM_REPO_INDEX_FILE: &str = "index.yaml";

pub const HELM_DEFAULT_CHART_VERSION: &str = ">0.0.0-0";
pub const HELM_ERROR_PREFIX: &str = "ERROR:";

pub const PRODUCT_NAMES: &[&str] = &[
"airflow",
Expand Down
Loading

0 comments on commit dd01271

Please sign in to comment.