0%

解析GoMobile生成的"胶水代码"

标题要唬人…在做Golang跨平台的时候,使用gomobile bind -target=ios xxx 就可以生成对应平台的framework。

本文讨论的内容是Golang是如何实现了OC和Go的数据通讯与接口调用,以及如何做到让开发者无感知的从OC对象的引用计数切换到Golang的垃圾回收机制。

其实Go的语言层面的通讯实际上是由bind生成的中间层的代码来实现的,什么是中间层的代码?我们简单做实验…

写一个Go的例子,以下所有方法就是我们本文需要讨论的所有内容了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package g_mb_test

type GMBInterface interface {
InterfaceTest()
}
type GMBTest struct {
}

func (t *GMBTest) GMBTestOne(value int32) {

}

func (t *GMBTest) GMBTestSecond(data []byte, str string) {

}

func (t *GMBTest) GMBTest3() string {
return “”
}

func (t *GMBTest) GMBTest4() []byte {
return nil
}

func (t *GMBTest) GMBTest5() GMBInterface {
return nil
}

func (t *GMBTest) GMBTest6() *GMBTest {
return nil
}

func (t *GMBTest) GMBTest7(i GMBInterface) {
}
  1. Go 导出的对象在OC中是怎么获取到的?
  2. OC是怎么调用Golang的对象方法的?
  3. OC是怎么传递给Golang参数的?不同的类型是否一样(NSData -> []byte, NSString -> string, interface(OC) -> interface)?
  4. OC传递给Golang 的参数Golang怎么释放(或者OC怎么释放)?
  5. 相反Golang是怎么返回值的?不同的类型是否一样([]byte -> NSData, string -> NSString, interface -> interface(OC))?
  6. Golang 如果返回的是一个被导出的类型,又是什么样的?

首先Golang本身是由C来实现的,所以Golang可以”无压力”和C进行双向通讯(cgo)。其次,OC的本质其实也是C,OC也是可以和C无缝进行双向通讯。在这两个条件下我们可以假想,OC和Golang 的双向通讯是不是也可以用C?

实验开始,我们使用命令行gomobile bind -target=ios/amd64 -work Caio/g_mb_test来保留工作目录。

找到控制台输出的临时目录,生成了如下的结构。go-buildxxx下是生成的中间产物。而src下就是gomobile自动生成的中间代码。

先从对象的初始化开始…

1
2
3
4
5
6
7
- (instancetype)init {
self = [super init];
if (self) {
__ref = go_seq_from_refnum(new_g_mb_test_GMBTest());
}
return self;
}

new_g_mb_test_GMBTest 实际是由 go在静态库中实现的方法,可以从g_mb_test-amd64.h头文件中找到它的声明。对应了src中的go_g_mb_testmain.go文件中的new_g_mb_test_GMBTest()方法。这个go文件是gomobile在编译go之前自动生成的代码。

可以理解为在iOS中引入了一个”静态 framework”,这个静态库实现了new_g_mb_test_GMBTest()。只不过这个framework是由Go语言实现的。

可以看到 new_g_mb_test_GMBTest()返回了一个 int32值,而这个值最后传递给了go_seq_from_refnum(),这个方法是在seq_darwin.m中。

1
2
3
4
5
6
7
8
9
GoSeqRef *go_seq_from_refnum(int32_t refnum) {
if (refnum == NULL_REFNUM) {
return nil;
}
if (IS_FROM_GO(refnum)) {
return [[GoSeqRef alloc] initWithRefnum:refnum obj:NULL];
}
return [[GoSeqRef alloc] initWithRefnum:refnum obj:go_seq_objc_from_refnum(refnum)];
}

通过”字面”意思看,是返回了一个GoSeqRef,通过IS_FROM_GO判断传入的refnum(也就是上面提到的new_g_mb_test_GMBTest()返回的值是不是Go生成的),来决定GoSeqRef中的obj是否为NULL。如果不是Go生成的那么就返回go_seq_objc_from_refnum()转换refnum为obj,这个分支我们之后再讲。

那么我们看看这个所谓的”静态库”是怎么生成这个int32值的。
注:以下运行的代码已经在Go中了

