混合编程黑科技:跨语言编程问题迎刃而解的3个要点

本文探讨了混合编程的概念,解释了其背后的原理,通过Objective-C与JavaScript、Lua的混合编程案例,揭示了静态与动态binding的区别,并强调了混合编程中的核心问题:数值和对象转化、参数传递、生命周期管理。通过理解这些要点,可以更好地应对跨语言编程挑战。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

首先,混合编程是什么鬼?


这个世界上编程语言真不少,光常用就有:C、C++、Java、C#、Objective-C、Javascript、Python、Lua、Swift等等等,遑论一些专业性比较强的DSL了。而且软件的应用场景也数不胜数:嵌入式设备、后端服务器、桌面程序/GUI、移动端平台、Web、并行计算……


那么,如果某个场景下光靠一种语言无法满足业务需求该怎么办;亦或是某个依赖的库早已有其他语言编写的成熟可靠版本,重写完全划不来;再者有可能每一个开发者有自己偏好的开发语言,但却不得不一起协作?

我想,解决这些问题的最好方式,就是采用混合编程,也就是使用不止一种程序设计语言来开发应用程序这种方案。

那么,混合编程背后的原理是什么?


编程语言,以我浅薄的认识来说,大概本质上是对机器语言(1和0)的高度抽象,是基于不同思维方向和设计模式的抽象,所以才会被人为区分成过程式、面向对象、函数式等等不同类别。那么,这种甚至是基于不同思维层面而发明的语言,看似应该老死不相往来,如何才能相互调用呢?

幸好,计算机科学领域中有一个超重要又很实用的概念,那就是“分层”!有一句话怎么说来着?“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”。


以上面这张粗糙的图来说,假设存在语言1和语言2基于重要的分层概念,对于这两种语言的支持,肯定是从硬件、驱动、操作系统、编译器+运行时等等一层层叠加上来的,那么,如果想要跨这两种语言进行调用,要怎么办呢?

这时候,可以把每一种语言的运行支持比作一栋楼房(原谅我这不甚恰当的类比,但有没有发现其实二者架构神似?),那么现在想要达到的目标好比就是,把一条消息从一号楼的顶楼送到二号楼的顶楼,这完全可以用我们日常生活中的简单常识搞定对不对?很显然,要做的是,确定这两栋楼之间有没有天桥连接(一楼也可以被认为是0高度的天桥……),如果一号楼的楼层x与二号楼的楼层y之间存在一条路径的话,信使就可以首先从一号楼的顶楼下到楼层x,然后通过天桥到达二号楼的楼层y,最终上到二号楼的顶楼就达成了我们的目标!

上述这通看似废话的介绍暗示了什么呢?不就等同于,任意两种语言,若是在其运行环境支持的某一层上可以通过某种渠道某种技术可以进行无碍沟通的话,从原理上来说,就实现了二者相互调用的目的吗(似乎发现了什么了不得的东西……)

然后,来看看混合编程的具体案例

闲话休表,介绍一下本文的主角,在iOS开发中遇到的,混合编程的两种典型案例——Objective-C与javascript,以及Objective-c与lua。

关于Javascript(后面均简称JS)本文不再赘言,主要讲一讲iOS端有什么方式可以执行JS代码呢,相信从事iOS开发的诸位都了解UIWebView有如下方法:

  • (nullable NSString )stringByEvaluatingJavaScriptFromString:(NSString )script; 或者WKWebView的

  • (void)evaluateJavaScript:(NSString )javaScriptString completionHandler:(void (^ nullable)(nullable id, NSError __nullable error))completionHandler; `` 都可以来执行一段JS脚本代码字符串。当然,这是单向的调用,如果想要实现反向的调用怎么办呢? 为此在iOS 7中正式引入JavaScriptCore框架,可以大幅度简化Objective-C与JS之间的相互调用过程,几段简单的示例代码如下

