Skip to content

Commit 764757b

Browse files
authored
fix: FFI/cgo string passing (#133)
* 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
1 parent 0211d0a commit 764757b

File tree

12 files changed

+719
-579
lines changed

12 files changed

+719
-579
lines changed

Cargo.lock

Lines changed: 453 additions & 439 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,14 @@ repository = "https://github.com/stackabletech/stackable-cockpit/"
1717
async-trait = "0.1"
1818
axum = { version = "0.6", features = ["http2", "headers"] }
1919
bcrypt = "0.15"
20+
bindgen = "0.68.1"
21+
cc = "1.0.83"
2022
clap = { version = "4.2.1", features = ["derive", "env"] }
2123
clap_complete = "4.2"
2224
comfy-table = { version = "7.0", features = ["custom_styling"] }
2325
directories = "5.0"
2426
dotenvy = "0.15"
2527
futures = "0.3"
26-
gobuild = "0.1.0-alpha.2"
2728
indexmap = { version = "2.0", features = ["serde"] }
2829
k8s-openapi = { version = "0.19", default-features = false, features = ["v1_27"] }
2930
kube = { version = "0.85", default-features = false, features = ["client", "rustls-tls"] }

extra/completions/stackablectl.bash

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extra/completions/stackablectl.fish

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/helm-sys/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ publish = false
1111
links = "helm"
1212

1313
[build-dependencies]
14-
gobuild.workspace = true
14+
cc.workspace = true
15+
bindgen.workspace = true
16+
snafu.workspace = true

rust/helm-sys/build.rs

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,95 @@
1-
use std::env;
1+
use std::{
2+
env::{self, VarError},
3+
path::PathBuf,
4+
process::Command,
5+
};
26

3-
use gobuild::BuildMode;
7+
use snafu::{ResultExt, Snafu};
48

5-
const ENV_GO_HELM_WRAPPER: &str = "GO_HELM_WRAPPER";
9+
#[derive(Debug, Snafu)]
10+
enum Error {
11+
#[snafu(display("Failed to find env var"))]
12+
EnvVarNotFound { source: VarError },
13+
14+
#[snafu(display("Unsupported GOARCH: {arch}"))]
15+
UnsupportedGoArch { arch: String },
16+
17+
#[snafu(display("Unsupported GOOS: {os}"))]
18+
UnsupportedGoOs { os: String },
19+
}
620

721
fn main() {
8-
// cgo requires an explicit dependency on libresolv on some platforms (such as Red Hat Enterprise Linux 8 and derivatives)
22+
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
23+
24+
println!("cargo:rerun-if-changed=go-helm-wrapper/main.go");
25+
26+
let cc = cc::Build::new().try_get_compiler().unwrap();
27+
let goarch = get_goarch().unwrap();
28+
let goos = get_goos().unwrap();
29+
30+
let mut cmd = Command::new("go");
31+
cmd.arg("build")
32+
.args(["-buildmode", "c-archive"])
33+
.arg("-o")
34+
.arg(out_path.join("libgo-helm-wrapper.a"))
35+
.arg("go-helm-wrapper/main.go")
36+
.env("CGO_ENABLED", "1")
37+
.env("GOARCH", goarch)
38+
.env("GOOS", goos)
39+
.env("CC", format!("'{}'", cc.path().display()));
40+
41+
cmd.status().expect("Failed to build go-helm-wrapper");
42+
43+
let bindings = bindgen::builder()
44+
.header(out_path.join("libgo-helm-wrapper.h").to_str().unwrap())
45+
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
46+
.generate()
47+
.expect("Failed to generate Rust bindings from Go header file");
48+
49+
bindings
50+
.write_to_file(out_path.join("bindings.rs"))
51+
.expect("Failed to write bindings");
52+
953
println!("cargo:rustc-link-lib=resolv");
10-
println!("cargo:rerun-if-env-changed={ENV_GO_HELM_WRAPPER}");
11-
match env::var(ENV_GO_HELM_WRAPPER) {
12-
Ok(go_helm_wrapper) => {
13-
// Reuse pre-built helm wrapper if possible
14-
eprintln!("Reusing pre-built go-helm-wrapper ({go_helm_wrapper:?})");
15-
println!("cargo:rustc-link-lib=static:+verbatim={go_helm_wrapper}");
16-
}
17-
Err(env::VarError::NotPresent) => {
18-
gobuild::Build::new()
19-
.file("go-helm-wrapper/main.go")
20-
.buildmode(BuildMode::CArchive)
21-
.compile("go-helm-wrapper");
22-
}
23-
Err(err @ env::VarError::NotUnicode(..)) => {
24-
panic!("{ENV_GO_HELM_WRAPPER} must be valid unicode: {err}");
25-
}
26-
}
54+
println!("cargo:rustc-link-lib=static=go-helm-wrapper");
55+
println!(
56+
"cargo:rustc-link-search=native={}",
57+
out_path.to_str().unwrap()
58+
);
59+
}
60+
61+
fn get_goarch() -> Result<String, Error> {
62+
let arch = env::var("CARGO_CFG_TARGET_ARCH").context(EnvVarNotFoundSnafu)?;
63+
64+
let arch = match arch.as_str() {
65+
"x86" => "386",
66+
"x86_64" => "amd64",
67+
"mips" => "mips",
68+
"powerpc" => "ppc",
69+
"powerpc64" => "ppc64",
70+
"arm" => "arm",
71+
"aarch64" => "arm64",
72+
_ => return UnsupportedGoArchSnafu { arch }.fail(),
73+
};
74+
75+
Ok(arch.into())
76+
}
77+
78+
fn get_goos() -> Result<String, Error> {
79+
let os = env::var("CARGO_CFG_TARGET_OS").context(EnvVarNotFoundSnafu)?;
80+
81+
let os = match os.as_str() {
82+
"windows" => "windows",
83+
"macos" => "darwin",
84+
"ios" => "darwin",
85+
"linux" => "linux",
86+
"android" => "android",
87+
"freebsd" => "freebsd",
88+
"dragonfly" => "dragonfly",
89+
"openbsd" => "openbsd",
90+
"netbsd" => "netbsd",
91+
_ => return UnsupportedGoOsSnafu { os }.fail(),
92+
};
93+
94+
Ok(os.into())
2795
}

rust/helm-sys/go-helm-wrapper/main.go

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package main
22

3+
/*
4+
#include <stdlib.h>
5+
*/
6+
import "C"
7+
38
import (
4-
"C"
59
"context"
610
"encoding/json"
711
"fmt"
812
"time"
13+
"unsafe"
914

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

3338
//export go_install_helm_release
34-
func go_install_helm_release(releaseName string, chartName string, chartVersion string, valuesYaml string, namespace string, suppressOutput bool) *C.char {
39+
func go_install_helm_release(releaseName *C.char, chartName *C.char, chartVersion *C.char, valuesYaml *C.char, namespace *C.char, suppressOutput bool) *C.char {
3540
helmClient := getHelmClient(namespace, suppressOutput)
3641

3742
timeout, _ := time.ParseDuration("10m")
3843
chartSpec := gohelm.ChartSpec{
39-
ReleaseName: releaseName,
40-
ChartName: chartName,
41-
Version: chartVersion,
42-
ValuesYaml: valuesYaml,
43-
Namespace: namespace,
44+
ReleaseName: C.GoString(releaseName),
45+
ChartName: C.GoString(chartName),
46+
Version: C.GoString(chartVersion),
47+
ValuesYaml: C.GoString(valuesYaml),
48+
Namespace: C.GoString(namespace),
4449
UpgradeCRDs: true,
4550
Wait: true,
4651
Timeout: timeout,
@@ -54,21 +59,21 @@ func go_install_helm_release(releaseName string, chartName string, chartVersion
5459
}
5560

5661
//export go_uninstall_helm_release
57-
func go_uninstall_helm_release(releaseName string, namespace string, suppressOutput bool) *C.char {
62+
func go_uninstall_helm_release(releaseName *C.char, namespace *C.char, suppressOutput bool) *C.char {
5863
helmClient := getHelmClient(namespace, suppressOutput)
5964

60-
if err := helmClient.UninstallReleaseByName(releaseName); err != nil {
65+
if err := helmClient.UninstallReleaseByName(C.GoString(releaseName)); err != nil {
6166
return C.CString(fmt.Sprintf("%s%s", HELM_ERROR_PREFIX, err))
6267
}
6368

6469
return C.CString("")
6570
}
6671

6772
//export go_helm_release_exists
68-
func go_helm_release_exists(releaseName string, namespace string) bool {
73+
func go_helm_release_exists(releaseName *C.char, namespace *C.char) bool {
6974
helmClient := getHelmClient(namespace, true)
7075

71-
release, _ := helmClient.GetRelease(releaseName)
76+
release, _ := helmClient.GetRelease(C.GoString(releaseName))
7277
return release != nil
7378
}
7479

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

8489
// 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 {
112117
// operations.
113118
//
114119
//export go_add_helm_repo
115-
func go_add_helm_repo(name string, url string) *C.char {
116-
helmClient := getHelmClient("default", true) // Namespace doesn't matter
120+
func go_add_helm_repo(name *C.char, url *C.char) *C.char {
121+
helmClient := getHelmClient(C.CString("default"), true) // Namespace doesn't matter
117122

118123
chartRepo := repo.Entry{
119-
Name: name,
120-
URL: url,
124+
Name: C.GoString(name),
125+
URL: C.GoString(url),
121126
}
122127

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

130-
func getHelmClient(namespace string, suppressOutput bool) gohelm.Client {
135+
//export free_go_string
136+
func free_go_string(ptr *C.char) {
137+
C.free(unsafe.Pointer(ptr))
138+
}
139+
140+
func getHelmClient(namespace *C.char, suppressOutput bool) gohelm.Client {
131141
options := gohelm.Options{
132-
Namespace: namespace,
142+
Namespace: C.GoString(namespace),
133143
Debug: false,
134144
}
135145

rust/helm-sys/src/lib.rs

Lines changed: 105 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,112 @@
1-
use std::{marker::PhantomData, os::raw::c_char};
1+
#![allow(non_upper_case_globals)]
2+
#![allow(non_camel_case_types)]
3+
#![allow(improper_ctypes)]
4+
#![allow(non_snake_case)]
25

3-
#[repr(C)]
4-
pub struct GoString<'a> {
5-
p: *const u8,
6-
n: i64,
7-
_lifetime: PhantomData<&'a str>,
6+
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
7+
8+
use std::ffi::{c_char, CStr, CString};
9+
10+
pub const HELM_ERROR_PREFIX: &str = "ERROR:";
11+
12+
pub fn install_helm_release(
13+
release_name: &str,
14+
chart_name: &str,
15+
chart_version: &str,
16+
values_yaml: &str,
17+
namespace: &str,
18+
suppress_output: bool,
19+
) -> String {
20+
let release_name = CString::new(release_name).unwrap();
21+
let chart_name = CString::new(chart_name).unwrap();
22+
let chart_version = CString::new(chart_version).unwrap();
23+
let values_yaml = CString::new(values_yaml).unwrap();
24+
let namespace = CString::new(namespace).unwrap();
25+
26+
unsafe {
27+
let c = go_install_helm_release(
28+
release_name.as_ptr() as *mut c_char,
29+
chart_name.as_ptr() as *mut c_char,
30+
chart_version.as_ptr() as *mut c_char,
31+
values_yaml.as_ptr() as *mut c_char,
32+
namespace.as_ptr() as *mut c_char,
33+
suppress_output as u8,
34+
);
35+
36+
cstr_ptr_to_string(c)
37+
}
38+
}
39+
40+
pub fn uninstall_helm_release(
41+
release_name: &str,
42+
namespace: &str,
43+
suppress_output: bool,
44+
) -> String {
45+
let release_name = CString::new(release_name).unwrap();
46+
let namespace = CString::new(namespace).unwrap();
47+
48+
unsafe {
49+
let c = go_uninstall_helm_release(
50+
release_name.as_ptr() as *mut c_char,
51+
namespace.as_ptr() as *mut c_char,
52+
suppress_output as u8,
53+
);
54+
55+
cstr_ptr_to_string(c)
56+
}
57+
}
58+
59+
pub fn check_helm_release_exists(release_name: &str, namespace: &str) -> bool {
60+
let release_name = CString::new(release_name).unwrap();
61+
let namespace = CString::new(namespace).unwrap();
62+
63+
unsafe {
64+
go_helm_release_exists(
65+
release_name.as_ptr() as *mut c_char,
66+
namespace.as_ptr() as *mut c_char,
67+
) != 0
68+
}
869
}
970

10-
impl<'a> From<&'a str> for GoString<'a> {
11-
fn from(str: &'a str) -> Self {
12-
GoString {
13-
p: str.as_ptr(),
14-
n: str.len() as i64,
15-
_lifetime: PhantomData,
16-
}
71+
pub fn list_helm_releases(namespace: &str) -> String {
72+
let namespace = CString::new(namespace).unwrap();
73+
74+
unsafe {
75+
let c = go_helm_list_releases(namespace.as_ptr() as *mut c_char);
76+
cstr_ptr_to_string(c)
1777
}
1878
}
1979

20-
extern "C" {
21-
pub fn go_install_helm_release(
22-
release_name: GoString,
23-
chart_name: GoString,
24-
chart_version: GoString,
25-
values_yaml: GoString,
26-
namespace: GoString,
27-
suppress_output: bool,
28-
) -> *const c_char;
29-
pub fn go_uninstall_helm_release(
30-
release_name: GoString,
31-
namespace: GoString,
32-
suppress_output: bool,
33-
) -> *const c_char;
34-
pub fn go_helm_release_exists(release_name: GoString, namespace: GoString) -> bool;
35-
pub fn go_helm_list_releases(namespace: GoString) -> *const c_char;
36-
pub fn go_add_helm_repo(name: GoString, url: GoString) -> *const c_char;
80+
pub fn add_helm_repository(repository_name: &str, repository_url: &str) -> String {
81+
let repository_name = CString::new(repository_name).unwrap();
82+
let repository_url = CString::new(repository_url).unwrap();
83+
84+
unsafe {
85+
let c = go_add_helm_repo(
86+
repository_name.as_ptr() as *mut c_char,
87+
repository_url.as_ptr() as *mut c_char,
88+
);
89+
90+
cstr_ptr_to_string(c)
91+
}
92+
}
93+
94+
/// Checks if the result string is an error, and if so, returns the error message as a string.
95+
pub fn to_helm_error(result: &str) -> Option<String> {
96+
if !result.is_empty() && result.starts_with(HELM_ERROR_PREFIX) {
97+
return Some(result.replace(HELM_ERROR_PREFIX, ""));
98+
}
99+
100+
None
101+
}
102+
103+
/// Converts a raw C string pointer into an owned Rust [`String`]. This function
104+
/// also makes sure, that the pointer (and underlying memory) of the Go string is
105+
/// freed. The pointer **cannot** be used afterwards.
106+
unsafe fn cstr_ptr_to_string(c: *mut c_char) -> String {
107+
let cstr = CStr::from_ptr(c);
108+
let s = String::from_utf8_lossy(cstr.to_bytes()).to_string();
109+
free_go_string(cstr.as_ptr() as *mut c_char);
110+
111+
s
37112
}

0 commit comments

Comments
 (0)