Skip to content

Remove CGO from macOS #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ accessing system clipboards, but here are a few details you might need to know.

### Dependency

- macOS: require Cgo, no dependency
- macOS: no Cgo, no build dependency
- Linux: require X11 dev package. For instance, install `libx11-dev` or `xorg-dev` or `libX11-devel` to access X window system.
Wayland sessions are currently unsupported; running under Wayland
typically requires an XWayland bridge and `DISPLAY` to be set.
Expand Down
134 changes: 95 additions & 39 deletions clipboard_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,82 +8,93 @@

package clipboard

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

unsigned int clipboard_read_string(void **out);
unsigned int clipboard_read_image(void **out);
int clipboard_write_string(const void *bytes, NSInteger n);
int clipboard_write_image(const void *bytes, NSInteger n);
NSInteger clipboard_change_count();
*/
import "C"
import (
"context"
"time"
"unsafe"

"github.com/ebitengine/purego"
"github.com/ebitengine/purego/objc"
)

var (
appkit = must(purego.Dlopen("/System/Library/Frameworks/AppKit.framework/AppKit", purego.RTLD_GLOBAL|purego.RTLD_NOW))

_NSPasteboardTypeString = must2(purego.Dlsym(appkit, "NSPasteboardTypeString"))
_NSPasteboardTypePNG = must2(purego.Dlsym(appkit, "NSPasteboardTypePNG"))

class_NSPasteboard = objc.GetClass("NSPasteboard")
class_NSData = objc.GetClass("NSData")

sel_generalPasteboard = objc.RegisterName("generalPasteboard")
sel_length = objc.RegisterName("length")
sel_getBytesLength = objc.RegisterName("getBytes:length:")
sel_dataForType = objc.RegisterName("dataForType:")
sel_clearContents = objc.RegisterName("clearContents")
sel_setDataForType = objc.RegisterName("setData:forType:")
sel_dataWithBytesLength = objc.RegisterName("dataWithBytes:length:")
sel_changeCount = objc.RegisterName("changeCount")
)

func must(sym uintptr, err error) uintptr {
if err != nil {
panic(err)
}
return sym
}

func must2(sym uintptr, err error) uintptr {
if err != nil {
panic(err)
}
// dlsym returns a pointer to the object so dereference like this to avoid possible misuse of 'unsafe.Pointer' warning
return **(**uintptr)(unsafe.Pointer(&sym))
}

func initialize() error { return nil }

func read(t Format) (buf []byte, err error) {
var (
data unsafe.Pointer
n C.uint
)
switch t {
case FmtText:
n = C.clipboard_read_string(&data)
return clipboard_read_string(), nil
case FmtImage:
n = C.clipboard_read_image(&data)
}
if data == nil {
return nil, errUnavailable
return clipboard_read_image(), nil
}
defer C.free(unsafe.Pointer(data))
if n == 0 {
return nil, nil
}
return C.GoBytes(data, C.int(n)), nil
return nil, errUnavailable
}