1
2
3
4
5
*go_g_mb_testmain.go*
//export new_g_mb_test_GMBTest
func new_g_mb_test_GMBTest() C.int32_t {
return C.int32_t(_seq.ToRefNum(new(g_mb_test.GMBTest)))
}

可以看到 new_g_mb_test_GMBTest()返回了一个 int32,而这个int32值还是由_seq.ToRefNum()方法返回的。这个方法是由golang.org/x/mobile/bind/seq/ref.go 下的bind提供的。方法参数就是一个go struct对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
*golang.org/x/mobile/bind/seq/ref.go*
// ToRefNum increments the reference count for an object and
// returns its refnum.
func ToRefNum(obj interface{}) int32 {
// We don’t track foreign objects, so if obj is a proxy
// return its refnum.
if r, ok := obj.(proxy); ok {
refnum := r.Bind_proxy_refnum__()
if refnum <= 0 {
panic(fmt.Errorf("seq: proxy contained invalid Go refnum: %d", refnum))
}
return refnum
}
refs.Lock()
num := refs.refs[obj]
if num != 0 {
s := refs.objs[num]
refs.objs[num] = countedObj{s.obj, s.cnt + 1}
} else {
num = refs.next
refs.next--
if refs.next > 0 {
panic(“refs.next underflow”)
}
refs.refs[obj] = num
refs.objs[num] = countedObj{obj, 1}
}
refs.Unlock()

return int32(num)
}

关于proxy那块的代码,我们稍等之后再看。后面的代码也不急着读,先看后面提到的refs对象:

1
2
3
4
5
6
7
// refs stores Go objects that have been passed to another language.
var refs struct {
sync.Mutex
next int32 // next reference number to use for Go object, always negative
refs map[interface{}]int32
objs map[int32]countedObj
}

注释写的很明确了,refs存储了从Golang传递给其他语言的对象。

结构体中有两个map成员变量,一个是refs,一个是objs。refs的key是对象,value就是上面提到的refnum,objs的key是refnum,value是这个Go对象的引用计数对象。还有一个变量是next,next是下一个Go对象的refnum,而且永远是负数。

那么上面的后半段代码也就很容易看懂了。从ref取出obj对应的num,计数加一。如果没有的话,next-1作为下一个refnum,把对象存进map。

所以:

  1. 在Go中有一个全局变量,维护了所有外部正在使用的Go对象。
  2. 从Go生成的对象,想要被外部使用,都是通过一个refnum的值在这个全局变量中索引的。
  3. 并且还维护了一个引用计数,每次取这个对象都会+1。
  4. 另外,从Go导出的refnum都是负数。

到这里,我们其实可以小总结一下了:
从上面的代码可以知道 Go中的对象在 oc 中的体现是:

OC对象持有一个GoSeqRef,GoSeqRef 持有一个 refnum,refnum在Go中的refs的objs和refs中存储了真正的Go对象和引用计数值。

再看一个OC方法的基本调用

既然已经生成Golang的对象了,我们看一下 gmbTestOne 这个基本调用。

1
2
3
4
5
6
- (void)gmbTestOne:(int32_t)value {
int32_t refnum = go_seq_go_to_refnum(self._ref);
int32_t _value = (int32_t)value;
proxyg_mb_test_GMBTest_GMBTestOne(refnum, _value);
}

go_seq_go_to_refnum()是在Seq_darwin.m中实现的。

1
2
3
4
- (int32_t)incNum {
IncGoRef(_refnum);
return _refnum;
}

可以看到最终是返回了_refnum。但是在此之前,还有一个IncGoRef(_refnum);
可以看ref.go的实现,方法实现是增加Go对象的引用计数。
思考:从OC ARC的对象回收的角度来看,似乎”言之有理”。”int32_t refnum “虽然是个int,但是得要持有这个对象,不然会有可能在调用过程中被释放。那么猜想一样,它会在方法域结束的时候释放?我们接着看…

然后和上面的逻辑一样,proxyg_mb_test_GMBTest_GMBTestOne() 是framework实现的方法,第一个参数是refnum,第二个参数是value的值。