//数值传递JSContext *context = [[JSContext alloc] init]; 
JSValue *jsVal = [context evaluateScript:@"21+7"]; 
int iVal = [jsVal toInt32]; 
NSLog(@"JSValue int: %d", iVal); //输出28
//数据结构传递JSContext *context = [[JSContext alloc] init]; 
[context evaluateScript:@"var arr = [21, 7 , 'www.163.com'];"]; 
JSValue *jsArr = context[@"arr"]; // Get array from JSContext NSLog(@"JS Array: %@; Length: %@", jsArr, jsArr[@"length"]); //输出为 JS Array: 21,7,www.163.com Length: 3jsArr[1] = @"blog"; // Use JSValue as array jsArr[7] = @7; 

NSLog(@"JS Array: %@; Length: %d", jsArr, [jsArr[@"length"] toInt32]); //输出 JS Array: 21,blog,www.163.com,,,,,7; Length: 8NSArray *nsArr = [jsArr toArray]; 
NSLog(@"NSArray: %@", nsArr);/* 输出NSArray: (
    21,
    blog,
    "www.163.com",
    "<null>",
    "<null>",
    "<null>",
    "<null>",
    7
)
*/
//方法引入JSContext *context = [[JSContext alloc] init];
context[@"log"] = ^() {    NSLog(@"+++++++Begin Log+++++++");    NSArray *args = [JSContext currentArguments];    for (JSValue *jsVal in args) {        NSLog(@"%@", jsVal);
    }

    JSValue *this = [JSContext currentThis];    NSLog(@"this: %@",this);    NSLog(@"-------End Log-------");
};

[context evaluateScript:@"log('wayne', [7, 21], { hello:'world', js:100 });"];/*输出为
+++++++Begin Log+++++++
wayne
7,21
[object Object]
this: [object GlobalObject]
-------End Log-------
*/

喔!看起来真的很简单,所有JS相关的变量对象都被封装成了JSContext和JSValue类型,看起来既清爽又简约。但这好比是Apple在刚才那两栋楼的楼顶上各搭建了一个黑黝黝的通道入口,开发者可以把东西扔进去,在另一个楼顶的通道出口就可以轻轻松松收到了,简直是魔法般的存在!但这种黑盒背后隐藏了大量的细节,而那句话怎么说来着?细节是魔鬼!

这种黑科技背后的实现原理是什么呢


既然不太甘心,那么不妨也自己动手尝试下如何实现从基础这一套神奇的通道,在此我选择的就是Objective-C和Lua这对组合了。首先简要介绍下主角Lua,它是一种优雅又简单易学的编程语言,支持自动内存管理、词法作用域、闭包、迭代器、协程(coroutine)、尾调用等特性,其变量引用规则采用词法作用域或者说静态作用域方式,而且数据结构特别简洁明了(只有table这一种,兼任数组和哈希表二种角色)。

下面简单的代码可以帮助大家熟悉一下Lua:

local saySomeThing = function()
    print("holy shit!")endlocal t = {10, "hello world!"}
t["func"] = saySomeThing 

local b = 100function add(one, another)    return one + anotherendlocal result = add(t[1], b)
print(" t[1] + b = ", result)//输出110t["func"]() //输出 holy shit!

另外,Lua具有很好的可扩展性和可嵌入特性,所以被称为胶水语言(glue language)。这么赞誉它是因为原始提供了设计良好的C API,可供开发者自行搭建C和Lua世界的通道。

那么,开始动手吧。 首先,假设我们需要把3个C函数导入到Lua去被调用,那么就得到了如下所示代码。他们似乎看起来有共同之处,那么就是除了方法名之外的函数签名是完全一样的,即传入参数一定是lua_State 类型,返回值一定是static int类型。没错,这就是Lua世界给出的一个强制约定,也就是所有满足如下签名要求的C函数才有被Lua的运行时接纳的资格。当然这种约束也是有目的的:传入的lua_State 类型变量即是Lua的运行上下文对象,可以从中提取从lua世界传入的各种参数;而返回的static int类型数值则是代表从C的世界中往Lua的世界中传入了多少个数据。

