首先从 Go 语言本身去找,无果

随后找一些 low-level 的库的支持,golang 的 sys 似乎可以,但是好像没有 Mac 支持

一度想通过 Python、 Apple Script 等方式调用的方式绕过,觉得不优雅。

最后了解到 go 可以直接运行 C 语言,得到这样一串代码

https://stackoverflow.com/a/23451568/11185460

因此就开始了对 Object-C 的学习。

首先通过一个 hello world 代码初步调通 Object-C 和 go 的编译和类型转换

package main

/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation
#import <Foundation/Foundation.h>
void hello() {
    NSLog(@"Hello World");
}
*/
import "C"

func main() {
    C.hello()
}

然后注意到解决方案里提供的 object-c 的脚本里有一个 CFStringRef 类型。

这个 Ref 就是个指针嘛,一开始想的是怎么把 Ref 直接变成 char 类型,因此找到了NSString 类型,并且了解到了 CFStringRef 和 NSString 类型的转换。

后来发现 NSString 类型定义时似乎必须用指针形式,所以一度卡主。

后来找到了一个代码片段,提供了将 CFStringRef 转换为 go string 的代码,明白了语言之间因为数据类型存储位不统一,不能简单的直接转换,而是应该直接从内存中拿值的方式解决。

https://gist.github.com/groob/34da9aa005dfc5a3360f65744ae52861

最后改出了这一份代码a

package main

/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Cocoa
#import <Cocoa/Cocoa.h>

int
GetFrontMostAppPid(void){
	NSRunningApplication* app = [[NSWorkspace sharedWorkspace]
								  frontmostApplication];
	pid_t pid = [app processIdentifier];
	return pid;
}

CFStringRef
GetAppTitle(pid_t pid) {
	CFStringRef title = NULL;
	// Get the process ID of the frontmost application.
	//	NSRunningApplication* app = [[NSWorkspace sharedWorkspace]
	//								  frontmostApplication];
	//	pid_t pid = [app processIdentifier];

	// See if we have accessibility permissions, and if not, prompt the user to
	// visit System Preferences.
	NSDictionary *options = @{(id)kAXTrustedCheckOptionPrompt: @YES};
	Boolean appHasPermission = AXIsProcessTrustedWithOptions(
								 (__bridge CFDictionaryRef)options);
	if (!appHasPermission) {
	   return title; // we don't have accessibility permissions
	}
	// Get the accessibility element corresponding to the frontmost application.
	AXUIElementRef appElem = AXUIElementCreateApplication(pid);
	if (!appElem) {
	  return title;
	}

	// Get the accessibility element corresponding to the frontmost window
	// of the frontmost application.
	AXUIElementRef window = NULL;
	if (AXUIElementCopyAttributeValue(appElem,
		  kAXFocusedWindowAttribute, (CFTypeRef*)&window) != kAXErrorSuccess) {
	  CFRelease(appElem);
	  return title;
	}

	// Finally, get the title of the frontmost window.
	AXError result = AXUIElementCopyAttributeValue(window, kAXTitleAttribute,
					   (CFTypeRef*)&title);

	// At this point, we don't need window and appElem anymore.
	CFRelease(window);
	CFRelease(appElem);

	if (result != kAXErrorSuccess) {
	  // Failed to get the window title.
	  return title;
	}

	// Success! Now, do something with the title, e.g. copy it somewhere.

	// Once we're done with the title, release it.
	CFRelease(title);

    return title;
}
static inline CFIndex cfstring_utf8_length(CFStringRef str, CFIndex *need) {
  CFIndex n, usedBufLen;
  CFRange rng = CFRangeMake(0, CFStringGetLength(str));
  return CFStringGetBytes(str, rng, kCFStringEncodingUTF8, 0, 0, NULL, 0, need);
}
*/
import "C"
import (
	"github.com/shirou/gopsutil/v3/process"
	"reflect"
	"unsafe"
)

//import "github.com/shirou/gopsutil/v3/process"
func cfstringGo(cfs C.CFStringRef) string {
	var usedBufLen C.CFIndex
	n := C.cfstring_utf8_length(cfs, &usedBufLen)
	if n <= 0 {
		return ""
	}
	rng := C.CFRange{location: C.CFIndex(0), length: n}
	buf := make([]byte, int(usedBufLen))

	bufp := unsafe.Pointer(&buf[0])
	C.CFStringGetBytes(cfs, rng, C.kCFStringEncodingUTF8, 0, 0, (*C.UInt8)(bufp), C.CFIndex(len(buf)), &usedBufLen)

	sh := &reflect.StringHeader{
		Data: uintptr(bufp),
		Len:  int(usedBufLen),
	}
	return *(*string)(unsafe.Pointer(sh))
}

func main() {
	pid := C.GetFrontMostAppPid()

	ps, _ := process.NewProcess(int32(pid))
	title_ref := C.CFStringRef(C.GetAppTitle(pid))

	println(pid)
	println(ps.Name())
	println(cfstringGo(title_ref))
}