Skip to content

Commit

Permalink
fix: FFI/cgo string passing (#133)
Browse files Browse the repository at this point in the history
* fix: FFI/cgo string passing

* Add GOARCH and GOOS env vars to Go command

* Add CC env var

* Add LDFLAGS to cgo

* Add libresolv

* Add doc comment to cstr_ptr_to_string function

* Fix typo
  • Loading branch information
Techassi authored Nov 6, 2023
1 parent 0211d0a commit 764757b
Showing 12 changed files with 719 additions and 579 deletions.
892 changes: 453 additions & 439 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -17,13 +17,14 @@ 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"
cc = "1.0.83"
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"] }
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.

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

[build-dependencies]
gobuild.workspace = true
cc.workspace = true
bindgen.workspace = true
snafu.workspace = true
110 changes: 89 additions & 21 deletions rust/helm-sys/build.rs
Original file line number Diff line number Diff line change
@@ -1,27 +1,95 @@
use std::env;
use std::{
env::{self, VarError},
path::PathBuf,
process::Command,
};

use gobuild::BuildMode;
use snafu::{ResultExt, Snafu};

const ENV_GO_HELM_WRAPPER: &str = "GO_HELM_WRAPPER";
#[derive(Debug, Snafu)]
enum Error {
#[snafu(display("Failed to find env var"))]
EnvVarNotFound { source: VarError },

#[snafu(display("Unsupported GOARCH: {arch}"))]
UnsupportedGoArch { arch: String },

#[snafu(display("Unsupported GOOS: {os}"))]
UnsupportedGoOs { os: String },
}

fn main() {
// cgo requires an explicit dependency on libresolv on some platforms (such as Red Hat Enterprise Linux 8 and derivatives)
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());

println!("cargo:rerun-if-changed=go-helm-wrapper/main.go");

let cc = cc::Build::new().try_get_compiler().unwrap();
let goarch = get_goarch().unwrap();
let goos = get_goos().unwrap();

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")
.env("GOARCH", goarch)
.env("GOOS", goos)
.env("CC", format!("'{}'", cc.path().display()));

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=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}");
}
}
println!("cargo:rustc-link-lib=static=go-helm-wrapper");
println!(
"cargo:rustc-link-search=native={}",
out_path.to_str().unwrap()
);
}

fn get_goarch() -> Result<String, Error> {
let arch = env::var("CARGO_CFG_TARGET_ARCH").context(EnvVarNotFoundSnafu)?;

let arch = match arch.as_str() {
"x86" => "386",
"x86_64" => "amd64",
"mips" => "mips",
"powerpc" => "ppc",
"powerpc64" => "ppc64",
"arm" => "arm",
"aarch64" => "arm64",
_ => return UnsupportedGoArchSnafu { arch }.fail(),
};

Ok(arch.into())
}

fn get_goos() -> Result<String, Error> {
let os = env::var("CARGO_CFG_TARGET_OS").context(EnvVarNotFoundSnafu)?;

let os = match os.as_str() {
"windows" => "windows",
"macos" => "darwin",
"ios" => "darwin",
"linux" => "linux",
"android" => "android",
"freebsd" => "freebsd",
"dragonfly" => "dragonfly",
"openbsd" => "openbsd",
"netbsd" => "netbsd",
_ => return UnsupportedGoOsSnafu { os }.fail(),
};

Ok(os.into())
}
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"
@@ -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,
@@ -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
}

@@ -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)
@@ -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 {
@@ -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,
}

135 changes: 105 additions & 30 deletions rust/helm-sys/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,112 @@
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
}

/// Converts a raw C string pointer into an owned Rust [`String`]. This function
/// also makes sure, that the pointer (and underlying memory) of the Go string is
/// freed. The pointer **cannot** be used afterwards.
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
}
Loading

0 comments on commit 764757b

Please sign in to comment.