猜想它的实现应该是,通过 refnum找到Go中的对象,然后调用对应的方法。我们来看Go的实现:
注:以下运行的代码已经在Go中了

1
2
3
4
5
6
7
//export proxyg_mb_test_GMBTest_GMBTestOne
func proxyg_mb_test_GMBTest_GMBTestOne(refnum C.int32_t, param_value C.int32_t) {
ref := _seq.FromRefNum(int32(refnum))
v := ref.Get().(*g_mb_test.GMBTest)
_param_value := int32(param_value)
v.GMBTestOne(_param_value)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// FromRefNum returns the Ref for a refnum. If the refnum specifies a
// foreign object, a finalizer is set to track its lifetime.
func FromRefNum(num int32) *Ref {
if num == NullRefNum {
return nil
}
ref := &Ref{num}
if num > 0 {
// This is a foreign object reference.
// Track its lifetime with a finalizer.
runtime.SetFinalizer(ref, FinalizeRef)
}

return ref
}

和OC中一样,在Go中也有一个Ref对象,这个对象包裹的就是一个refnum。
num > 0是指代外部传入的对象,我们先不看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Get returns the underlying object.
func (r *Ref) Get() interface{} {
refnum := r.Bind_Num
refs.Lock()
o, ok := refs.objs[refnum]
refs.Unlock()
if !ok {
panic(fmt.Sprintf(“unknown ref %d”, refnum))
}
// This is a Go reference and its refnum was incremented
// before crossing the language barrier.
Delete(refnum)
return o.obj
}

前半段印证了我们的猜想,取出对象并且返回。Delete的实现是引用计数-1,如果到0了,那就从map中删除。

印证我们上面思考中的猜想!Golang对象并不是在结束的时候才释放,在Golang中 v := ref.Get().(*g_mb_test.GMBTest) 已经持有,其实不必再持有了,等Golang的方法结束的时候自然会通过Golang的垃圾回收释放。

最后代码就是一个Go的方法调用了,把int参数传入。

总结一下:
OC调用Golang的方法,实际是,在OC中调用方法的时候,先用self.ref 获取 refnum。这个步骤会增加Go中对象的引用计数。然后调入Go的方法,把refnum传入,取出Golang对象,减小引用计数。然后调用具体实现的方法。

Golang返回对象

Golang的方法返回对象其实和上面的初始化方法大同小异,只是结合了基本方法调用。注:Go返回的对象类型必须是Go导出的类型,非Go导出的类型Go不识别或者OC不识别(除了NSData,NSString等基本类型)。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (G_mb_testGMBTest*)gmbTest6 {
int32_t refnum = go_seq_go_to_refnum(self._ref);
int32_t r0 = proxyg_mb_test_GMBTest_GMBTest6(refnum);
G_mb_testGMBTest* _ret0_ = nil;
GoSeqRef* _ret0__ref = go_seq_from_refnum(r0);
if (_ret0__ref != NULL) {
_ret0_ = _ret0__ref.obj;
if (_ret0_ == nil) {
_ret0_ = [[G_mb_testGMBTest alloc] initWithRef:_ret0__ref];
}
}
return _ret0_;
}

proxyg_mb_test_GMBTest_GMBTest6()方法返回了一个int值,这个值必然是对应的Golang的对象。

1
2
3
4
5
6
7
8
9
10
11
//export proxyg_mb_test_GMBTest_GMBTest6
func proxyg_mb_test_GMBTest_GMBTest6(refnum C.int32_t) C.int32_t {
ref := _seq.FromRefNum(int32(refnum))
v := ref.Get().(*g_mb_test.GMBTest)
res_0 := v.GMBTest6()
var _res_0 C.int32_t = _seq.NullRefNum
if res_0 != nil {
_res_0 = C.int32_t(_seq.ToRefNum(res_0))
}
return _res_0
}

方法调用的地方我们不再详述,返回的res_0如果是nil,则会返回_seq.NullRefNum = 41。即用一个特定的值来表示null。
返回后OC同样的会在go_seq_from_refnum()中判断41这个值。

总结一下:
Go返回对象实际是返回一个refnum值,OC使用GoSeqRef来存储这个值。(返回的对象始终是Go已导出的对象类型)

Golang返回接口类型

实际上Go和其他语言通讯大部分都会使用接口类型

1
2
3
4
5
6
7
8
9
10
11
12
13
- (id<G_mb_testGMBInterface>)gmbTest5 {
int32_t refnum = go_seq_go_to_refnum(self._ref);
int32_t r0 = proxyg_mb_test_GMBTest_GMBTest5(refnum);
G_mb_testGMBInterface* _ret0_ = nil;
GoSeqRef* _ret0__ref = go_seq_from_refnum(r0);
if (_ret0__ref != NULL) {
_ret0_ = _ret0__ref.obj;
if (_ret0_ == nil) {
_ret0_ = [[G_mb_testGMBInterface alloc] initWithRef:_ret0__ref];
}
}
return _ret0_;
}

proxyg_mb_test_GMBTest_GMBTest5()和上面的proxyg_mb_test_GMBTest_GMBTest6()返回对象没有什么差别,主要差别在于处理返回的值变成go中的refnum。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation G_mb_testGMBInterface {
}

- (instancetype)initWithRef:(id)ref {
self = [super init];
if (self) { __ref = ref; }
return self;
}

- (void)interfaceTest {
int32_t refnum = go_seq_go_to_refnum(self._ref);
proxyg_mb_test_GMBInterface_InterfaceTest(refnum);
}

@end

在g_mb_test_darwin.m中自动生成了一个G_mb_testGMBInterface类,而这个类实现了G_mb_testGMBInterface,也就是我们定义的接口,并且存储了GO返回的refnum。而接口的实现内调用了proxyg_mb_test_GMBInterface_InterfaceTest()函数。这个函数实际就是通过返回的refnum来获取Go中的对象,调用Go中实现的接口。

总结:
Go返回的接口类型,在OC上还是一个refnum。胶水代码自动生成了一个实现这个接口的类,并且保存了这个GoSeqRef。在调用接口的时候,实际上是调用的这个自动生成的类的方法。最终还是通过存储的GoSeqRef找到Go中的对象,来调用具体方法。

至此,Go方法返回数据都已经讲完了(NSData和NSString我们放在单独的环节)。接下来我们看一下从OC传对象至Go

OC传递对象至Go

这一节我们将补充讲完之前跳过的环节。

1
2
3
4
5
6
7
8
9
10
11
- (void)gmbTest7:(id<G_mb_testGMBInterface>)i {
int32_t refnum = go_seq_go_to_refnum(self._ref);
int32_t _i;
if ([i conformsToProtocol:@protocol(goSeqRefInterface)]) {
id<goSeqRefInterface> i_proxy = (id<goSeqRefInterface>)(i);
_i = go_seq_go_to_refnum(i_proxy._ref);
} else {
_i = go_seq_to_refnum(i);
}
proxyg_mb_test_GMBTest_GMBTest7(refnum, _i);
}

goSeqRefInterface接口表示的是Go传出的对象(因为有GoSeqRef)。
如果是Go的对象,则增加引用计数。
如果不是Go的对象,即OC new出来的对象,调用go_seq_to_refnum()

方法内部实际是调用的全局变量tracker。
看到这里其实已经明白了,在OC里实现了一个和Go中refs一样的结构。用来标识从OC传入Go的对象的引用计数。
Go在OC里的引用计数是,每次在调用Go对象之前持有一次,在Go由refnum找到对应对象之后,再释放一次。那么OC的对象呢?

继续看到下一个方法proxyg_mb_test_GMBTest_GMBTest7()的参数传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//export proxyg_mb_test_GMBTest_GMBTest7
func proxyg_mb_test_GMBTest_GMBTest7(refnum C.int32_t, param_i C.int32_t) {
ref := _seq.FromRefNum(int32(refnum))
v := ref.Get().(*g_mb_test.GMBTest)
var _param_i g_mb_test.GMBInterface
_param_i_ref := _seq.FromRefNum(int32(param_i))
if _param_i_ref != nil {
if param_i < 0 { // go object
_param_i = _param_i_ref.Get().(g_mb_test.GMBInterface)
} else { // foreign object
_param_i = (*proxyg_mb_test_GMBInterface)(_param_i_ref)
}
}
v.GMBTest7(_param_i)
}

之前提到过,_seq.FromRefNum只判断null(41),否则返回一个Ref指针。如果传入的参数是Go生成的,那么就直接使用。否则,如果是外部传入的,直接把_param_i_ref强转为了proxyg_mb_test_GMBInterface类型。

看到后面还有一个对proxyg_mb_test_GMBInterface 的定义

1
2
3
4
5
6
7
type proxyg_mb_test_GMBInterface _seq.Ref

func (p *proxyg_mb_test_GMBInterface) Bind_proxy_refnum__() int32 { return (*_seq.Ref)(p).Bind_IncNum() }

func (p *proxyg_mb_test_GMBInterface) InterfaceTest() {
C.cproxyg_mb_test_GMBInterface_InterfaceTest(C.int32_t(p.Bind_proxy_refnum__()))
}

两个方法:
Bind_proxy_refnum__()调用了Ref的Bind_IncNum(),看名字和注释的意思是增加外部变量的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func init() {
_seq.FinalizeRef = func(ref *_seq.Ref) {
refnum := ref.Bind_Num
if refnum < 0 {
panic(fmt.Sprintf(“not a foreign ref: %d”, refnum))
}
C.go_seq_dec_ref(C.int32_t(refnum))
}
_seq.IncForeignRef = func(refnum int32) {
if refnum < 0 {
panic(fmt.Sprintf("not a foreign ref: %d", refnum))
}
C.go_seq_inc_ref(C.int32_t(refnum))
}
}

在初始化的时候,会给seq设置两个全局函数,一个是释放,另一个就是刚刚Bind_IncNum()函数内调用的_seq.IncForeignRef(),
C.cproxyg_mb_test_GMBInterface_InterfaceTest(),这个两个方法内实际调用了Cgo_seq_inc_ref()和C.go_seq_dec_ref(),这个是链接静态库的实现的符号。

1
2
3
4
5
6
7
8
9
10
11
12
void go_seq_dec_ref(int32_t refnum) {
@autoreleasepool {
[tracker dec:refnum];
}
}

void go_seq_inc_ref(int32_t refnum) {
@autoreleasepool {
[tracker inc:refnum];
}
}

seq_darwin.m中可以看到,确实是增加和减少引用计数。

在上面讲到FromRefNum的时候,如果是外部传入的对象,则会给GoRef设置一个析构函数。
也就是说,在GoRef这个结构体的生命周期内,持有外部的对象。GoRef 映射的就是外部对象。

继续回到上面,InterfaceTest()是接口的另一个方法

1
2
3
4
5
6
void cproxyg_mb_test_GMBInterface_InterfaceTest(int32_t refnum) {
@autoreleasepool {
G_mb_testGMBInterface* o = go_seq_objc_from_refnum(refnum);
[o interfaceTest];
}
}

和_seq.FinalizeRef一样,调用到了oc。而OC的go_seq_objc_from_refnum()方法在拿到oc对象之后,调用了[tracker dec:refnum];
这样,在OC层完成了这个对象的释放

总结:
OC的接口对象传递至Go的时候(非Go导出对象),Go肯定也是以一个接口对象存在。OC在传入之前计数+1(计数为1),传入之后,Go对生成的GoRef增加析构函数,析构函数内对计数 -1。
在Go内调用GoRef重定义的接口函数时候,会先增加引用计数,调回OC之后,在OC取到对应的OC对象后,OC会主动对计数-1。这一点和Go传出对象很类似。

另外,刚刚我们跳过了go_seq_from_refnum()以及ToRefNum()的代码,都是考虑的特殊情况。我们刚刚讨论的都是OC创建的对象传递至Go,Go创建的对象传递至OC。但是还有OC传递至Go后,Go在把同一个对象返回给OC。相反也有Go给OC后,OC把同一个对象给了Go。(这个同一个对象,在OC内是指GoSeqRef,在Go内是指GoRef,两侧都是镜像的。)对于这种情况,两侧都是不需要转换的,OC内直接从GoSeqRef取出obj调用,Go内直接找到refnum对应的对象调用。

总结

  1. Go导出的静态库实际是一个C实现的静态库,还会生成OC的代码(胶水代码)来包裹这个库,以达到导出OC类、方法和接口
  2. Go的对象和OC对象在对方侧,都一个int32(refnum)作为对象参数,来对应每一个传递过去的对象。如果refnum>0表示OC对象,<0表示Go对象。
  3. Go的对象和OC对象在对方侧,分别有一个存储了refnum的GoRef struct和GoSeqRef class来表示对方侧的对象。
  4. Go和OC侧都有全局变量来维护已经传入至对方侧的对象,以及引用计数。
  5. 不论是OC还是Go,如果refnum表示的是本侧的对象,那么就直接从全局变量中取出对象使用。
  6. OC调用Go对象方法,实际是调用Go的C函数,通过传递refnum告知Go具体的Go对象。
  7. Go调用OC对象方法,实际是通过声明式,调用C函数,由.m实现。同理通过传递refnum来告知OC具体的OC对象。
  8. OC调用Go的接口对象,实际是生成了实现接口的类的胶水代码,接口对象持有了GoSeqRef,接口方法实际也是通过传递refnum调用Go
  9. Go调用OC的接口对象,实际是通过声明式,调用C函数,由.m实现。通过重定义GoRef,实现OC的接口方法。接口方法内通过传递refnum调用.m即OC
  10. Go传递至OC的对象,Go在new或者函数返回之前会调用_seq.ToRefNum(),存储在全局变量中,并计数为1。OC存储的GoSeqRef在dealloc的时候通过DestroyRef()将计数-1。
  11. OC传递至Go的对象,OC在传递给Go之前会调用assignRefnumAndIncRefcount:存储在全局变量中,并计数为1。Go在生成GoRef的时候会设置该对象的析构回调,在回调内,通过调用go_seq_dec_ref()将计数-1
  12. Go对象方法被OC调用之前,OC会使用go_seq_go_to_refnum()将计数+1,Go在将refnum转换为go obj之后,又会将计数-1
  13. OC对象方法被Go调用之前,Go会使用Bind_IncNum()将计数+1,OC在将refnum转换为oc obj 之后,又会将计数-1。

以上的总结已经完全可以解答文章开始时候的提问了。

//最后这里回头补充一个图解

后记

还有两个特殊的地方 NSString 和 NSData。这两个对象在Go <—> OC gomobile 是默认支持这两种数据类型的互转的。

1
2
3
4
5
6
7
8
9
10
typedef struct nstring {
void *ptr;
int len;
} nstring;

typedef struct nbyteslice {
void *ptr;
int len;
} nbyteslice;
typedef int nint;

这两个数据类型在传递的时候,都会被转换为定义好的C语言的struct类型。

String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// encodeString copies a Go string and returns it as a nstring.
func encodeString(s string) C.nstring {
n := C.int(len(s))
if n == 0 {
return C.nstring{}
}
ptr := C.malloc(C.size_t(n))
if ptr == nil {
panic("encodeString: malloc failed")
}
copy((*[1<<31 - 1]byte)(ptr)[:n], s)
return C.nstring{ptr: ptr, len: n}
}

// decodeString converts a nstring to a Go string. The
// data in str is freed after use.
func decodeString(str C.nstring) string {
if str.ptr == nil {
return ""
}
s := C.GoStringN((*C.char)(str.ptr), str.len)
C.free(str.ptr)
return s
}

Go 侧 string 通过encode和decode方法,在cstring和go string之间互转,每次转换都会copy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
NSString *go_seq_to_objc_string(nstring str) {
if (str.len == 0) { // empty string.
return @“”;
}
NSString * res = [[NSString alloc] initWithBytesNoCopy:str.ptr
length:str.len
encoding:NSUTF8StringEncoding
freeWhenDone:YES];
return res;
}

nstring go_seq_from_objc_string(NSString *s) {
nstring res = {NULL, 0};
int len = [s lengthOfBytesUsingEncoding:NSUTF8StringEncoding];

if (len == 0) {
if (s.length > 0) {
LOG_INFO(@“unable to encode an NSString into UTF-8”);
}
return res;
}

char *buf = (char *)malloc(len);
if (buf == NULL) {
LOG_FATAL(@"malloc failed");
}
NSUInteger used;
[s getBytes:buf
maxLength:len
usedLength:&used
encoding:NSUTF8StringEncoding
options:0
range:NSMakeRange(0, [s length])
remainingRange:NULL];
res.ptr = buf;
res.len = used;
return res;
}

OC 侧 NSString 通过go_seq_to_objc_string和go_seq_from_objc_string方法,在cstring和nsstring之间互转,每次转换都会copy

且对方侧生成的cstring由己方释放。

Bytes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

// fromSlice converts a slice to a nbyteslice.
// If cpy is set, a malloc’ed copy of the data is returned.
func fromSlice(s []byte, cpy bool) C.nbyteslice {
if s == nil || len(s) == 0 {
return C.nbyteslice{}
}
ptr, n := unsafe.Pointer(&s[0]), C.int(len(s))
if cpy {
nptr := C.malloc(C.size_t(n))
if nptr == nil {
panic("fromSlice: malloc failed")
}
copy((*[1<<31 - 1]byte)(nptr)[:n], (*[1<<31 - 1]byte)(ptr)[:n])
ptr = nptr
}
return C.nbyteslice{ptr: ptr, len: n}
}

// toSlice takes a nbyteslice and returns a byte slice with the data. If cpy is
// set, the slice contains a copy of the data. If not, the generated Go code
// calls releaseByteSlice after use.
func toSlice(s C.nbyteslice, cpy bool) []byte {
if s.ptr == nil || s.len == 0 {
return nil
}
var b []byte
if cpy {
b = C.GoBytes(s.ptr, C.int(s.len))
C.free(s.ptr)
} else {
b = (*[1<<31 - 1]byte)(unsafe.Pointer(s.ptr))[:s.len:s.len]
}
return b
}

Go 侧 nbyteslice 通过fromSlice和toSlice方法,在nbyteslice和go slice之间互转。

OC -> Go 如果对方(OC)copy == true,Go也会拷贝,拷贝完就会释放nbyteslice,否则只是使用。
Go -> OC (copy 才会拷贝,go->OC copy=true)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
NSData *go_seq_to_objc_bytearray(nbyteslice s, int copy) {
if (s.ptr == NULL) {
return NULL;
}
BOOL freeWhenDone = copy ? YES : NO;
return [NSData dataWithBytesNoCopy:s.ptr length:s.len freeWhenDone:freeWhenDone];
}

nbyteslice go_seq_from_objc_bytearray(NSData *data, int copy) {
struct nbyteslice res = {NULL, 0};
int sz = data.length;
if (sz == 0) {
return res;
}
void *ptr;
// If the argument was not a NSMutableData, copy the data so that
// the NSData is not changed from Go. The corresponding free is called
// by releaseByteSlice.
if (copy || ![data isKindOfClass:[NSMutableData class]]) {
void *arr_copy = malloc(sz);
if (arr_copy == NULL) {
LOG_FATAL(@"malloc failed");
}
memcpy(arr_copy, [data bytes], sz);
ptr = arr_copy;
} else {
ptr = (void *)[data bytes];
}
res.ptr = ptr;
res.len = sz;
return res;
}

OC 侧 NSData 通过go_seq_to_objc_bytearray和go_seq_from_objc_bytearray方法,在nbyteslice和NSData之间互转。

OC -> Go (NSMutableData && !copy 才不会拷贝,OC->Go copy = false,即NSData拷贝,NSMutableData不拷贝),并且方法调用结束就会释放bytes。(如果在Go内使用slice的时候切协程了,有大概率读取到的bytes是被释放了)
Go -> OC OC不拷贝从Go返回的bytes,OC在释放NSData的时候会释放nbyteslice.

总之一个宗旨,一侧指定了copy,就由另一侧释放。没有指定copy,就由本侧释放,即谁创建谁释放的原则。(NSData是指定了false,但是执行拷贝了,还是由本侧释放)


2020-3-18 记
用 Dart 来写 Objective-C 代码

这篇文章讲述的是在Dart中写OC,与GoMobile的原理相比有些许相似…