//C functionsstatic int saySomething(lua_State *L){    if (!L) {        return 0;
    }

    lua_pushstring(L, "Now Native Talking");    return 1;
}static int add(lua_State *L){    double first = lua_tonumber(L, 1);    double second = lua_tonumber(L, 2);

    lua_pushnumber(L, first + second);    return 1;
}static int transformToUpper(lua_State *L){    const char *str = lua_tostring(L, 1);    if (!str || strlen(str) == 0) {
        lua_pushstring(L, "");        return 1;
    }    char *transformed = NULL;

    size_t len = strlen(str);
    transformed = (char*)malloc(sizeof(char)*len + 1);    for (size_t i = 0 ; i < len; i ++) {
        transformed[i] = toupper(str[i]);
    }
    transformed[len] = '\0';

    lua_pushstring(L, transformed);
    lua_pushstring(L, str);    return 2;
}

然后让我们把这3个方法导入到Lua的世界中去。

//C 代码static const struct luaL_reg customLib[] = {
    {"add", add},
    {"saySomething", saySomething},
    {"transformToUpper", transformToUpper}
};

.....self.state = luaL_newstate();

luaL_openlibs(self.state);
luaL_register(self.state, "native", customLib);

而通过这个lua上下文对象执行的lua代码中就可以这么写:

-- lua代码,测试导入lua世界的native functions
-- call function without returnsnative.saySomething()
-- call function with a single number-typed returnlocal a = 10local b = 100local result = native.add(a, b)print(" a + b = ", result) --输出  a + b = 110-- call function with two string-typed returns     
local str = "hello world!"local transformed, original = native.transformToUpper(str)print("original:", original, " transformed:", transformed) --输出original:    hello world!     transformed:    HELLO WORLD!

看吧,现在已经成功地在C这栋“楼”和lua这栋“楼”之间搭建了一座(简陋的)通道了!当然,Objective-C由于本身是C语言的一个超集,所以可以利用C的中间层,实现Objective-C实现的类成员方法和Lua的相互调用,具体步骤对诸君而言都没什么难度,故在此不再赘言。

静态binding,还是动态binding?


上述这种通过明确的接口列表注册要导入Lua世界的C函数的方式,我们称其为静态binding。既然有静态的,那当然也会有动态的binding——而这仰赖于Objective-C强大的runtime特性,最典型的一个框架就是Wax。引用Wax之后,就避免了Objective-C的方法都必须预先导入才能从Lua中调用的繁琐,而是直接可以在lua脚本中编写如下代码就可以为Wax正确解析并执行:

-- lua code
view = UIView:initWithFrame(CGRect(0, 0, 320, 100))

-- all methods available to a UIView object can be accessed this way
view:setBackgroundColor(UIColor:redColor())

亦或

-- Created in "MyController.lua"--
-- Creates an Objective-C class called MyController with UIViewController-- as the parent. This is a real Objective-C object, you could even
-- reference it from Objective-C code if you wanted to.
waxClass{"MyController", UIViewController}

function init()
  -- to call a method on super, simply use self.super
  self.super:initWithNibName_bundle("MyControllerView.xib", nil)  return selfendfunction viewDidLoad()
  -- Do all your other stuff hereend

甚至支持block:

-- lua code
UIView:animateWithDuration_animations_completion(1, 
    toblock(        function()
            label:setCenter(CGPoint(300, 300))
        end
    ),    toblock(
             function(finished)
                print('lua animations completion ' .. tostring(finished))
            end
    ,{"void", "BOOL"}))

是不是和Objective-C代码在函数传递、普通和匿名方法写法上存在一些区别之外,几乎一模一样?这就是将Lua的元方法(Meta Method)特性和Objective-C强大的运行时能力组合起来得到的。

