函数的视角

很多时候,我们都在做翻译工作。

跨语言函数调用

比如说跨语言的函数调用,就是在不同语言之间通过在某一个语言端的中介的API做函数调用翻译工作。

  • JNI,通过Java Native Interface,我们的本地代码直接调用JVM(Java Virtual Machine)的指针和Java代码之间跨语言交互。实际上对于JNI代码来说,并不是和Java代码交互,而是和JNIEnv的API交互。
  • Lua,通过Lua的C函数接口,我们可以给Lua注册一堆的C接口。我们的本地代码直接调用的lua_State* 和Lua代码之间跨语言交互。实际上对于Lua的C接口函数来说,并不是和Lua代码交互,而是在lua_State的API交互。
  • P/Inovke,通过.NET P/Invoke,我们可以在.NET里调用C的动态链接库。本地代码只要是标准C导出接口即可。

对比之下,区别在于谁来做

  • Lua是固定了接口,参数传递基于lua_State的栈去传递。lua_State做查找工作,程序员手工做了翻译以及参数的出栈、入栈。
  • JNI是让C++接口去声明参数类型信息。编译器根据用户在Java和C端声明,做了查找工作。
  • P/Invoke是让.NET自己去声明参数类型信息。编译器根据用户的声明做了自动查找翻译工作。

这三者做做了两件事情

  1. 约定参数类型映射。
    • JNI是在C++端声明Java的参数类型所映射的本地类型。
    • P/Invoke是在.NET端声明本地类型到.NET类型的映射
    • Lua是在C/C++端让程序员直接隐式假设lua_State栈上每个槽应该放的是什么类型的数据。
  2. 约定参数入栈顺序
    • JNI和P/Invoke遵循C/C++的调用约定
    • Lua则规定了参数的出栈入栈规则

回顾一下和函数调用约定相关的有cdcel、stdcall、fastcall等。下面的图在汇编层演示了这种过程。

所以,逻辑上跨语言调用做的是在语言之间规定怎样调用对方的函数的规则,是属于函数调用的范畴。

我们考虑每一个函数都有输入参数和返回值。函数可以有单参数和多参数,而多参数可以通过柯里化变成单参数。
所以,函数问题可以简化为单参数单返回值函数的问题。可以用符号表示 r=f(a)

这假设了f总是顺序执行。我们可以通过Continuation的概念把函数表示为f(a,c)的形式,
f接收a,同步或者异步执行完毕后把结果传递给Continuation,也就是c去处理,c的一个例子是
function(r) print(r) end

网络协议

任何和约定相关的,都可以称之为协议。和协议伴生的一般就是协议包,和协议包伴生的一般就是协议包的编码解码。可以看下典型的网络协议。

  • TCP协议和TCP协议包;
  • Udp协议和Udp协议包;
  • 基于TCP的Http协议和Http协议包。
  • 基于Udp的P2P协议和P2P协议包;

一个网络协议是建立在发送一个包a,返回一个包r这样的基本操作之上的。比如说TCP握手:

client a         client b
send syn   ----> recv syn
recv ack   <---- send ack
send ackack----> recv ackack     

我们可以定义函数
send(request,function(response) end)

则TCP握手可以表示为

--client a                       
send(syn,function(ack) 
  send(ack,ignoreresponse)
  send(data,function(dataack)
     send(data,function(dataack)
      ...
     end)  
  end)
end)

--client b
send(nothing,function(syn)
  send(ack,function(ackack)
     send(data,function(dataack)
        ...
     end) 
  end)
end)

Udp协议和TCP协议的区别在于,TCP处理了每个函数调用的异常,例如超时没有返回,我们可以调整send函数原型

send(request,function(response) 
     --// 返回了期待的值 
  end,function(timeout) 
     --// 没收到期待的值,某种重发机制
  end,function(error)
     --// 彻底超时了,失败
  end)

所以,网络上发包和收包可以规约为函数调用。

ps:一个网络协议包可以拆开成包头和包体,而一条汇编指令、一条.NET的IL指令,一条JVM的bytecode,一个Lua的指令,也同样可以有进一步拆开成opcode+register。可以把一个网络协议包看成一条网络汇编指令
网络协议包的包头一般包含有packagetype,可以当作是opcode,包体则是register里的数据。
ps:编程语言的指令,都喜欢把int拆开去使用,前几个位表示什么,接着几个位表示什么。网络协议包也类似。我觉的对bit的操作是编程世界里的最低粒度的封装抽象

RPC

基于网络协议,就可以实现网络通信。在应用层,我们更喜欢把网络通信当成是函数调用了。于是有了RPC.
无论是Apache Thrift还是Google Protocol Buffer还是.NET WCF,都用某种代码生成器去生成网络通信封装成函数的事情。

函数、对象和闭包

无论是函数还是对象,都是一堆数据。不能闭包的语言里,函数只有代码数据。可以闭包的语言里,闭包了的函数除了代码数据还有被捕获的变量的数据(所谓upvalue,up是是upstream,也就是上游,可见只能捕获自己上游作用域的变量)。
在命令式语言里,对象是一个状态机,状态的维护在并发的时候遇到了data race的问题。在纯函数式语言里,数据是不可变的,无副作用的,并发的时候就消除了data race。在一个命令式语言里的函数式特性里还是会遇到data race。

references

posted @   ffl  阅读(497)  评论(1编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示