// write writes the given data to clipboard and
// returns true if success or false if failed.
func write(t Format, buf []byte) (<-chan struct{}, error) {
var ok C.int
var ok bool
switch t {
case FmtText:
if len(buf) == 0 {
ok = C.clipboard_write_string(unsafe.Pointer(nil), 0)
ok = clipboard_write_string(nil)
} else {
ok = C.clipboard_write_string(unsafe.Pointer(&buf[0]),
C.NSInteger(len(buf)))
ok = clipboard_write_string(buf)
}
case FmtImage:
if len(buf) == 0 {
ok = C.clipboard_write_image(unsafe.Pointer(nil), 0)
ok = clipboard_write_image(nil)
} else {
ok = C.clipboard_write_image(unsafe.Pointer(&buf[0]),
C.NSInteger(len(buf)))
ok = clipboard_write_image(buf)
}
default:
return nil, errUnsupported
}
if ok != 0 {
if !ok {
return nil, errUnavailable
}

// use unbuffered data to prevent goroutine leak
changed := make(chan struct{}, 1)
cnt := C.long(C.clipboard_change_count())
cnt := clipboard_change_count()
go func() {
for {
// not sure if we are too slow or the user too fast :)
time.Sleep(time.Second)
cur := C.long(C.clipboard_change_count())
cur := clipboard_change_count()
if cnt != cur {
changed <- struct{}{}
close(changed)
Expand All @@ -98,15 +109,15 @@ func watch(ctx context.Context, t Format) <-chan []byte {
recv := make(chan []byte, 1)
// not sure if we are too slow or the user too fast :)
ti := time.NewTicker(time.Second)
lastCount := C.long(C.clipboard_change_count())
lastCount := clipboard_change_count()
go func() {
for {
select {
case <-ctx.Done():
close(recv)
return
case <-ti.C:
this := C.long(C.clipboard_change_count())
this := clipboard_change_count()
if lastCount != this {
b := Read(t)
if b == nil {
Expand All @@ -120,3 +131,48 @@ func watch(ctx context.Context, t Format) <-chan []byte {
}()
return recv
}

func clipboard_read_string() []byte {
var pasteboard = objc.ID(class_NSPasteboard).Send(sel_generalPasteboard)
var data = pasteboard.Send(sel_dataForType, _NSPasteboardTypeString)
if data == 0 {
return nil
}
var size = uint(data.Send(sel_length))
if size == 0 {
return nil
}
out := make([]byte, size)
data.Send(sel_getBytesLength, unsafe.SliceData(out), size)
return out
}

func clipboard_read_image() []byte {
var pasteboard = objc.ID(class_NSPasteboard).Send(sel_generalPasteboard)
data := pasteboard.Send(sel_dataForType, _NSPasteboardTypePNG)
if data == 0 {
return nil
}
size := data.Send(sel_length)
out := make([]byte, size)
data.Send(sel_getBytesLength, unsafe.SliceData(out), size)
return out
}

func clipboard_write_image(bytes []byte) bool {
pasteboard := objc.ID(class_NSPasteboard).Send(sel_generalPasteboard)
data := objc.ID(class_NSData).Send(sel_dataWithBytesLength, unsafe.SliceData(bytes), len(bytes))
pasteboard.Send(sel_clearContents)
return pasteboard.Send(sel_setDataForType, data, _NSPasteboardTypePNG) != 0
}

func clipboard_write_string(bytes []byte) bool {
pasteboard := objc.ID(class_NSPasteboard).Send(sel_generalPasteboard)
data := objc.ID(class_NSData).Send(sel_dataWithBytesLength, unsafe.SliceData(bytes), len(bytes))
pasteboard.Send(sel_clearContents)
return pasteboard.Send(sel_setDataForType, data, _NSPasteboardTypeString) != 0
}

func clipboard_change_count() int {
return int(objc.ID(class_NSPasteboard).Send(sel_generalPasteboard).Send(sel_changeCount))
}
62 changes: 0 additions & 62 deletions clipboard_darwin.m

This file was deleted.

2 changes: 1 addition & 1 deletion clipboard_nocgo.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//go:build !windows && !cgo
//go:build !darwin && !windows && !cgo

package clipboard

Expand Down
8 changes: 4 additions & 4 deletions clipboard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ func TestClipboardInit(t *testing.T) {
if val, ok := os.LookupEnv("CGO_ENABLED"); !ok || val != "0" {
t.Skip("CGO_ENABLED is set to 1")
}
if runtime.GOOS == "windows" {
t.Skip("Windows does not need to check for cgo")
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
t.Skip("Windows and macOS does not need to check for cgo")
}

if err := clipboard.Init(); !errors.Is(err, clipboard.ErrCgoDisabled) {
Expand Down Expand Up @@ -297,8 +297,8 @@ func TestClipboardNoCgo(t *testing.T) {
if val, ok := os.LookupEnv("CGO_ENABLED"); !ok || val != "0" {
t.Skip("CGO_ENABLED is set to 1")
}
if runtime.GOOS == "windows" {
t.Skip("Windows should always be tested")
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
t.Skip("Windows and macOS should always be tested")
}

t.Run("Read", func(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module golang.design/x/clipboard
go 1.24

require (
github.com/ebitengine/purego v0.8.4
golang.org/x/image v0.28.0
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 h1:Wdx0vgH5Wgsw+lF//LJKmWOJBLWX6nprsMqnf99rYDE=
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
Expand Down