元方法+运行时调用这对组合好像很厉害


从名字来看,“元”这个字似乎是很强力的一种修饰,譬如元帅、元首、元气几个词莫不如是,那么元方法呢,其实也是同样。元方法其实是为为Lua中table对象设置的元表中关联方法的总称(基于lua5.1版本),其中很重要的两个就是 index 以及 newindex两个方法。通过key从table对象中获取关联的value失败时,index方法会被触发;而newindex则是在向table对象首次设置某key-value的键值对的时候会被触发。自然,可以设想在lua代码执行的时候,碰到不存在的变量譬如UIView时可以触发全局的元方法,而如果其元方法乃是native的C 函数,则可以将"UIView"这个key传入到native中区。 然后利用runtime方法譬如 NSClassFromString就可以获取的真正的UIView类;而调用的方法,可以通过同样的方式传入native,再利用 instanceMethodForSelector/methodForSelector/methodSignatureForSelector等获得相应的实现,并发起调用(其实为了兼容64位系统,最终是把函数调用封装为NSInvocation调用并去派发的,此处暂不多做介绍)。

在native中完成了类实例方法/类方法的调用获得了返回值之后,要怎样递送回lua的世界中呢?通常不同语言对于基础数值类型(primitive value)都有相应支持,以之前自定义的3个C函数为例,会发现其中有这样的逻辑:

lua_pushstring(L, "Now Native Talking"); lua_pushnumber(L, first + second);

很明显,是通过为不同基础类型变量提供显式传递方法以便将值传入到lua的世界中去。如果是Objective-C的类实例对象呢,为此,lua单独提供了userdata类型,可以将native对象封装为可以在lua世界中存在变量。

现在,我们可以实现在lua中触发native对象的生成,并将其封装成userdata传递会lua的世界,是不是就天下太平了?很明显,这里还存在一个问题,那就是同一个对象,可能同时在native和lua都被引用着,那么它的生命周期该如何控制呢?毕竟两个世界是遵循着不同的内存管理策略——Objective采用的是引用计数(ARC/MRC),而lua则是GC。解决这个问题同样是利用了lua元方法gc,若为userdata类型变量关联该方法,那么在其所有引用都已断开的时候,即lua将要对其进行对象销毁和内存回收时,会触发gc的调用;如此一来,可以在__gc的C函数实现中将native对象的引用计数做相应递减,代表lua世界也已不再引用该对象;可以按照Objective-C的正常规范,在其引用计数归零时触发native的回收机制。

总的来说,其简略架构图如下: 



静态、动态binding对比


同样是是实现不同语言之间的相互调用,但静态binding和动态binding采用完全不同的设计思想,设计上的差异同样会带来使用上的区别。

首先很明显的是,如果是静态binding,那么每一个导入另一种语言世界的方法都需要手动实现(但有些工具可以极大简化这种繁琐,例如SWIG);而动态binding则利用了Objective-C强大的runtime特性和lua的元方法特性,可以省却方法手动导入这一步骤。但动态binding这种便利总是有代价的,最大的问题就是性能消耗比静态binding要高出不少,这也是ColorTouch移动端UI开发框架采用静态binding为主,动态binding为辅的设计方案的主要原因。

混合编程的核心问题

上面啰啰嗦嗦说了好多,其实归结起来,若要实现混合编程,最主要的几个核心问题就是:

  1. 不同语言世界之间数值、对象、变量的转化;

  2. 为另一个语言函数传入的参数,以及从另一个语言世界获得方法返回值的处理;

  3. 跨语言世界存在的对象,其生命周期管理。

总的来说,这三点就是我个人认为,如果要处理跨语言的编程问题,要最为关注的要点。把握、解决了这些要点,其他的问题相信就会迎刃而解!

· EDN ·

作者:网易杭州研究院 · 魏炜

网易云信|IM快速开发黑科技

ID:neteaseim  长按识别,关注精彩

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值