1// package core implements core functionality for the generated files
2// this core lib is imported by the generated code
3package core
4
5import (
6 "bytes"
7 "fmt"
8 "os"
9 "os/exec"
10 "path/filepath"
11 "runtime"
12 "strings"
13 "sync"
14 "unsafe"
15
16 "github.com/ebitengine/purego"
17)
18
19func PuregoSafeRegister(fptr interface{}, libs []uintptr, name string) {
20 for _, lib := range libs {
21 sym, err := purego.Dlsym(lib, name)
22 if err == nil {
23 registerFuncSafe(fptr, sym)
24 return
25 }
26 }
27}
28
29// registerFuncSafe wraps purego.RegisterFunc with panic recovery for
30// functions whose signatures exceed upstream purego limits (e.g. "too
31// many stack arguments"). The function pointer stays nil and will
32// panic only if actually called.
33func registerFuncSafe(fptr interface{}, sym uintptr) {
34 defer func() { recover() }()
35 purego.RegisterFunc(fptr, sym)
36}
37
38// paths to where the shared object files should be located
39// this is unique per architecture
40// Debian/Ubuntu has it split into specific arch folder, Fedora is just /usr/lib64
41// Flatpak uses /app/lib for application libraries and runtimes don't vendor `pkg-config` as the fallback
42// see:
43// https://fedora.pkgs.org/38/fedora-x86_64/gtk4-4.10.1-1.fc38.x86_64.rpm.html
44// https://fedora.pkgs.org/38/fedora-aarch64/gtk4-4.10.1-1.fc38.aarch64.rpm.html
45// https://ubuntu.pkgs.org/23.04/ubuntu-main-amd64/libgtk-4-1_4.10.1+ds-2ubuntu1_amd64.deb.html
46// https://ubuntu.pkgs.org/23.04/ubuntu-main-arm64/libgtk-4-1_4.10.1+ds-2ubuntu1_arm64.deb.html
47// https://docs.flatpak.org/en/latest/flatpak-builder-command-reference.html (see --libdir)
48var paths = map[string][]string{
49 "amd64": {"/app/lib/", "/usr/lib/x86_64-linux-gnu/", "/usr/lib64/", "/usr/lib/"},
50 "arm64": {"/app/lib/", "/usr/lib/aarch64-linux-gnu/", "/usr/lib64/", "/usr/lib/"},
51}
52
53// names is a lookup from library names to shared object filenames
54// This is populated dynamically via SetSharedLibrary
55var names = map[string][]string{}
56
57// pkgConfNames is a lookup from library names to pkg-config library names
58// This is populated dynamically via SetPackageName
59var pkgConfNames = map[string]string{}
60
61// SetPackageName registers a pkg-config package name for a library.
62// This is used by the code generator to set package names from GIR files.
63// It won't override existing entries to preserve defaults.
64func SetPackageName(libName, pkgName string) {
65 if _, exists := pkgConfNames[libName]; !exists && pkgName != "" {
66 pkgConfNames[libName] = pkgName
67 }
68}
69
70// SetSharedLibraries registers shared library names for a library.
71// This is used by the code generator to set library names from GIR files.
72// It won't override existing entries to preserve defaults.
73func SetSharedLibraries(libName string, sharedLibs []string) {
74 if _, exists := names[libName]; !exists && len(sharedLibs) > 0 {
75 names[libName] = sharedLibs
76 }
77}
78
79// findSos tries to find all shared objects from a path and a library name
80// It does this by mapping the library name to all suitable shared object filenames and then trying some suffixes
81func findSos(path string, name string) []string {
82 sos := []string{}
83 for _, n := range names[name] {
84 suffixes := []string{"", ".0", ".1", ".2"}
85 fn := filepath.Join(path, n)
86 for _, s := range suffixes {
87 if _, err := os.Stat(fn + s); err == nil {
88 sos = append(sos, fn+s)
89 }
90 }
91 }
92 return sos
93}
94
95// findPkgConf finds all shared object files with pkg-config
96// it does this by running pkg-config --libs-only-L libname
97// and then it loops over the directories returned and finds all suitable ones
98func findPkgConf(name string) []string {
99 cmd := exec.Command("pkg-config", "--libs-only-L", pkgConfNames[name])
100 var out, outerr bytes.Buffer
101 cmd.Stdout = &out
102 cmd.Stderr = &outerr
103 err := cmd.Run()
104 if err != nil {
105 fmt.Fprintf(os.Stderr, "pkg-config, failed with: %v and stderr: %s\n", err, outerr.String())
106 return []string{}
107 }
108 outs := strings.Split(out.String(), "-L")
109 for _, v := range outs {
110 c := strings.TrimSpace(v)
111 if c == "" {
112 continue
113 }
114 g := findSos(c, name)
115 if len(g) > 0 {
116 return g
117 }
118 }
119 return []string{}
120}
121
122// GetPaths gets all shared object files from a library name
123// it does it in the following order
124// see if PUREGOTK_LIBNAME_PATH is set (full path to the lib)
125// - e.g. PUREGOTK_GTK_PATH
126// see if PUREGOTK_LIB_FOLDER is set (root folder where to look for libs)
127// go over the hardcoded paths
128// find a library name with pkg-config
129// panic if failed
130// TODO: Hardcore a library shared object with linker -X flag
131// This is useful for packaging
132func GetPaths(name string) []string {
133 // try to get from env var
134 ev := fmt.Sprintf("PUREGOTK_%s_PATH", name)
135 if v := os.Getenv(ev); v != "" {
136 return []string{v}
137 }
138
139 // Or if a general folder is set where everywhere is located, return that
140 ep := os.Getenv("PUREGOTK_LIB_FOLDER")
141 if ep != "" {
142 g := findSos(ep, name)
143 if len(g) == 0 {
144 panic(fmt.Sprintf("Could not find lib: %s, at path: %s with env: %s", name, ep, "PUREGOTK_FOLDER"))
145 }
146 return g
147 }
148
149 // fallback to lookup a path if no env var is found
150 gp, ok := paths[runtime.GOARCH]
151 if ok {
152 // try to loop over paths
153 for _, p := range gp {
154 g := findSos(p, name)
155 if len(g) > 0 {
156 return g
157 }
158
159 }
160 }
161 // last effort: pkg-config
162 g := findPkgConf(name)
163 if len(g) > 0 {
164 return g
165 }
166
167 panic(fmt.Sprintf("Path for library: %s not found. Please set the path to this library shared object file manually with env variable: %s or PUREGOTK_LIB_FOLDER. Or make sure pkg-config is setup correctly", strings.ToLower(name), ev))
168}
169
170// hasSuffix tests whether the string s ends with suffix.
171// This function was copied from purego
172func hasSuffix(s, suffix string) bool {
173 return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
174}
175
176// ByteSlice creates a pointer to a byte slice of C strings
177// This function was copied from purego
178func ByteSlice(name []string) **byte {
179 if name == nil {
180 return nil
181 }
182 res := make([]*byte, len(name)+1)
183 for i, v := range name {
184 res[i] = CString(v)
185 }
186
187 // the last element is NULL terminated for GTK
188 res[len(name)] = nil
189 return &res[0]
190}
191
192// CString converts a go string to *byte that can be passed to C code.
193// This function was copied from purego
194func CString(name string) *byte {
195 if hasSuffix(name, "\x00") {
196 return &(*(*[]byte)(unsafe.Pointer(&name)))[0]
197 }
198 b := make([]byte, len(name)+1)
199 copy(b, name)
200 return &b[0]
201}
202
203// GoStringSlice gets a string slice from a char** array
204// This function was copied from purego
205func GoStringSlice(c uintptr) []string {
206 var ret []string
207 for i := 0; ; i++ {
208 ptrAddr := c + uintptr(i)*unsafe.Sizeof(uintptr(0))
209 addr := *(*unsafe.Pointer)(unsafe.Pointer(&ptrAddr))
210 // We take the address and then dereference it to trick go vet from creating a possible misuse of unsafe.Pointer
211 ptr := *(*uintptr)(addr)
212 if ptr == 0 {
213 break
214 }
215 ret = append(ret, GoString(ptr))
216 }
217
218 return ret
219}
220
221// GoString copies a char* to a Go string.
222// This function was copied from purego
223func GoString(c uintptr) string {
224 // We take the address and then dereference it to trick go vet from creating a possible misuse of unsafe.Pointer
225 ptr := *(*unsafe.Pointer)(unsafe.Pointer(&c))
226 if ptr == nil {
227 return ""
228 }
229 var length int
230 for {
231 if *(*byte)(unsafe.Add(ptr, uintptr(length))) == '\x00' {
232 break
233 }
234 length++
235 }
236 return string(unsafe.Slice((*byte)(ptr), length))
237}
238
239var (
240 xGStrdup func(string) uintptr
241 gstrdupOnce sync.Once
242 xGFree func(uintptr)
243 gfreeOnce sync.Once
244)
245
246// GStrdup allocates a C-owned copy of a Go string using g_strdup.
247// The returned pointer must be freed with g_free (typically by the callee
248// when transfer-ownership="full").
249func GStrdup(s string) uintptr {
250 gstrdupOnce.Do(func() {
251 var libs []uintptr
252 for _, libPath := range GetPaths("GLIB") {
253 lib, err := purego.Dlopen(libPath, purego.RTLD_NOW|purego.RTLD_GLOBAL)
254 if err != nil {
255 continue
256 }
257 libs = append(libs, lib)
258 }
259 PuregoSafeRegister(&xGStrdup, libs, "g_strdup")
260 })
261 return xGStrdup(s)
262}
263
264// GStrdupNullable is like GStrdup but accepts a nullable *string.
265// Returns 0 for nil, or a g_strdup'd copy for non-nil.
266func GStrdupNullable(s *string) uintptr {
267 if s == nil {
268 return 0
269 }
270 return GStrdup(*s)
271}
272
273// GFree frees memory allocated by GLib allocation APIs (for example g_strdup).
274// Passing 0 is a no-op.
275func GFree(ptr uintptr) {
276 if ptr == 0 {
277 return
278 }
279 gfreeOnce.Do(func() {
280 var libs []uintptr
281 for _, libPath := range GetPaths("GLIB") {
282 lib, err := purego.Dlopen(libPath, purego.RTLD_NOW|purego.RTLD_GLOBAL)
283 if err != nil {
284 continue
285 }
286 libs = append(libs, lib)
287 }
288 PuregoSafeRegister(&xGFree, libs, "g_free")
289 })
290 xGFree(ptr)
291}
292
293// GFreeNullable frees a nullable GLib-allocated pointer.
294func GFreeNullable(ptr uintptr) {
295 if ptr == 0 {
296 return
297 }
298 GFree(ptr)
299}
300
301// NullableStringToPtr converts a nullable Go string to a uintptr suitable for C calls.
302// Returns both the pointer and the backing byte slice. The caller MUST call
303// runtime.KeepAlive(bytes) after the C call completes to prevent GC from collecting
304// the memory before C has finished reading it.
305func NullableStringToPtr(s *string) (uintptr, []byte) {
306 if s == nil {
307 return 0, nil
308 }
309 b := append([]byte(*s), 0)
310 return uintptr(unsafe.Pointer(&b[0])), b
311}
312
313// PtrToNullableString converts a nullable char* to a Go *string (nil when NULL).
314func PtrToNullableString(ptr uintptr) *string {
315 if ptr == 0 {
316 return nil
317 }
318 str := GoString(ptr)
319 return &str
320}